agent-ecosystem/test/main/services/team/OpenCodeReadinessBridge.test.ts

744 lines
22 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import {
OpenCodeReadinessBridge,
type OpenCodeReadinessBridgeCommandExecutor,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeReadinessBridge';
import {
REQUIRED_AGENT_TEAMS_APP_TOOL_IDS,
} from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
import type { OpenCodeTeamLaunchReadiness } from '../../../../src/main/services/team/opencode/readiness/OpenCodeTeamLaunchReadiness';
import type {
OpenCodeBridgeFailureKind,
OpenCodeBridgeCommandName,
OpenCodeBridgeResult,
OpenCodeBridgeSuccess,
OpenCodeLaunchTeamCommandData,
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
describe('OpenCodeReadinessBridge', () => {
it('executes the read-only opencode.readiness command and returns readiness data', async () => {
const readinessResult = readiness({ state: 'ready', launchAllowed: true });
const executor = fakeExecutor(bridgeSuccess(readinessResult));
const bridge = new OpenCodeReadinessBridge(executor, { timeoutMs: 15_000 });
await expect(
bridge.checkOpenCodeTeamLaunchReadiness({
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: true,
})
).resolves.toBe(readinessResult);
expect(executor.execute).toHaveBeenCalledWith(
'opencode.readiness',
{
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: true,
},
{
cwd: '/repo',
timeoutMs: 15_000,
}
);
expect(bridge.getLastOpenCodeRuntimeSnapshot('/repo')).toMatchObject({
capabilitySnapshotId: 'cap-1',
version: '1.14.19',
});
});
it('maps bridge failures into fail-closed readiness', async () => {
const executor = fakeExecutor(
bridgeFailure('timeout', 'OpenCode readiness command timed out', [
{
id: 'diag-1',
type: 'opencode_bridge_unknown_outcome',
providerId: 'opencode',
severity: 'warning',
message: 'timed out',
createdAt: '2026-04-21T12:00:00.000Z',
},
])
);
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.checkOpenCodeTeamLaunchReadiness({
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: false,
})
).resolves.toMatchObject({
state: 'unknown_error',
launchAllowed: false,
modelId: 'openai/gpt-5.4-mini',
hostHealthy: false,
requiredToolsPresent: false,
missing: ['OpenCode readiness command timed out'],
diagnostics: [
'OpenCode readiness bridge failed: timeout: OpenCode readiness command timed out',
'opencode_bridge_unknown_outcome: timed out',
],
});
expect(bridge.getLastOpenCodeRuntimeSnapshot('/repo')).toBeNull();
});
it('executes host cleanup through the direct bridge command', async () => {
const executor = fakeExecutor(
bridgeCommandSuccess({
command: 'opencode.cleanupHosts',
requestId: 'cleanup-req-1',
data: {
cleaned: 1,
remaining: 0,
hosts: [
{
hostKey: 'host-key',
projectPath: '/repo',
pid: 123,
port: 43116,
action: 'disposed',
reason: 'stale host has no active leases during startup',
leaseCount: 0,
},
],
diagnostics: [],
},
})
);
const bridge = new OpenCodeReadinessBridge(executor, { cleanupTimeoutMs: 5_000 });
await expect(
bridge.cleanupOpenCodeHosts({
reason: 'startup',
mode: 'stale',
projectPath: '/repo',
staleAgeMs: 1_000,
})
).resolves.toMatchObject({
cleaned: 1,
remaining: 0,
});
expect(executor.execute).toHaveBeenCalledWith(
'opencode.cleanupHosts',
{
reason: 'startup',
mode: 'stale',
projectPath: '/repo',
staleAgeMs: 1_000,
},
{
cwd: '/repo',
timeoutMs: 5_000,
}
);
});
it('gives observeMessageDelivery enough time for OpenCode plain-text fallback reconciliation', async () => {
const executor = fakeExecutor(
bridgeCommandSuccess({
command: 'opencode.observeMessageDelivery',
requestId: 'observe-req-1',
data: {
observed: true,
memberName: 'tom',
sessionId: 'session-tom',
diagnostics: [],
responseObservation: {
state: 'responded_plain_text',
deliveredUserMessageId: 'user-message-1',
assistantMessageId: 'assistant-message-1',
toolCallNames: ['message_send'],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: 'plain_assistant_text',
latestAssistantPreview: 'GAUNTLET_CONCURRENT_TOM_OK_1',
reason: 'assistant_replied_with_plain_text',
},
},
})
);
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.observeOpenCodeTeamMessageDelivery({
teamId: 'team-a',
teamName: 'team-a',
laneId: 'primary',
runId: 'run-1',
projectPath: '/repo',
memberName: 'tom',
messageId: 'gauntlet-concurrent-tom-1',
prePromptCursor: 'cursor-before',
})
).resolves.toMatchObject({
observed: true,
responseObservation: {
state: 'responded_plain_text',
latestAssistantPreview: 'GAUNTLET_CONCURRENT_TOM_OK_1',
},
});
expect(executor.execute).toHaveBeenCalledWith(
'opencode.observeMessageDelivery',
{
teamId: 'team-a',
teamName: 'team-a',
laneId: 'primary',
runId: 'run-1',
projectPath: '/repo',
memberName: 'tom',
messageId: 'gauntlet-concurrent-tom-1',
prePromptCursor: 'cursor-before',
},
{
cwd: '/repo',
timeoutMs: 20_000,
}
);
});
it('executes OpenCode task ledger backfill through a direct read-only bridge command', async () => {
const executor = fakeExecutor(
bridgeCommandSuccess({
command: 'opencode.backfillTaskLedger',
requestId: 'backfill-req-1',
data: {
schemaVersion: 1,
providerId: 'opencode',
teamName: 'team-a',
taskId: 'task-1',
projectDir: '/claude/project',
workspaceRoot: '/repo',
dryRun: false,
scannedSessions: 1,
scannedToolparts: 2,
candidateEvents: 2,
importedEvents: 2,
skippedEvents: 0,
outcome: 'imported',
notices: [],
diagnostics: [],
},
})
);
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.backfillOpenCodeTaskLedger({
teamName: 'team-a',
taskId: 'task-1',
taskDisplayId: 'abc12345',
projectDir: '/claude/project',
workspaceRoot: '/repo',
deliveryContextPath: '/tmp/claude-team-opencode-ledger-context-test/delivery-context.json',
deliveryContextHash: 'a'.repeat(64),
})
).resolves.toMatchObject({
outcome: 'imported',
importedEvents: 2,
});
expect(executor.execute).toHaveBeenCalledWith(
'opencode.backfillTaskLedger',
{
teamName: 'team-a',
taskId: 'task-1',
taskDisplayId: 'abc12345',
projectDir: '/claude/project',
workspaceRoot: '/repo',
deliveryContextPath: '/tmp/claude-team-opencode-ledger-context-test/delivery-context.json',
deliveryContextHash: 'a'.repeat(64),
},
{
cwd: '/repo',
timeoutMs: 45_000,
stdoutLimitBytes: 2_000_000,
stderrLimitBytes: 512_000,
}
);
});
it('does not query commandStatus on successful OpenCode sendMessage', async () => {
const executor = fakeExecutor(
bridgeCommandSuccess({
command: 'opencode.sendMessage',
requestId: 'send-req-1',
data: {
accepted: true,
memberName: 'bob',
sessionId: 'session-bob',
diagnostics: [],
},
})
);
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.sendOpenCodeTeamMessage({
teamId: 'team-a',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
projectPath: '/repo',
memberName: 'bob',
text: 'hello',
messageId: 'message-1',
deliveryAttemptId: 'ledger-1:1:payload',
})
).resolves.toMatchObject({
accepted: true,
sessionId: 'session-bob',
});
expect(executor.execute).toHaveBeenCalledOnce();
expect(executor.execute).toHaveBeenCalledWith(
'opencode.sendMessage',
expect.objectContaining({
deliveryAttemptId: 'ledger-1:1:payload',
payloadHash: expect.any(String),
}),
expect.objectContaining({
cwd: '/repo',
timeoutMs: 45_000,
requestId: expect.stringMatching(/^opencode-send-/),
})
);
});
it('falls back to observed sendMessage when acceptance capability is missing', async () => {
const execute = vi
.fn()
.mockRejectedValueOnce(
new Error(
'OpenCode delivery acceptance mode is required, but the orchestrator does not advertise contract version 1.'
)
)
.mockResolvedValueOnce(
bridgeCommandSuccess({
command: 'opencode.sendMessage',
requestId: 'send-req-observed',
data: {
accepted: true,
memberName: 'bob',
sessionId: 'session-bob',
diagnostics: [],
},
})
);
const executor = {
execute: execute as unknown as OpenCodeReadinessBridgeCommandExecutor['execute'] &
ReturnType<typeof vi.fn>,
};
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.sendOpenCodeTeamMessage({
teamId: 'team-a',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
projectPath: '/repo',
memberName: 'bob',
text: 'hello',
messageId: 'message-1',
deliveryAttemptId: 'ledger-1:1:payload',
settlementMode: 'acceptance',
})
).resolves.toMatchObject({
accepted: true,
sessionId: 'session-bob',
diagnostics: [
expect.objectContaining({
code: 'opencode_accept_fast_capability_missing',
severity: 'warning',
}),
],
});
expect(execute).toHaveBeenCalledTimes(2);
expect(execute.mock.calls[0]?.[1]).toMatchObject({ settlementMode: 'acceptance' });
expect(execute.mock.calls[1]?.[1]).toMatchObject({ settlementMode: 'observed' });
expect(execute.mock.calls[1]?.[2]).toMatchObject({
requestId: expect.stringMatching(/-observed$/),
});
});
it('recovers accepted OpenCode sendMessage after bridge timeout through commandStatus by default', async () => {
const executor = fakeSequenceExecutor([
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
bridgeCommandSuccess({
command: 'opencode.commandStatus',
requestId: 'status-req-1',
data: {
status: 'prompt_accepted',
safeToRetry: false,
accepted: true,
sessionId: 'session-bob',
runtimePromptMessageId: 'msg_prompt_1',
diagnostics: ['OpenCode prompt acceptance recovered from offline_sqlite.'],
},
}),
]);
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.sendOpenCodeTeamMessage({
teamId: 'team-a',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
projectPath: '/repo',
memberName: 'bob',
text: 'hello',
messageId: 'message-1',
deliveryAttemptId: 'ledger-1:1:payload',
})
).resolves.toMatchObject({
accepted: true,
sessionId: 'session-bob',
diagnostics: expect.arrayContaining([
expect.objectContaining({
code: 'opencode_send_recovered_after_bridge_timeout',
}),
]),
});
expect(executor.execute).toHaveBeenCalledTimes(2);
const sendOptions = executor.execute.mock.calls[0]?.[2] as { requestId?: string } | undefined;
expect(executor.execute.mock.calls[1]).toEqual([
'opencode.commandStatus',
expect.objectContaining({
originalCommand: 'opencode.sendMessage',
originalRequestId: sendOptions?.requestId,
deliveryAttemptId: 'ledger-1:1:payload',
payloadHash: expect.any(String),
}),
{
cwd: '/repo',
timeoutMs: 5_000,
},
]);
});
it('does not query commandStatus for non-timeout OpenCode sendMessage failures', async () => {
const executor = fakeExecutor(bridgeFailure('provider_error', 'OpenCode send failed', []));
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.sendOpenCodeTeamMessage({
teamId: 'team-a',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
projectPath: '/repo',
memberName: 'bob',
text: 'hello',
messageId: 'message-1',
deliveryAttemptId: 'ledger-1:1:payload',
})
).resolves.toMatchObject({
accepted: false,
memberName: 'bob',
diagnostics: [
expect.objectContaining({
code: 'provider_error',
}),
],
});
expect(executor.execute).toHaveBeenCalledOnce();
});
it('keeps the timeout failure path when timeout commandStatus is unknown', async () => {
const executor = fakeSequenceExecutor([
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
bridgeCommandSuccess({
command: 'opencode.commandStatus',
requestId: 'status-req-1',
data: {
status: 'unknown',
safeToRetry: false,
accepted: false,
diagnostics: ['No orchestrator-side command outcome record matched the requested OpenCode command.'],
},
}),
]);
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.sendOpenCodeTeamMessage({
teamId: 'team-a',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
projectPath: '/repo',
memberName: 'bob',
text: 'hello',
messageId: 'message-1',
deliveryAttemptId: 'ledger-1:1:payload',
})
).resolves.toMatchObject({
accepted: false,
memberName: 'bob',
diagnostics: [
expect.objectContaining({
code: 'timeout',
}),
],
});
expect(executor.execute).toHaveBeenCalledTimes(2);
});
it('keeps the timeout failure path when timeout commandStatus is unavailable', async () => {
const executor = fakeSequenceExecutor([
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
bridgeFailure('timeout', 'OpenCode commandStatus timed out', []),
]);
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.sendOpenCodeTeamMessage({
teamId: 'team-a',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
projectPath: '/repo',
memberName: 'bob',
text: 'hello',
messageId: 'message-1',
deliveryAttemptId: 'ledger-1:1:payload',
})
).resolves.toMatchObject({
accepted: false,
memberName: 'bob',
diagnostics: [
expect.objectContaining({
code: 'timeout',
}),
],
});
expect(executor.execute).toHaveBeenCalledTimes(2);
});
it('keeps the timeout failure path when timeout commandStatus reports precondition mismatch', async () => {
const executor = fakeSequenceExecutor([
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
bridgeCommandSuccess({
command: 'opencode.commandStatus',
requestId: 'status-req-1',
data: {
status: 'precondition_mismatch',
safeToRetry: false,
accepted: false,
diagnostics: ['OpenCode command status payloadHash mismatch.'],
},
}),
]);
const bridge = new OpenCodeReadinessBridge(executor);
await expect(
bridge.sendOpenCodeTeamMessage({
teamId: 'team-a',
teamName: 'team-a',
laneId: 'secondary:opencode:bob',
projectPath: '/repo',
memberName: 'bob',
text: 'hello',
messageId: 'message-1',
deliveryAttemptId: 'ledger-1:1:payload',
})
).resolves.toMatchObject({
accepted: false,
memberName: 'bob',
diagnostics: [
expect.objectContaining({
code: 'timeout',
}),
],
});
expect(executor.execute).toHaveBeenCalledTimes(2);
});
it('routes state-changing launch commands through the guarded command service when configured', async () => {
const executor = fakeExecutor(
bridgeFailure('internal_error', 'direct bridge must not run', [])
);
const stateChangingExecute = vi.fn();
const stateChangingCommands = {
async execute<TBody, TData>(input: {
command: OpenCodeBridgeCommandName;
body: TBody;
}): Promise<OpenCodeBridgeResult<TData>> {
stateChangingExecute(input);
return bridgeCommandSuccess<OpenCodeLaunchTeamCommandData>({
command: input.command,
requestId: 'guarded-req-1',
data: {
runId: 'run-1',
teamLaunchState: 'ready',
members: {},
warnings: [],
diagnostics: [],
idempotencyKey: 'idem-1',
runtimeStoreManifestHighWatermark: 0,
},
}) as unknown as OpenCodeBridgeResult<TData>;
},
};
const bridge = new OpenCodeReadinessBridge(executor, { stateChangingCommands });
await expect(
bridge.launchOpenCodeTeam({
runId: 'run-1',
laneId: 'primary',
teamId: 'team-a',
teamName: 'team-a',
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
members: [],
leadPrompt: '',
expectedCapabilitySnapshotId: 'cap-1',
manifestHighWatermark: 0,
})
).resolves.toMatchObject({
runId: 'run-1',
teamLaunchState: 'ready',
idempotencyKey: 'idem-1',
});
expect(stateChangingExecute).toHaveBeenCalledWith(
expect.objectContaining({
command: 'opencode.launchTeam',
teamName: 'team-a',
laneId: 'primary',
runId: 'run-1',
capabilitySnapshotId: 'cap-1',
cwd: '/repo',
})
);
expect(executor.execute).not.toHaveBeenCalled();
});
});
function fakeExecutor(
result: OpenCodeBridgeResult<unknown>
): OpenCodeReadinessBridgeCommandExecutor {
return {
execute: vi.fn(async () => result) as OpenCodeReadinessBridgeCommandExecutor['execute'],
};
}
function fakeSequenceExecutor(
results: OpenCodeBridgeResult<unknown>[]
): OpenCodeReadinessBridgeCommandExecutor & {
execute: ReturnType<typeof vi.fn>;
} {
const execute = vi.fn(async () => {
const next = results.shift();
if (!next) {
throw new Error('No fake bridge result queued');
}
return next;
});
return {
execute: execute as unknown as OpenCodeReadinessBridgeCommandExecutor['execute'] &
ReturnType<typeof vi.fn>,
};
}
function bridgeSuccess(
data: OpenCodeTeamLaunchReadiness
): OpenCodeBridgeSuccess<OpenCodeTeamLaunchReadiness> {
return {
ok: true,
schemaVersion: 1,
requestId: 'req-1',
command: 'opencode.readiness',
completedAt: '2026-04-21T12:00:01.000Z',
durationMs: 1000,
runtime: {
providerId: 'opencode',
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'bin-1',
version: '1.14.19',
capabilitySnapshotId: 'cap-1',
},
diagnostics: [],
data,
};
}
function bridgeFailure(
kind: OpenCodeBridgeFailureKind,
message: string,
diagnostics: OpenCodeBridgeResult<unknown>['diagnostics']
): OpenCodeBridgeResult<unknown> {
return {
ok: false,
schemaVersion: 1,
requestId: 'req-1',
command: 'opencode.readiness',
completedAt: '2026-04-21T12:00:01.000Z',
durationMs: 1000,
error: {
kind,
message,
retryable: true,
},
diagnostics,
};
}
function bridgeCommandSuccess<TData>(input: {
command: OpenCodeBridgeCommandName;
requestId: string;
data: TData;
}): OpenCodeBridgeSuccess<TData> {
return {
ok: true,
schemaVersion: 1,
requestId: input.requestId,
command: input.command,
completedAt: '2026-04-21T12:00:01.000Z',
durationMs: 1000,
runtime: {
providerId: 'opencode',
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'bin-1',
version: '1.14.19',
capabilitySnapshotId: 'cap-1',
},
diagnostics: [],
data: input.data,
};
}
function readiness(
overrides: Partial<OpenCodeTeamLaunchReadiness> = {}
): 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: [...REQUIRED_AGENT_TEAMS_APP_TOOL_IDS],
runtimeStoreReadinessReason: 'runtime_store_manifest_valid',
},
...overrides,
};
}