agent-ecosystem/test/main/services/team/OpenCodeVersionPolicy.test.ts
2026-04-21 20:28:22 +03:00

375 lines
10 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 {
createEmptyEndpointMap,
type OpenCodeApiCapabilities,
type OpenCodeApiEndpointKey,
} from '../../../../src/main/services/team/opencode/capabilities/OpenCodeApiCapabilities';
import {
assertOpenCodeProductionE2EGate,
buildOpenCodeBinaryFingerprint,
evaluateOpenCodeSupport,
parseOpenCodeSemver,
selectPermissionReplyRouteFromCache,
shouldReuseCompatibilitySnapshot,
type OpenCodeCompatibilitySnapshot,
type OpenCodeRouteCompatibilityCache,
} from '../../../../src/main/services/team/opencode/version/OpenCodeVersionPolicy';
import {
OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS,
OPENCODE_PRODUCTION_E2E_REQUIRED_SIGNALS,
type OpenCodeProductionE2EEvidence,
} from '../../../../src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidence';
import { REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS } from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
describe('OpenCodeVersionPolicy', () => {
let tempDir: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-version-policy-'));
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('parses stable, v-prefixed and prerelease semver strings', () => {
expect(parseOpenCodeSemver('1.14.19')).toEqual({
major: 1,
minor: 14,
patch: 19,
prerelease: [],
});
expect(parseOpenCodeSemver('v1.14.19-beta.1')).toEqual({
major: 1,
minor: 14,
patch: 19,
prerelease: ['beta', '1'],
});
expect(parseOpenCodeSemver('not-a-version')).toBeNull();
});
it('rejects versions below minimum and prereleases by default', () => {
expect(
evaluateOpenCodeSupport({
version: '1.4.0',
capabilities: readyCapabilities(),
evidence: passingEvidence(),
})
).toMatchObject({
supported: false,
supportLevel: 'unsupported_too_old',
diagnostics: ['OpenCode 1.4.0 is below supported minimum 1.14.19'],
});
expect(
evaluateOpenCodeSupport({
version: '1.14.19-beta.1',
capabilities: readyCapabilities(),
evidence: passingEvidence(),
})
).toMatchObject({
supported: false,
supportLevel: 'unsupported_prerelease',
diagnostics: ['OpenCode prerelease 1.14.19-beta.1 is not enabled for production team launch'],
});
});
it('requires capabilities and production E2E evidence before production support', () => {
expect(
evaluateOpenCodeSupport({
version: '1.14.19',
capabilities: missingCapabilities(['POST permission reply route']),
evidence: passingEvidence(),
})
).toMatchObject({
supported: false,
supportLevel: 'supported_capabilities_pending',
diagnostics: ['POST permission reply route'],
});
expect(
evaluateOpenCodeSupport({
version: '1.14.19',
capabilities: readyCapabilities(),
evidence: null,
})
).toMatchObject({
supported: false,
supportLevel: 'supported_e2e_pending',
diagnostics: [
'OpenCode version is capability-compatible but production E2E evidence is missing',
],
});
});
it('accepts supported version only when capabilities and E2E evidence pass', () => {
expect(
evaluateOpenCodeSupport({
version: '1.14.19',
capabilities: readyCapabilities(),
evidence: passingEvidence(),
})
).toMatchObject({
supported: true,
supportLevel: 'production_supported',
diagnostics: [],
});
});
it('rejects stale or incomplete production E2E evidence', () => {
expect(
assertOpenCodeProductionE2EGate({
evidence: passingEvidence({ version: '1.14.18' }),
testedVersion: '1.14.19',
})
).toMatchObject({
ok: false,
diagnostics: expect.arrayContaining([
'OpenCode production E2E evidence version 1.14.18 does not match tested version 1.14.19',
]),
});
expect(
assertOpenCodeProductionE2EGate({
evidence: passingEvidence({
requiredSignals: requiredSignals({ canonical_log_projection_observed: false }),
}),
testedVersion: '1.14.19',
})
).toMatchObject({
ok: false,
diagnostics: expect.arrayContaining([
'OpenCode production E2E evidence is missing signals: canonical_log_projection_observed',
]),
});
});
it('invalidates compatibility snapshot when binary identity or version changes', () => {
const cached = compatibilitySnapshot({
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'fingerprint-a',
version: '1.14.19',
});
expect(
shouldReuseCompatibilitySnapshot({
cached,
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'fingerprint-a',
version: '1.14.19',
})
).toBe(true);
expect(
shouldReuseCompatibilitySnapshot({
cached,
binaryPath: '/usr/local/bin/opencode',
binaryFingerprint: 'fingerprint-a',
version: '1.14.19',
})
).toBe(false);
expect(
shouldReuseCompatibilitySnapshot({
cached,
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'fingerprint-b',
version: '1.14.19',
})
).toBe(false);
expect(
shouldReuseCompatibilitySnapshot({
cached,
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'fingerprint-a',
version: '1.15.0',
})
).toBe(false);
});
it('builds binary fingerprints from path, realpath, size and mtime', async () => {
const binaryPath = path.join(tempDir, 'opencode');
await fs.writeFile(binaryPath, 'version-a', 'utf8');
const first = await buildOpenCodeBinaryFingerprint(binaryPath);
await fs.writeFile(binaryPath, 'version-b-longer', 'utf8');
const second = await buildOpenCodeBinaryFingerprint(binaryPath);
expect(first).toMatch(/^[a-f0-9]{64}$/);
expect(second).toMatch(/^[a-f0-9]{64}$/);
expect(second).not.toBe(first);
});
it('selects permission reply route from current capability cache', () => {
expect(selectPermissionReplyRouteFromCache(routeCache({ permissionReply: true }))).toEqual({
kind: 'primary_permission_reply',
method: 'POST',
pathTemplate: '/permission/:requestID/reply',
bodyShape: { reply: 'once' },
});
expect(
selectPermissionReplyRouteFromCache(
routeCache({
permissionReply: false,
permissionLegacySessionRespond: true,
})
)
).toEqual({
kind: 'deprecated_session_permission',
method: 'POST',
pathTemplate: '/session/:sessionID/permissions/:permissionID',
bodyShape: { response: 'once' },
});
expect(selectPermissionReplyRouteFromCache(routeCache({ permissionReply: false }))).toBeNull();
});
});
function readyCapabilities(): OpenCodeApiCapabilities {
const endpoints = createEmptyEndpointMap();
const evidence = {} as OpenCodeApiCapabilities['evidence'];
for (const key of Object.keys(endpoints) as OpenCodeApiEndpointKey[]) {
endpoints[key] = true;
evidence[key] = 'openapi';
}
return {
version: '1.14.19',
source: 'openapi_doc' as const,
endpoints,
requiredForTeamLaunch: {
ready: true,
missing: [],
},
evidence,
diagnostics: [],
};
}
function missingCapabilities(missing: string[]) {
return {
...readyCapabilities(),
requiredForTeamLaunch: {
ready: false,
missing,
},
};
}
function passingEvidence(
overrides: Partial<OpenCodeProductionE2EEvidence> = {}
): OpenCodeProductionE2EEvidence {
const createdAt = new Date().toISOString();
const expiresAt = new Date(Date.now() + 60_000).toISOString();
const requiredToolIds = REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => `agent_teams_${tool}`);
const durableCheckpoints = OPENCODE_PRODUCTION_E2E_READY_CHECKPOINTS.map((name) => ({
name,
observedAt: createdAt,
}));
return {
schemaVersion: 1,
evidenceId: 'e2e-1',
createdAt,
expiresAt,
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: 'ses-1',
launchState: 'confirmed_alive',
},
],
durableCheckpoints,
},
reconcile: {
runId: 'run-1',
teamLaunchState: 'ready',
memberCount: 1,
},
stop: {
runId: 'run-1',
stopped: true,
stoppedSessionIds: ['ses-1'],
},
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'];
}
function compatibilitySnapshot(
overrides: Partial<OpenCodeCompatibilitySnapshot>
): OpenCodeCompatibilitySnapshot {
return {
schemaVersion: 1,
createdAt: '2026-04-21T12:00:00.000Z',
binaryPath: '/opt/homebrew/bin/opencode',
binaryFingerprint: 'fingerprint-a',
installMethod: 'brew',
version: '1.14.19',
semver: {
major: 1,
minor: 14,
patch: 19,
prerelease: [],
},
supported: true,
supportLevel: 'production_supported',
apiCapabilities: readyCapabilities(),
testedEvidencePath: '/tmp/opencode-e2e',
diagnostics: [],
...overrides,
};
}
function routeCache(
overrides: Partial<Record<keyof ReturnType<typeof createEmptyEndpointMap>, boolean>>
) {
return {
binaryFingerprint: 'fingerprint-a',
version: '1.14.19',
routes: Object.fromEntries(
Object.keys(createEmptyEndpointMap()).map((key) => [
key,
{
available: overrides[key as keyof typeof overrides] ?? false,
evidence: 'openapi',
lastVerifiedAt: '2026-04-21T12:00:00.000Z',
},
])
),
} as OpenCodeRouteCompatibilityCache;
}