import { describe, expect, it, vi } from 'vitest'; import { OpenCodeTeamRuntimeAdapter, type OpenCodeTeamRuntimeBridgePort, type TeamRuntimeLaunchInput, } from '../../../../src/main/services/team/runtime'; import type { OpenCodeTeamLaunchReadiness } from '../../../../src/main/services/team/opencode/readiness/OpenCodeTeamLaunchReadiness'; import type { OpenCodeLaunchTeamCommandData } from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract'; import type { PersistedTeamLaunchSnapshot } from '../../../../src/shared/types'; describe('OpenCodeTeamRuntimeAdapter', () => { it('maps readiness failures to a structured prepare block', async () => { const bridge = bridgePort( readiness({ state: 'mcp_unavailable', launchAllowed: false, missing: ['runtime_deliver_message'], diagnostics: ['OpenCode missing canonical app MCP tool id'], }) ); const adapter = new OpenCodeTeamRuntimeAdapter(bridge, { launchMode: 'production' }); await expect(adapter.prepare(launchInput())).resolves.toEqual({ ok: false, providerId: 'opencode', reason: 'mcp_unavailable', retryable: true, diagnostics: ['OpenCode missing canonical app MCP tool id', 'runtime_deliver_message'], warnings: [], }); expect(bridge.checkOpenCodeTeamLaunchReadiness).toHaveBeenCalledWith({ projectPath: '/repo', selectedModel: 'openai/gpt-5.4-mini', requireExecutionProbe: true, launchMode: 'production', }); }); it('uses runtime-only readiness for model-less preflight checks', async () => { const bridge = bridgePort(readiness({ state: 'ready', launchAllowed: true, modelId: null })); const adapter = new OpenCodeTeamRuntimeAdapter(bridge, { launchMode: 'production' }); await expect(adapter.prepare(launchInput({ model: undefined, runtimeOnly: true }))).resolves .toMatchObject({ ok: true, providerId: 'opencode', modelId: null, }); expect(bridge.checkOpenCodeTeamLaunchReadiness).toHaveBeenCalledWith({ projectPath: '/repo', selectedModel: null, requireExecutionProbe: false, launchMode: undefined, }); }); it('fails closed when launch mode is disabled', async () => { const bridge = bridgePort(readiness({ state: 'ready', launchAllowed: true })); const adapter = new OpenCodeTeamRuntimeAdapter( bridge ); await expect(adapter.prepare(launchInput())).resolves.toMatchObject({ ok: false, providerId: 'opencode', reason: 'opencode_team_launch_disabled', retryable: false, }); expect(bridge.checkOpenCodeTeamLaunchReadiness).not.toHaveBeenCalled(); }); it('maps ready bridge launch data to successful runtime evidence only with required checkpoints', async () => { const launchOpenCodeTeam = vi.fn(async () => ({ runId: 'run-1', teamLaunchState: 'ready', members: { alice: { sessionId: 'oc-session-1', launchState: 'confirmed_alive', runtimePid: 123, model: 'openai/gpt-5.4-mini', evidence: [ { kind: 'required_tools_proven', observedAt: '2026-04-21T00:00:00.000Z' }, { kind: 'delivery_ready', observedAt: '2026-04-21T00:00:00.000Z' }, { kind: 'member_ready', observedAt: '2026-04-21T00:00:00.000Z' }, { kind: 'run_ready', observedAt: '2026-04-21T00:00:00.000Z' }, ], }, }, warnings: [], diagnostics: [], }) satisfies OpenCodeLaunchTeamCommandData); const adapter = new OpenCodeTeamRuntimeAdapter( bridgePort(readiness({ state: 'ready', launchAllowed: true }), { getLastOpenCodeRuntimeSnapshot: vi.fn(() => ({ providerId: 'opencode' as const, binaryPath: '/opt/homebrew/bin/opencode', binaryFingerprint: 'version:1.14.19', version: '1.14.19', capabilitySnapshotId: 'cap-1', })), launchOpenCodeTeam, }), { launchMode: 'dogfood' } ); await expect(adapter.launch(launchInput())).resolves.toMatchObject({ runId: 'run-1', teamName: 'team-a', launchPhase: 'finished', teamLaunchState: 'clean_success', members: { alice: { providerId: 'opencode', launchState: 'confirmed_alive', sessionId: 'oc-session-1', runtimePid: 123, hardFailure: false, }, }, }); expect(launchOpenCodeTeam).toHaveBeenCalledWith( expect.objectContaining({ expectedCapabilitySnapshotId: 'cap-1', manifestHighWatermark: null, }) ); }); it('reconciles from existing persisted launch snapshot without treating OpenCode as truth', async () => { const snapshot = launchSnapshot(); const adapter = new OpenCodeTeamRuntimeAdapter( bridgePort(readiness({ state: 'adapter_disabled', launchAllowed: false })) ); await expect( adapter.reconcile({ runId: 'run-1', teamName: 'team-a', providerId: 'opencode', expectedMembers: launchInput().expectedMembers, previousLaunchState: snapshot, reason: 'startup_recovery', }) ).resolves.toMatchObject({ runId: 'run-1', teamName: 'team-a', launchPhase: 'active', teamLaunchState: 'partial_pending', members: { alice: { providerId: 'opencode', launchState: 'runtime_pending_bootstrap', runtimeAlive: true, bootstrapConfirmed: false, }, }, snapshot, }); }); it('keeps missing bridge members pending while reconcile is still launching', async () => { const reconcileOpenCodeTeam = vi.fn(async () => ({ runId: 'run-1', teamLaunchState: 'launching', members: { alice: { sessionId: 'oc-session-1', launchState: 'confirmed_alive', model: 'openai/gpt-5.4-mini', evidence: [{ kind: 'member_ready', observedAt: '2026-04-21T00:00:00.000Z' }], }, }, warnings: [], diagnostics: [], durableCheckpoints: [], manifestHighWatermark: null, runtimeStoreManifestHighWatermark: null, }) satisfies OpenCodeLaunchTeamCommandData); const adapter = new OpenCodeTeamRuntimeAdapter( bridgePort(readiness({ state: 'ready', launchAllowed: true }), { reconcileOpenCodeTeam, }), { launchMode: 'dogfood' } ); const result = await adapter.reconcile({ runId: 'run-1', teamName: 'team-a', providerId: 'opencode', expectedMembers: [ ...launchInput().expectedMembers, { name: 'bob', providerId: 'opencode', model: 'openai/gpt-5.4-mini', cwd: '/repo', }, ], previousLaunchState: launchSnapshot(), reason: 'startup_recovery', }); expect(result.teamLaunchState).toBe('partial_pending'); expect(result.members.alice?.launchState).toBe('confirmed_alive'); expect(result.members.bob).toMatchObject({ providerId: 'opencode', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, agentToolAccepted: false, bootstrapConfirmed: false, hardFailure: false, }); expect(result.members.bob?.diagnostics).toContain( 'OpenCode bridge response did not include bob; keeping the member pending until lane state materializes.' ); }); it('acknowledges stop without mutating live OpenCode ownership in the adapter shell', async () => { const adapter = new OpenCodeTeamRuntimeAdapter( bridgePort(readiness({ state: 'adapter_disabled', launchAllowed: false })) ); await expect( adapter.stop({ runId: 'run-1', teamName: 'team-a', providerId: 'opencode', reason: 'user_requested', previousLaunchState: launchSnapshot(), }) ).resolves.toMatchObject({ stopped: true, members: { alice: { providerId: 'opencode', stopped: true, }, }, }); }); it('maps permission-blocked bridge members to runtime_pending_permission instead of bootstrap pending', async () => { const launchOpenCodeTeam = vi.fn(async () => ({ runId: 'run-1', teamLaunchState: 'permission_blocked', members: { alice: { sessionId: 'oc-session-1', launchState: 'permission_blocked', pendingPermissionRequestIds: ['perm-1', 'perm-1', 'perm-2'], diagnostics: ['waiting for permission approval'], runtimePid: 123, model: 'openai/gpt-5.4-mini', evidence: [ { kind: 'required_tools_proven', observedAt: '2026-04-21T00:00:00.000Z' }, { kind: 'delivery_ready', observedAt: '2026-04-21T00:00:00.000Z' }, { kind: 'permission_blocked', observedAt: '2026-04-21T00:00:00.000Z' }, ], }, }, warnings: [], diagnostics: [], }) satisfies OpenCodeLaunchTeamCommandData); const adapter = new OpenCodeTeamRuntimeAdapter( bridgePort(readiness({ state: 'ready', launchAllowed: true }), { getLastOpenCodeRuntimeSnapshot: vi.fn(() => ({ providerId: 'opencode' as const, binaryPath: '/opt/homebrew/bin/opencode', binaryFingerprint: 'version:1.14.19', version: '1.14.19', capabilitySnapshotId: 'cap-1', })), launchOpenCodeTeam, }), { launchMode: 'dogfood' } ); const result = await adapter.launch(launchInput()); expect(result).toMatchObject({ teamLaunchState: 'partial_pending', members: { alice: { providerId: 'opencode', launchState: 'runtime_pending_permission', pendingPermissionRequestIds: ['perm-1', 'perm-2'], runtimeAlive: true, agentToolAccepted: true, bootstrapConfirmed: false, hardFailure: false, }, }, }); expect(result).toMatchObject({ members: { alice: { diagnostics: expect.arrayContaining(['waiting for permission approval']), }, }, }); }); it('keeps missing bridge members in bootstrap pending even when another member blocks on permission', async () => { const launchOpenCodeTeam = vi.fn(async () => ({ runId: 'run-1', teamLaunchState: 'permission_blocked', members: { alice: { sessionId: 'oc-session-1', launchState: 'permission_blocked', pendingPermissionRequestIds: ['perm-1'], diagnostics: ['waiting for permission approval'], runtimePid: 123, model: 'openai/gpt-5.4-mini', evidence: [ { kind: 'required_tools_proven', observedAt: '2026-04-21T00:00:00.000Z' }, { kind: 'delivery_ready', observedAt: '2026-04-21T00:00:00.000Z' }, { kind: 'permission_blocked', observedAt: '2026-04-21T00:00:00.000Z' }, ], }, }, warnings: [], diagnostics: [], }) satisfies OpenCodeLaunchTeamCommandData); const adapter = new OpenCodeTeamRuntimeAdapter( bridgePort(readiness({ state: 'ready', launchAllowed: true }), { getLastOpenCodeRuntimeSnapshot: vi.fn(() => ({ providerId: 'opencode' as const, binaryPath: '/opt/homebrew/bin/opencode', binaryFingerprint: 'version:1.14.19', version: '1.14.19', capabilitySnapshotId: 'cap-1', })), launchOpenCodeTeam, }), { launchMode: 'dogfood' } ); const result = await adapter.launch( launchInput({ expectedMembers: [ ...launchInput().expectedMembers, { name: 'bob', providerId: 'opencode', model: 'openai/gpt-5.4-mini', cwd: '/repo', }, ], }) ); expect(result.teamLaunchState).toBe('partial_pending'); expect(result.members.alice?.launchState).toBe('runtime_pending_permission'); expect(result.members.bob).toMatchObject({ providerId: 'opencode', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, agentToolAccepted: false, bootstrapConfirmed: false, hardFailure: false, pendingPermissionRequestIds: undefined, }); expect(result.members.bob?.diagnostics).toContain( 'OpenCode bridge response did not include bob; keeping the member pending until lane state materializes.' ); }); }); function bridgePort( readinessResult: OpenCodeTeamLaunchReadiness, overrides: Partial = {} ): OpenCodeTeamRuntimeBridgePort { return { checkOpenCodeTeamLaunchReadiness: vi.fn(async () => readinessResult), ...overrides, }; } function launchInput(overrides: Partial = {}): TeamRuntimeLaunchInput { return { runId: 'run-1', teamName: 'team-a', cwd: '/repo', providerId: 'opencode', model: 'openai/gpt-5.4-mini', skipPermissions: false, expectedMembers: [ { name: 'alice', providerId: 'opencode', model: 'openai/gpt-5.4-mini', cwd: '/repo', }, ], previousLaunchState: null, ...overrides, }; } function readiness( overrides: Partial = {} ): OpenCodeTeamLaunchReadiness { return { state: 'adapter_disabled', launchAllowed: false, modelId: 'openai/gpt-5.4-mini', availableModels: ['openai/gpt-5.4-mini'], opencodeVersion: '1.14.19', installMethod: 'brew', binaryPath: '/opt/homebrew/bin/opencode', hostHealthy: true, appMcpConnected: true, requiredToolsPresent: true, permissionBridgeReady: true, runtimeStoresReady: true, supportLevel: 'production_supported', missing: [], diagnostics: [], evidence: { capabilitiesReady: true, mcpToolProofRoute: '/experimental/tool/ids', observedMcpTools: ['agent-teams_runtime_deliver_message'], runtimeStoreReadinessReason: 'runtime_store_manifest_valid', }, ...overrides, }; } function launchSnapshot(): PersistedTeamLaunchSnapshot { return { version: 2, teamName: 'team-a', updatedAt: '2026-04-21T00:00:00.000Z', launchPhase: 'active', expectedMembers: ['alice'], teamLaunchState: 'partial_pending', summary: { confirmedCount: 0, pendingCount: 1, failedCount: 0, runtimeAlivePendingCount: 1, }, members: { alice: { name: 'alice', launchState: 'runtime_pending_bootstrap', agentToolAccepted: true, runtimeAlive: true, bootstrapConfirmed: false, hardFailure: false, lastEvaluatedAt: '2026-04-21T00:00:00.000Z', diagnostics: ['waiting for teammate check-in'], }, }, }; }