461 lines
14 KiB
TypeScript
461 lines
14 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
assertBridgeEvidenceCanCommitToRuntimeStores,
|
|
assertBridgeResultCanMutateState,
|
|
createOpenCodeBridgeHandshakeIdentityHash,
|
|
createOpenCodeBridgeIdempotencyKey,
|
|
isOpenCodeBridgeCommandName,
|
|
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
|
|
OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION,
|
|
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
|
|
parseSingleBridgeJsonResult,
|
|
stableHash,
|
|
validateBridgeResultEnvelope,
|
|
validateOpenCodeBridgeHandshake,
|
|
type OpenCodeBridgeCommandEnvelope,
|
|
type OpenCodeBridgeHandshake,
|
|
type OpenCodeBridgePeerIdentity,
|
|
type OpenCodeBridgeRuntimeSnapshot,
|
|
type OpenCodeBridgeSuccess,
|
|
} from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandContract';
|
|
|
|
describe('OpenCodeBridgeCommandContract', () => {
|
|
it('rejects bridge stdout with logs plus json', () => {
|
|
const result = parseSingleBridgeJsonResult('debug log\n{"ok":true}\n');
|
|
|
|
expect(result).toEqual({
|
|
ok: false,
|
|
error: 'Bridge stdout must contain exactly one JSON line, got 2',
|
|
});
|
|
});
|
|
|
|
it('parses exactly one bridge result JSON line', () => {
|
|
const stdout = `${JSON.stringify(
|
|
bridgeSuccess({
|
|
data: {
|
|
runId: 'run-1',
|
|
idempotencyKey: 'key-1',
|
|
runtimeStoreManifestHighWatermark: 10,
|
|
},
|
|
})
|
|
)}\n`;
|
|
|
|
const parsed = parseSingleBridgeJsonResult(stdout);
|
|
|
|
expect(parsed).toMatchObject({
|
|
ok: true,
|
|
value: {
|
|
ok: true,
|
|
requestId: 'req-1',
|
|
command: 'opencode.launchTeam',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('accepts opencode.cleanupHosts as a bridge command', () => {
|
|
expect(isOpenCodeBridgeCommandName('opencode.cleanupHosts')).toBe(true);
|
|
});
|
|
|
|
it('accepts opencode.backfillTaskLedger as a read-only bridge command', () => {
|
|
expect(isOpenCodeBridgeCommandName('opencode.backfillTaskLedger')).toBe(true);
|
|
});
|
|
|
|
it('uses capability recovery attempt id as part of the launch idempotency key', () => {
|
|
const baseInput = {
|
|
command: 'opencode.launchTeam' as const,
|
|
teamName: 'team-a',
|
|
laneId: 'secondary:opencode:alice',
|
|
runId: 'run-1',
|
|
body: {
|
|
runId: 'run-1',
|
|
laneId: 'secondary:opencode:alice',
|
|
teamId: 'team-a',
|
|
teamName: 'team-a',
|
|
projectPath: '/repo',
|
|
selectedModel: 'openai/gpt-5.4-mini',
|
|
members: [{ name: 'alice', role: 'Developer', prompt: 'Launch' }],
|
|
leadPrompt: '',
|
|
expectedCapabilitySnapshotId: 'cap-1',
|
|
manifestHighWatermark: null,
|
|
},
|
|
};
|
|
|
|
expect(createOpenCodeBridgeIdempotencyKey(baseInput)).not.toBe(
|
|
createOpenCodeBridgeIdempotencyKey({
|
|
...baseInput,
|
|
body: {
|
|
...baseInput.body,
|
|
capabilitySnapshotRecoveryAttemptId: 'opencode-capability-recovery-test',
|
|
},
|
|
})
|
|
);
|
|
});
|
|
|
|
it('validates result request id and command against the command envelope', () => {
|
|
const envelope: OpenCodeBridgeCommandEnvelope<Record<string, never>> = {
|
|
schemaVersion: 1,
|
|
requestId: 'req-expected',
|
|
command: 'opencode.launchTeam',
|
|
cwd: '/tmp/project',
|
|
startedAt: '2026-04-21T12:00:00.000Z',
|
|
timeoutMs: 10_000,
|
|
body: {},
|
|
};
|
|
|
|
expect(validateBridgeResultEnvelope(bridgeSuccess({ requestId: 'other' }), envelope)).toEqual({
|
|
ok: false,
|
|
reason: 'OpenCode bridge requestId mismatch',
|
|
});
|
|
|
|
expect(
|
|
validateBridgeResultEnvelope(
|
|
bridgeSuccess({ requestId: 'req-expected', command: 'opencode.stopTeam' }),
|
|
envelope
|
|
)
|
|
).toEqual({
|
|
ok: false,
|
|
reason: 'OpenCode bridge command mismatch',
|
|
});
|
|
});
|
|
|
|
it('does not allow state mutation when capability snapshot mismatches', () => {
|
|
const result = bridgeSuccess({
|
|
runtime: { capabilitySnapshotId: 'old-snapshot' },
|
|
data: { runId: 'run-1' },
|
|
});
|
|
|
|
expect(() =>
|
|
assertBridgeResultCanMutateState(result, {
|
|
requestId: 'req-1',
|
|
command: 'opencode.launchTeam',
|
|
runId: 'run-1',
|
|
capabilitySnapshotId: 'new-snapshot',
|
|
})
|
|
).toThrow('OpenCode bridge capability snapshot mismatch');
|
|
});
|
|
|
|
it('allows state mutation when caller has no capability snapshot evidence to compare', () => {
|
|
const result = bridgeSuccess({
|
|
runtime: { capabilitySnapshotId: 'runtime-snapshot' },
|
|
data: { runId: 'run-1' },
|
|
});
|
|
|
|
expect(() =>
|
|
assertBridgeResultCanMutateState(result, {
|
|
requestId: 'req-1',
|
|
command: 'opencode.launchTeam',
|
|
runId: 'run-1',
|
|
capabilitySnapshotId: null,
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it('rejects state-changing bridge evidence with stale manifest or idempotency mismatch', () => {
|
|
const result = bridgeSuccess({
|
|
data: {
|
|
runId: 'run-1',
|
|
idempotencyKey: 'key-1',
|
|
runtimeStoreManifestHighWatermark: 9,
|
|
},
|
|
});
|
|
|
|
expect(() =>
|
|
assertBridgeEvidenceCanCommitToRuntimeStores({
|
|
result,
|
|
requestId: 'req-1',
|
|
command: 'opencode.launchTeam',
|
|
runId: 'run-1',
|
|
capabilitySnapshotId: 'cap-1',
|
|
manifest: { highWatermark: 10 },
|
|
idempotencyKey: 'key-1',
|
|
})
|
|
).toThrow('Bridge result manifest high watermark is stale');
|
|
|
|
expect(() =>
|
|
assertBridgeEvidenceCanCommitToRuntimeStores({
|
|
result: bridgeSuccess({
|
|
data: {
|
|
runId: 'run-1',
|
|
idempotencyKey: 'other-key',
|
|
runtimeStoreManifestHighWatermark: 10,
|
|
},
|
|
}),
|
|
requestId: 'req-1',
|
|
command: 'opencode.launchTeam',
|
|
runId: 'run-1',
|
|
capabilitySnapshotId: 'cap-1',
|
|
manifest: { highWatermark: 10 },
|
|
idempotencyKey: 'key-1',
|
|
})
|
|
).toThrow('Bridge result idempotency key mismatch');
|
|
});
|
|
|
|
it('rejects handshake when server manifest high watermark is stale', () => {
|
|
const client = peerIdentity('claude_team');
|
|
const server = peerIdentity('agent_teams_orchestrator', {
|
|
runtimeStoreManifestHighWatermark: 9,
|
|
});
|
|
const handshake = buildHandshake({ client, server });
|
|
|
|
expect(
|
|
validateOpenCodeBridgeHandshake({
|
|
handshake,
|
|
expectedClient: client,
|
|
requiredCommand: 'opencode.launchTeam',
|
|
expectedCapabilitySnapshotId: 'cap-1',
|
|
expectedManifestHighWatermark: 10,
|
|
expectedRunId: 'run-1',
|
|
})
|
|
).toEqual({
|
|
ok: false,
|
|
reason: 'Bridge server runtime manifest high watermark is stale',
|
|
});
|
|
});
|
|
|
|
it('rejects handshake when identity hash does not match peer evidence', () => {
|
|
const client = peerIdentity('claude_team');
|
|
const server = peerIdentity('agent_teams_orchestrator');
|
|
const handshake: OpenCodeBridgeHandshake = {
|
|
...buildHandshake({ client, server }),
|
|
identityHash: 'tampered',
|
|
};
|
|
|
|
expect(
|
|
validateOpenCodeBridgeHandshake({
|
|
handshake,
|
|
expectedClient: client,
|
|
requiredCommand: 'opencode.launchTeam',
|
|
expectedCapabilitySnapshotId: 'cap-1',
|
|
expectedManifestHighWatermark: 10,
|
|
expectedRunId: 'run-1',
|
|
})
|
|
).toEqual({
|
|
ok: false,
|
|
reason: 'Bridge handshake identity hash mismatch',
|
|
});
|
|
});
|
|
|
|
it('accepts handshake evidence contract version and rejects invalid values', () => {
|
|
const client = peerIdentity('claude_team');
|
|
const server = peerIdentity('agent_teams_orchestrator');
|
|
server.bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion =
|
|
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION;
|
|
const validHandshake = buildHandshake({ client, server });
|
|
|
|
expect(
|
|
validateOpenCodeBridgeHandshake({
|
|
handshake: validHandshake,
|
|
expectedClient: client,
|
|
requiredCommand: 'opencode.launchTeam',
|
|
expectedCapabilitySnapshotId: 'cap-1',
|
|
expectedManifestHighWatermark: 10,
|
|
expectedRunId: 'run-1',
|
|
})
|
|
).toEqual({ ok: true });
|
|
|
|
server.bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion = 0;
|
|
const invalidHandshake = buildHandshake({ client, server });
|
|
|
|
expect(
|
|
validateOpenCodeBridgeHandshake({
|
|
handshake: invalidHandshake,
|
|
expectedClient: client,
|
|
requiredCommand: 'opencode.launchTeam',
|
|
expectedCapabilitySnapshotId: 'cap-1',
|
|
expectedManifestHighWatermark: 10,
|
|
expectedRunId: 'run-1',
|
|
})
|
|
).toEqual({
|
|
ok: false,
|
|
reason: 'Bridge handshake peer identity is invalid',
|
|
});
|
|
});
|
|
|
|
it('requires the delivery acceptance contract only for acceptance-mode sendMessage', () => {
|
|
const client = peerIdentity('claude_team');
|
|
const server = peerIdentity('agent_teams_orchestrator');
|
|
client.bridgeProtocol.supportedCommands.push('opencode.sendMessage');
|
|
server.bridgeProtocol.supportedCommands.push('opencode.sendMessage');
|
|
let handshake = withAcceptedCommands(buildHandshake({ client, server }), [
|
|
'opencode.launchTeam',
|
|
'opencode.stopTeam',
|
|
'opencode.sendMessage',
|
|
]);
|
|
|
|
expect(
|
|
validateOpenCodeBridgeHandshake({
|
|
handshake,
|
|
expectedClient: client,
|
|
requiredCommand: 'opencode.sendMessage',
|
|
expectedCapabilitySnapshotId: null,
|
|
expectedManifestHighWatermark: 10,
|
|
expectedRunId: 'run-1',
|
|
requiresDeliveryAcceptanceContract: false,
|
|
})
|
|
).toEqual({ ok: true });
|
|
|
|
expect(
|
|
validateOpenCodeBridgeHandshake({
|
|
handshake,
|
|
expectedClient: client,
|
|
requiredCommand: 'opencode.sendMessage',
|
|
expectedCapabilitySnapshotId: null,
|
|
expectedManifestHighWatermark: 10,
|
|
expectedRunId: 'run-1',
|
|
requiresDeliveryAcceptanceContract: true,
|
|
})
|
|
).toEqual({
|
|
ok: false,
|
|
reason:
|
|
`OpenCode delivery acceptance mode is required, but the orchestrator does not advertise contract version ${OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION}. Falling back to observed delivery mode is required.`,
|
|
});
|
|
|
|
server.bridgeProtocol.opencodeDeliveryAcceptanceContractVersion =
|
|
OPEN_CODE_DELIVERY_ACCEPTANCE_CONTRACT_VERSION;
|
|
handshake = withAcceptedCommands(buildHandshake({ client, server }), [
|
|
'opencode.launchTeam',
|
|
'opencode.stopTeam',
|
|
'opencode.sendMessage',
|
|
]);
|
|
|
|
expect(
|
|
validateOpenCodeBridgeHandshake({
|
|
handshake,
|
|
expectedClient: client,
|
|
requiredCommand: 'opencode.sendMessage',
|
|
expectedCapabilitySnapshotId: null,
|
|
expectedManifestHighWatermark: 10,
|
|
expectedRunId: 'run-1',
|
|
requiresDeliveryAcceptanceContract: true,
|
|
})
|
|
).toEqual({ ok: true });
|
|
});
|
|
|
|
it('creates deterministic idempotency keys for equivalent JSON bodies', () => {
|
|
const first = createOpenCodeBridgeIdempotencyKey({
|
|
command: 'opencode.launchTeam',
|
|
teamName: 'Team A',
|
|
runId: 'run-1',
|
|
body: { a: 1, b: { c: true, d: ['x'] } },
|
|
});
|
|
const second = createOpenCodeBridgeIdempotencyKey({
|
|
command: 'opencode.launchTeam',
|
|
teamName: 'Team A',
|
|
runId: 'run-1',
|
|
body: { b: { d: ['x'], c: true }, a: 1 },
|
|
});
|
|
|
|
expect(first).toBe(second);
|
|
expect(first).toMatch(
|
|
/^opencode:opencode\.launchTeam:Team_A:no-lane:run-1:[a-f0-9]{32}$/
|
|
);
|
|
expect(stableHash({ b: 2, a: 1 })).toBe(stableHash({ a: 1, b: 2 }));
|
|
});
|
|
});
|
|
|
|
type BridgeSuccessOverrides = Omit<Partial<OpenCodeBridgeSuccess<unknown>>, 'runtime'> & {
|
|
runtime?: Partial<OpenCodeBridgeRuntimeSnapshot>;
|
|
data?: unknown;
|
|
};
|
|
|
|
function bridgeSuccess(overrides: BridgeSuccessOverrides = {}): OpenCodeBridgeSuccess<unknown> {
|
|
const { runtime: runtimeOverrides, ...rest } = overrides;
|
|
return {
|
|
ok: true,
|
|
schemaVersion: 1,
|
|
requestId: 'req-1',
|
|
command: 'opencode.launchTeam',
|
|
completedAt: '2026-04-21T12:00:01.000Z',
|
|
durationMs: 1000,
|
|
runtime: {
|
|
providerId: 'opencode',
|
|
binaryPath: '/usr/local/bin/opencode',
|
|
binaryFingerprint: 'bin-1',
|
|
version: '1.0.0',
|
|
capabilitySnapshotId: 'cap-1',
|
|
...runtimeOverrides,
|
|
},
|
|
diagnostics: [],
|
|
data: {
|
|
runId: 'run-1',
|
|
idempotencyKey: 'key-1',
|
|
runtimeStoreManifestHighWatermark: 10,
|
|
},
|
|
...rest,
|
|
};
|
|
}
|
|
|
|
function peerIdentity(
|
|
peer: OpenCodeBridgePeerIdentity['peer'],
|
|
runtimeOverrides: Partial<OpenCodeBridgePeerIdentity['runtime']> = {}
|
|
): OpenCodeBridgePeerIdentity {
|
|
return {
|
|
schemaVersion: 1,
|
|
peer,
|
|
appVersion: '1.0.0',
|
|
gitSha: 'git-1',
|
|
buildId: 'build-1',
|
|
bridgeProtocol: {
|
|
minVersion: 1,
|
|
currentVersion: 1,
|
|
supportedCommands: [
|
|
'opencode.handshake',
|
|
'opencode.commandStatus',
|
|
'opencode.launchTeam',
|
|
'opencode.stopTeam',
|
|
],
|
|
opencodeAppManagedBootstrapContractVersion:
|
|
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
|
|
},
|
|
runtime: {
|
|
providerId: 'opencode',
|
|
binaryPath: '/usr/local/bin/opencode',
|
|
binaryFingerprint: 'bin-1',
|
|
version: '1.0.0',
|
|
capabilitySnapshotId: 'cap-1',
|
|
runtimeStoreManifestHighWatermark: 10,
|
|
activeRunId: 'run-1',
|
|
...runtimeOverrides,
|
|
},
|
|
featureFlags: {
|
|
opencodeTeamLaunch: true,
|
|
opencodeStateChangingCommands: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildHandshake(input: {
|
|
client: OpenCodeBridgePeerIdentity;
|
|
server: OpenCodeBridgePeerIdentity;
|
|
}): OpenCodeBridgeHandshake {
|
|
const withoutHash: Omit<OpenCodeBridgeHandshake, 'identityHash'> = {
|
|
schemaVersion: 1,
|
|
requestId: 'handshake-1',
|
|
client: input.client,
|
|
server: input.server,
|
|
agreedProtocolVersion: 1,
|
|
acceptedCommands: ['opencode.launchTeam', 'opencode.stopTeam'],
|
|
serverTime: '2026-04-21T12:00:00.000Z',
|
|
};
|
|
|
|
return {
|
|
...withoutHash,
|
|
identityHash: createOpenCodeBridgeHandshakeIdentityHash(withoutHash),
|
|
};
|
|
}
|
|
|
|
function withAcceptedCommands(
|
|
handshake: OpenCodeBridgeHandshake,
|
|
acceptedCommands: OpenCodeBridgeHandshake['acceptedCommands']
|
|
): OpenCodeBridgeHandshake {
|
|
const { identityHash: _identityHash, ...rest } = handshake;
|
|
const withoutHash: Omit<OpenCodeBridgeHandshake, 'identityHash'> = {
|
|
...rest,
|
|
acceptedCommands,
|
|
};
|
|
return {
|
|
...withoutHash,
|
|
identityHash: createOpenCodeBridgeHandshakeIdentityHash(withoutHash),
|
|
};
|
|
}
|