agent-ecosystem/test/main/services/team/OpenCodeTeamLaunchReadiness.test.ts
2026-05-20 13:03:24 +03:00

469 lines
15 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { createEmptyEndpointMap } from '../../../../src/main/services/team/opencode/capabilities/OpenCodeApiCapabilities';
import { REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS } from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
import {
type OpenCodeApiCapabilityPort,
type OpenCodeMcpToolProofPort,
type OpenCodeModelExecutionProbePort,
type OpenCodeRuntimeInventory,
type OpenCodeRuntimeInventoryPort,
type OpenCodeRuntimeStoreReadinessPort,
OpenCodeTeamLaunchReadinessService,
} from '../../../../src/main/services/team/opencode/readiness/OpenCodeTeamLaunchReadiness';
import type {
OpenCodeApiCapabilities,
OpenCodeApiEndpointKey,
} from '../../../../src/main/services/team/opencode/capabilities/OpenCodeApiCapabilities';
import type { OpenCodeMcpToolProof } from '../../../../src/main/services/team/opencode/mcp/OpenCodeMcpToolAvailability';
import type { RuntimeStoreReadinessCheck } from '../../../../src/main/services/team/opencode/store/RuntimeStoreManifest';
describe('OpenCodeTeamLaunchReadinessService', () => {
it('returns not_installed before probing deeper runtime dependencies', async () => {
const ports = createPorts({
inventory: { detected: false, diagnostics: ['PATH checked'] },
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'not_installed',
launchAllowed: false,
hostHealthy: false,
diagnostics: [
'PATH checked',
'OpenCode runtime binary is not installed or not reachable by launch preflight.',
],
});
expect(ports.capabilities.detect).not.toHaveBeenCalled();
expect(ports.mcpTools.prove).not.toHaveBeenCalled();
});
it('allows unauthenticated OpenCode when the selected model is a free route', async () => {
const ports = createPorts({
inventory: {
authenticated: false,
connectedProviders: [],
models: ['opencode/big-pickle'],
},
});
await expect(
service(ports).check(readinessInput({ selectedModel: 'opencode/big-pickle' }))
).resolves.toMatchObject({
state: 'ready',
launchAllowed: true,
modelId: 'opencode/big-pickle',
diagnostics: [
'No connected OpenCode provider found. Proceeding with a free OpenCode model route that does not require provider authentication.',
],
});
expect(ports.capabilities.detect).toHaveBeenCalled();
expect(ports.mcpTools.prove).toHaveBeenCalled();
});
it('uses the first free OpenCode model for unauthenticated default selection', async () => {
const ports = createPorts({
inventory: {
authenticated: false,
connectedProviders: [],
models: ['openai/gpt-5.4-mini', 'opencode/big-pickle'],
},
});
await expect(service(ports).check(readinessInput({ selectedModel: null }))).resolves.toMatchObject({
state: 'ready',
launchAllowed: true,
modelId: 'opencode/big-pickle',
});
expect(ports.capabilities.detect).toHaveBeenCalled();
expect(ports.mcpTools.prove).toHaveBeenCalled();
});
it('does not replace an explicit unauthenticated provider-backed model with a free route', async () => {
const ports = createPorts({
inventory: {
authenticated: false,
connectedProviders: [],
models: ['opencode/big-pickle', 'openai/gpt-5.4-mini'],
},
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'not_authenticated',
launchAllowed: false,
modelId: 'openai/gpt-5.4-mini',
});
expect(ports.capabilities.detect).not.toHaveBeenCalled();
expect(ports.mcpTools.prove).not.toHaveBeenCalled();
});
it('blocks unauthenticated OpenCode when the selected model needs a provider', async () => {
const ports = createPorts({
inventory: { authenticated: false, connectedProviders: [] },
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'not_authenticated',
launchAllowed: false,
opencodeVersion: '1.14.19',
diagnostics: [
'No connected OpenCode provider found. Choose a free OpenCode model such as Big Pickle, or connect a provider in OpenCode for provider-backed models.',
],
});
expect(ports.capabilities.detect).not.toHaveBeenCalled();
expect(ports.mcpTools.prove).not.toHaveBeenCalled();
});
it('requires execution probe before allowing unauthenticated configured local models', async () => {
const ports = createPorts({
inventory: {
authenticated: false,
connectedProviders: [],
models: ['llama.cpp/qwen-test:0.5b'],
},
});
await expect(
service(ports).check(
readinessInput({
selectedModel: 'llama.cpp/qwen-test:0.5b',
requireExecutionProbe: false,
})
)
).resolves.toMatchObject({
state: 'not_authenticated',
launchAllowed: false,
modelId: 'llama.cpp/qwen-test:0.5b',
});
expect(ports.modelExecution.verify).not.toHaveBeenCalled();
});
it('allows unauthenticated configured local models after execution proof', async () => {
const ports = createPorts({
inventory: {
authenticated: false,
connectedProviders: [],
models: ['llama.cpp/qwen-test:0.5b'],
},
});
await expect(
service(ports).check(
readinessInput({
selectedModel: 'llama.cpp/qwen-test:0.5b',
requireExecutionProbe: true,
})
)
).resolves.toMatchObject({
state: 'ready',
launchAllowed: true,
modelId: 'llama.cpp/qwen-test:0.5b',
diagnostics: [
'No connected OpenCode provider found. Proceeding with a configured local OpenCode model route after execution proof.',
],
});
expect(ports.modelExecution.verify).toHaveBeenCalledWith({
projectPath: '/repo',
modelId: 'llama.cpp/qwen-test:0.5b',
inventory: expect.objectContaining({
connectedProviders: [],
}),
});
});
it('maps execution probe authentication failures to not_authenticated', async () => {
const ports = createPorts({
inventory: {
authenticated: false,
connectedProviders: [],
models: ['llama.cpp/qwen-test:0.5b'],
},
modelProbe: {
outcome: 'not_authenticated',
reason: 'local server rejected request',
diagnostics: ['local server rejected request'],
},
});
await expect(
service(ports).check(
readinessInput({
selectedModel: 'llama.cpp/qwen-test:0.5b',
requireExecutionProbe: true,
})
)
).resolves.toMatchObject({
state: 'not_authenticated',
launchAllowed: false,
missing: ['local server rejected request'],
});
});
it('blocks unsupported versions before MCP and model probes', async () => {
const ports = createPorts({
inventory: { version: '1.4.0' },
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'unsupported_version',
launchAllowed: false,
supportLevel: 'unsupported_too_old',
missing: ['OpenCode 1.4.0 is below supported minimum 1.14.19'],
});
expect(ports.mcpTools.prove).not.toHaveBeenCalled();
});
it('blocks when API capabilities are missing required permission or tool routes', async () => {
const ports = createPorts({
capabilities: capabilities({ ready: false, missing: ['POST permission reply route'] }),
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'capabilities_missing',
launchAllowed: false,
permissionBridgeReady: false,
missing: ['POST permission reply route'],
evidence: {
capabilitiesReady: false,
},
});
});
it('does not require project-specific E2E evidence before runtime readiness checks', async () => {
const ports = createPorts();
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'ready',
launchAllowed: true,
supportLevel: 'production_supported',
});
});
it('blocks when runtime stores need recovery before readiness', async () => {
const ports = createPorts({
runtimeStores: {
ok: false,
reason: 'runtime_store_recovery_required',
diagnostics: ['Incomplete batch must be reconciled before readiness'],
},
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'runtime_store_blocked',
launchAllowed: false,
runtimeStoresReady: false,
missing: ['Incomplete batch must be reconciled before readiness'],
evidence: {
runtimeStoreReadinessReason: 'runtime_store_recovery_required',
},
});
});
it('blocks when required app MCP tools are not proven through OpenCode', async () => {
const ports = createPorts({
toolProof: toolProof({
ok: false,
missingTools: ['runtime_deliver_message'],
diagnostics: [
'OpenCode missing canonical app MCP tool id agent-teams_runtime_deliver_message',
],
}),
});
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'mcp_unavailable',
launchAllowed: false,
appMcpConnected: true,
requiredToolsPresent: false,
missing: ['runtime_deliver_message'],
diagnostics: [
'OpenCode missing canonical app MCP tool id agent-teams_runtime_deliver_message',
],
});
});
it('runs optional execution probe and blocks unavailable selected model', async () => {
const ports = createPorts({
modelProbe: {
outcome: 'unavailable',
reason: 'model rejected by provider',
diagnostics: ['model rejected by provider'],
},
});
await expect(
service(ports).check(readinessInput({ requireExecutionProbe: true }))
).resolves.toMatchObject({
state: 'model_unavailable',
launchAllowed: false,
modelId: 'openai/gpt-5.4-mini',
missing: ['model rejected by provider'],
});
});
it('allows launch when inventory, capabilities, stores, MCP and model probe are healthy', async () => {
const ports = createPorts();
await expect(service(ports).check(readinessInput())).resolves.toMatchObject({
state: 'ready',
launchAllowed: true,
modelId: 'openai/gpt-5.4-mini',
opencodeVersion: '1.14.19',
hostHealthy: true,
appMcpConnected: true,
requiredToolsPresent: true,
permissionBridgeReady: true,
runtimeStoresReady: true,
supportLevel: 'production_supported',
evidence: {
capabilitiesReady: true,
mcpToolProofRoute: '/experimental/tool/ids',
runtimeStoreReadinessReason: 'runtime_store_manifest_valid',
},
});
});
});
function service(ports: ReturnType<typeof createPorts>): OpenCodeTeamLaunchReadinessService {
return new OpenCodeTeamLaunchReadinessService(
ports.inventory,
ports.capabilities,
ports.mcpTools,
ports.runtimeStores,
ports.modelExecution
);
}
function readinessInput(
overrides: Partial<{
projectPath: string;
selectedModel: string | null;
requireExecutionProbe: boolean;
}> = {}
) {
return {
projectPath: '/repo',
selectedModel: 'openai/gpt-5.4-mini',
requireExecutionProbe: false,
...overrides,
};
}
function createPorts(
overrides: {
inventory?: Partial<OpenCodeRuntimeInventory>;
capabilities?: OpenCodeApiCapabilities;
toolProof?: OpenCodeMcpToolProof;
runtimeStores?: RuntimeStoreReadinessCheck;
modelProbe?: {
outcome: 'available' | 'unavailable' | 'not_authenticated' | 'unknown';
reason: string | null;
diagnostics: string[];
};
} = {}
): {
inventory: OpenCodeRuntimeInventoryPort & { probe: ReturnType<typeof vi.fn> };
capabilities: OpenCodeApiCapabilityPort & { detect: ReturnType<typeof vi.fn> };
mcpTools: OpenCodeMcpToolProofPort & { prove: ReturnType<typeof vi.fn> };
runtimeStores: OpenCodeRuntimeStoreReadinessPort & { check: ReturnType<typeof vi.fn> };
modelExecution: OpenCodeModelExecutionProbePort & { verify: ReturnType<typeof vi.fn> };
} {
return {
inventory: {
probe: vi.fn(() => Promise.resolve(inventory(overrides.inventory))),
},
capabilities: {
detect: vi.fn(() => Promise.resolve(overrides.capabilities ?? capabilities())),
},
mcpTools: {
prove: vi.fn(() => Promise.resolve(overrides.toolProof ?? toolProof())),
},
runtimeStores: {
check: vi.fn(() => Promise.resolve(overrides.runtimeStores ?? runtimeStores())),
},
modelExecution: {
verify: vi.fn(() => Promise.resolve(overrides.modelProbe ?? modelProbe())),
},
};
}
function inventory(overrides: Partial<OpenCodeRuntimeInventory> = {}): OpenCodeRuntimeInventory {
return {
detected: true,
binaryPath: '/opt/homebrew/bin/opencode',
installMethod: 'brew',
version: '1.14.19',
authenticated: true,
connectedProviders: ['openai'],
models: ['openai/gpt-5.4-mini'],
diagnostics: [],
...overrides,
};
}
function capabilities(
overrides: Partial<{
ready: boolean;
missing: string[];
}> = {}
): OpenCodeApiCapabilities {
const endpoints = createEmptyEndpointMap();
const evidence = {} as OpenCodeApiCapabilities['evidence'];
for (const key of Object.keys(endpoints) as OpenCodeApiEndpointKey[]) {
endpoints[key] = true;
evidence[key] = 'openapi';
}
if (overrides.ready === false) {
endpoints.permissionReply = false;
endpoints.permissionLegacySessionRespond = false;
}
return {
version: '1.14.19',
source: 'openapi_doc',
endpoints,
requiredForTeamLaunch: {
ready: overrides.ready ?? true,
missing: overrides.missing ?? [],
},
evidence,
diagnostics: [],
};
}
function toolProof(overrides: Partial<OpenCodeMcpToolProof> = {}): OpenCodeMcpToolProof {
return {
ok: true,
route: '/experimental/tool/ids',
canonicalServerName: 'agent_teams',
canonicalExpectedIds: Object.fromEntries(
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => [tool, `agent_teams_${tool}`])
),
observedTools: REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => `agent_teams_${tool}`),
missingTools: [],
matchedByRequiredTool: Object.fromEntries(
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => [tool, `agent_teams_${tool}`])
),
aliasMatchedByRequiredTool: Object.fromEntries(
REQUIRED_AGENT_TEAMS_RUNTIME_TOOLS.map((tool) => [tool, null])
),
diagnostics: [],
...overrides,
};
}
function runtimeStores(): RuntimeStoreReadinessCheck {
return {
ok: true,
reason: 'runtime_store_manifest_valid',
diagnostics: [],
};
}
function modelProbe() {
return {
outcome: 'available' as const,
reason: null,
diagnostics: [],
};
}