375 lines
11 KiB
TypeScript
375 lines
11 KiB
TypeScript
import { promises as fs } from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
assertOpenCodeProductionE2EArtifactGate,
|
|
buildOpenCodeProjectPathFingerprint,
|
|
OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS,
|
|
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS,
|
|
validateOpenCodeProductionE2EEvidence,
|
|
type OpenCodeProductionE2EEvidence,
|
|
} from '../../../../src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidence';
|
|
import { OpenCodeProductionE2EEvidenceStore } from '../../../../src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidenceStore';
|
|
import {
|
|
buildOpenCodeCanonicalMcpToolId,
|
|
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS,
|
|
} from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
|
|
|
|
describe('OpenCodeProductionE2EEvidence', () => {
|
|
let tempDir: string;
|
|
const now = new Date('2026-04-21T12:00:00.000Z');
|
|
|
|
beforeEach(async () => {
|
|
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-production-e2e-'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('accepts production evidence when runtime identity, project context and required MCP tools match', () => {
|
|
const evidence = passingEvidence();
|
|
|
|
expect(validateOpenCodeProductionE2EEvidence(evidence)).toEqual(evidence);
|
|
expect(
|
|
assertOpenCodeProductionE2EArtifactGate({
|
|
evidence,
|
|
artifactPath: '/tmp/opencode-e2e',
|
|
now,
|
|
expected: {
|
|
opencodeVersion: '1.14.19',
|
|
binaryFingerprint: 'version:1.14.19',
|
|
capabilitySnapshotId: 'cap-1',
|
|
selectedModel: 'openai/gpt-5.4-mini',
|
|
projectPathFingerprint: 'project-a',
|
|
requiredMcpTools: ['agent-teams_runtime_deliver_message'],
|
|
},
|
|
})
|
|
).toEqual({
|
|
ok: true,
|
|
diagnostics: [],
|
|
});
|
|
});
|
|
|
|
it('fails closed for stale, mismatched or incomplete evidence', () => {
|
|
const expired = passingEvidence({
|
|
expiresAt: '2026-04-21T11:59:59.000Z',
|
|
selectedModel: 'openrouter/anthropic/claude-sonnet-4.5',
|
|
requiredSignals: requiredSignals({ stale_run_rejected: false }),
|
|
mcpTools: {
|
|
requiredTools: ['agent-teams_runtime_deliver_message'],
|
|
observedTools: [],
|
|
},
|
|
});
|
|
|
|
expect(
|
|
assertOpenCodeProductionE2EArtifactGate({
|
|
evidence: expired,
|
|
artifactPath: '/tmp/opencode-e2e',
|
|
now,
|
|
expected: {
|
|
opencodeVersion: '1.14.19',
|
|
binaryFingerprint: 'version:1.14.19',
|
|
capabilitySnapshotId: 'cap-1',
|
|
selectedModel: 'openai/gpt-5.4-mini',
|
|
projectPathFingerprint: 'project-a',
|
|
requiredMcpTools: ['agent-teams_runtime_deliver_message'],
|
|
},
|
|
})
|
|
).toMatchObject({
|
|
ok: false,
|
|
diagnostics: expect.arrayContaining([
|
|
'OpenCode production E2E evidence is expired',
|
|
'OpenCode production E2E evidence is missing signals: stale_run_rejected',
|
|
'OpenCode production E2E evidence is missing observed MCP tools: agent-teams_runtime_deliver_message',
|
|
]),
|
|
});
|
|
});
|
|
|
|
it('reads missing evidence as a production-blocking diagnostic and quarantines corrupt artifacts', async () => {
|
|
const filePath = path.join(tempDir, 'production-e2e-evidence.json');
|
|
const store = new OpenCodeProductionE2EEvidenceStore({
|
|
filePath,
|
|
clock: () => now,
|
|
});
|
|
|
|
await expect(store.read()).resolves.toMatchObject({
|
|
ok: true,
|
|
evidence: null,
|
|
artifactPath: filePath,
|
|
diagnostics: ['OpenCode production E2E evidence artifact has not been written yet'],
|
|
});
|
|
|
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
await fs.writeFile(filePath, '{broken', 'utf8');
|
|
const corrupt = await store.read();
|
|
expect(corrupt).toMatchObject({
|
|
ok: false,
|
|
evidence: null,
|
|
artifactPath: filePath,
|
|
});
|
|
expect(corrupt.diagnostics[0]).toContain(
|
|
'OpenCode production E2E evidence store is unreadable'
|
|
);
|
|
});
|
|
|
|
it('writes evidence with the store path as artifactPath when the input omits it', async () => {
|
|
const filePath = path.join(tempDir, 'production-e2e-evidence.json');
|
|
const store = new OpenCodeProductionE2EEvidenceStore({
|
|
filePath,
|
|
clock: () => now,
|
|
});
|
|
|
|
await store.write({
|
|
...passingEvidence(),
|
|
artifactPath: null,
|
|
});
|
|
|
|
await expect(store.read()).resolves.toMatchObject({
|
|
ok: true,
|
|
evidence: {
|
|
artifactPath: filePath,
|
|
evidenceId: 'e2e-1',
|
|
},
|
|
diagnostics: [],
|
|
});
|
|
});
|
|
|
|
it('stores production evidence for multiple raw model ids and reads exact model matches when no project context is provided', async () => {
|
|
const filePath = path.join(tempDir, 'production-e2e-evidence.json');
|
|
const store = new OpenCodeProductionE2EEvidenceStore({
|
|
filePath,
|
|
clock: () => now,
|
|
});
|
|
|
|
await store.write(passingEvidence({ selectedModel: 'opencode/big-pickle' }));
|
|
await store.write(
|
|
passingEvidence({
|
|
evidenceId: 'e2e-2',
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
store.read({ selectedModel: 'opencode/minimax-m2.5-free' })
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
evidence: {
|
|
evidenceId: 'e2e-2',
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
},
|
|
diagnostics: [],
|
|
});
|
|
|
|
await expect(store.read({ selectedModel: 'openai/gpt-5.4-mini' })).resolves.toMatchObject({
|
|
ok: true,
|
|
evidence: null,
|
|
diagnostics: [
|
|
'OpenCode production E2E evidence artifact has no entry for selected model openai/gpt-5.4-mini',
|
|
],
|
|
});
|
|
});
|
|
|
|
it('reuses the current project production proof even when the requested OpenCode model differs', async () => {
|
|
const filePath = path.join(tempDir, 'production-e2e-evidence.json');
|
|
const store = new OpenCodeProductionE2EEvidenceStore({
|
|
filePath,
|
|
clock: () => now,
|
|
});
|
|
|
|
await store.write(
|
|
passingEvidence({
|
|
evidenceId: 'e2e-project-a',
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
projectPathFingerprint: buildOpenCodeProjectPathFingerprint('/repo-a'),
|
|
})
|
|
);
|
|
await store.write(
|
|
passingEvidence({
|
|
evidenceId: 'e2e-project-b',
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
projectPathFingerprint: buildOpenCodeProjectPathFingerprint('/repo-b'),
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
store.read({
|
|
selectedModel: 'opencode/nemotron-3-super-free',
|
|
projectPathFingerprint: buildOpenCodeProjectPathFingerprint('/repo-b'),
|
|
})
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
evidence: {
|
|
evidenceId: 'e2e-project-b',
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
},
|
|
diagnostics: [],
|
|
});
|
|
});
|
|
|
|
it('prefers a runtime-compatible project proof over a newer stale one from the same cwd', async () => {
|
|
const filePath = path.join(tempDir, 'production-e2e-evidence.json');
|
|
const store = new OpenCodeProductionE2EEvidenceStore({
|
|
filePath,
|
|
clock: () => now,
|
|
});
|
|
|
|
await store.write(
|
|
passingEvidence({
|
|
evidenceId: 'stale-newer',
|
|
createdAt: '2026-04-21T12:05:00.000Z',
|
|
selectedModel: 'opencode/big-pickle',
|
|
projectPathFingerprint: buildOpenCodeProjectPathFingerprint('/repo-a'),
|
|
capabilitySnapshotId: 'cap-stale',
|
|
})
|
|
);
|
|
await store.write(
|
|
passingEvidence({
|
|
evidenceId: 'matching-older',
|
|
createdAt: '2026-04-21T12:00:00.000Z',
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
projectPathFingerprint: buildOpenCodeProjectPathFingerprint('/repo-a'),
|
|
capabilitySnapshotId: 'cap-current',
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
store.read({
|
|
selectedModel: 'opencode/nemotron-3-super-free',
|
|
projectPathFingerprint: buildOpenCodeProjectPathFingerprint('/repo-a'),
|
|
opencodeVersion: '1.14.19',
|
|
binaryFingerprint: 'version:1.14.19',
|
|
capabilitySnapshotId: 'cap-current',
|
|
})
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
evidence: {
|
|
evidenceId: 'matching-older',
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
},
|
|
diagnostics: [],
|
|
});
|
|
});
|
|
|
|
it('stores production evidence for the same raw model across multiple project contexts', async () => {
|
|
const filePath = path.join(tempDir, 'production-e2e-evidence.json');
|
|
const store = new OpenCodeProductionE2EEvidenceStore({
|
|
filePath,
|
|
clock: () => now,
|
|
});
|
|
|
|
await store.write(
|
|
passingEvidence({
|
|
evidenceId: 'e2e-project-a',
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
projectPathFingerprint: buildOpenCodeProjectPathFingerprint('/repo-a'),
|
|
})
|
|
);
|
|
await store.write(
|
|
passingEvidence({
|
|
evidenceId: 'e2e-project-b',
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
projectPathFingerprint: buildOpenCodeProjectPathFingerprint('/repo-b'),
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
store.read({
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
projectPathFingerprint: buildOpenCodeProjectPathFingerprint('/repo-b'),
|
|
})
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
evidence: {
|
|
evidenceId: 'e2e-project-b',
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
},
|
|
diagnostics: [],
|
|
});
|
|
|
|
await expect(
|
|
store.read({
|
|
selectedModel: 'opencode/minimax-m2.5-free',
|
|
projectPathFingerprint: buildOpenCodeProjectPathFingerprint('/repo-c'),
|
|
})
|
|
).resolves.toMatchObject({
|
|
ok: true,
|
|
evidence: null,
|
|
diagnostics: ['OpenCode production E2E evidence artifact has no entry for the current working directory'],
|
|
});
|
|
});
|
|
});
|
|
|
|
function passingEvidence(
|
|
overrides: Partial<OpenCodeProductionE2EEvidence> = {}
|
|
): OpenCodeProductionE2EEvidence {
|
|
const createdAt = '2026-04-21T12:00:00.000Z';
|
|
const sessionId = 'session-1';
|
|
const requiredToolIds = REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) =>
|
|
buildOpenCodeCanonicalMcpToolId('agent-teams', tool)
|
|
);
|
|
|
|
return {
|
|
schemaVersion: 1,
|
|
evidenceId: 'e2e-1',
|
|
createdAt,
|
|
expiresAt: '2026-04-21T12:10:00.000Z',
|
|
version: '1.14.19',
|
|
passed: true,
|
|
artifactPath: '/tmp/opencode-e2e',
|
|
binaryFingerprint: 'version:1.14.19',
|
|
capabilitySnapshotId: 'cap-1',
|
|
selectedModel: 'openai/gpt-5.4-mini',
|
|
projectPathFingerprint: 'project-a',
|
|
requiredSignals: requiredSignals(),
|
|
mcpTools: {
|
|
requiredTools: requiredToolIds,
|
|
observedTools: requiredToolIds,
|
|
},
|
|
launch: {
|
|
runId: 'run-1',
|
|
teamId: 'team-a',
|
|
teamLaunchState: 'ready',
|
|
memberCount: 1,
|
|
sessions: [
|
|
{
|
|
memberName: 'Dev',
|
|
sessionId,
|
|
launchState: 'confirmed_alive',
|
|
},
|
|
],
|
|
durableCheckpoints: OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS.map((name) => ({
|
|
name,
|
|
observedAt: createdAt,
|
|
})),
|
|
},
|
|
reconcile: {
|
|
runId: 'run-1',
|
|
teamLaunchState: 'ready',
|
|
memberCount: 1,
|
|
},
|
|
stop: {
|
|
runId: 'run-1',
|
|
stopped: true,
|
|
stoppedSessionIds: [sessionId],
|
|
},
|
|
logProjection: {
|
|
observed: true,
|
|
projectedMessageCount: 1,
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function requiredSignals(
|
|
overrides: Partial<
|
|
Record<(typeof OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS)[number], boolean>
|
|
> = {}
|
|
) {
|
|
return Object.fromEntries(
|
|
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS.map((signal) => [signal, overrides[signal] ?? true])
|
|
) as OpenCodeProductionE2EEvidence['requiredSignals'];
|
|
}
|