test(runtime): cover opencode timeout diagnostics

This commit is contained in:
777genius 2026-05-22 16:48:40 +03:00
parent 7e6ebce093
commit 71bfab1758
2 changed files with 277 additions and 0 deletions

View file

@ -275,6 +275,181 @@ describe('AgentTeamsRuntimeProviderManagementCliClient', () => {
);
});
it('returns structured diagnostics when provider directory loading times out', async () => {
const error = new Error(
'Command timed out after 45000ms: /repo/cli-dev runtime providers directory --runtime opencode --json'
);
Object.assign(error, {
stdout: 'inventory started\n',
stderr: 'OpenCode provider key=sk-secret-value-123456 still probing\n',
});
execCliMock.mockRejectedValue(error);
const client = new AgentTeamsRuntimeProviderManagementCliClient();
const response = await client.loadProviderDirectory({
runtimeId: 'opencode',
projectPath: '/Users/test/project',
query: null,
filter: 'all',
limit: 50,
cursor: null,
refresh: false,
});
expect(response.error?.message).toContain(
'OpenCode provider settings timed out while waiting for the Agent Teams runtime.'
);
expect(response.error?.message).toContain(
'This is not enough evidence to conclude that OpenCode auth is missing.'
);
expect(response.error?.message).toContain('OpenCode provider key=...redacted');
expect(response.error?.message).not.toContain('sk-secret-value-123456');
expect(response.error?.diagnostics?.summary).toBe(
'OpenCode provider settings timed out while waiting for the Agent Teams runtime.'
);
expect(response.error?.diagnostics?.command).toBe(
'/repo/cli-dev runtime providers directory --runtime opencode --json --project-path /Users/test/project --filter all --limit 50'
);
expect(response.error?.diagnostics?.stderrPreview).toBe(
'OpenCode provider key=...redacted still probing'
);
expect(response.error?.diagnostics?.stdoutPreview).toBe('inventory started');
expect(response.error?.diagnostics?.hints).toContain(
'If the runtime binary is stale, update Agent Teams so the runtime can return a degraded OpenCode diagnostic instead of timing out.'
);
});
it('preserves runtime-side degraded JSON errors from rejected command output', async () => {
const error = new Error('Command failed after runtime returned degraded JSON');
Object.assign(error, {
stdout: '',
stderr: JSON.stringify({
schemaVersion: 1,
runtimeId: 'opencode',
error: {
code: 'runtime-unhealthy',
message:
'OpenCode inventory probe timed out after 12000ms during opencode providers list',
recoverable: true,
diagnostics: {
summary: 'OpenCode inventory probe timed out',
likelyCause: 'OpenCode providers list did not finish before the runtime budget.',
command:
'/repo/cli-dev runtime providers view --runtime opencode --json --compact',
stderrPreview: 'provider api_key: sk-secret-value-123456',
hints: ['Check OpenCode CLI startup and local OpenCode plugins.'],
},
},
}),
});
execCliMock.mockRejectedValue(error);
const client = new AgentTeamsRuntimeProviderManagementCliClient();
const response = await client.loadView({
runtimeId: 'opencode',
});
expect(response.error?.message).toBe(
'OpenCode inventory probe timed out after 12000ms during opencode providers list'
);
expect(response.error?.diagnostics?.summary).toBe('OpenCode inventory probe timed out');
expect(response.error?.diagnostics?.likelyCause).toBe(
'OpenCode providers list did not finish before the runtime budget.'
);
expect(response.error?.diagnostics?.stderrPreview).toBe(
'provider api_key: ...redacted'
);
expect(response.error?.diagnostics?.stderrPreview).not.toContain('sk-secret-value-123456');
expect(response.error?.diagnostics?.hints).toContain(
'Check OpenCode CLI startup and local OpenCode plugins.'
);
});
it('preserves degraded JSON from stderr when stdout contains noisy logs', async () => {
const error = new Error('Command failed after mixed runtime output');
Object.assign(error, {
stdout: 'runtime preflight log {not json}\n',
stderr: JSON.stringify({
schemaVersion: 1,
runtimeId: 'opencode',
error: {
code: 'runtime-unhealthy',
message:
'OpenCode inventory probe timed out after 12000ms during opencode agent list',
recoverable: true,
diagnostics: {
summary: 'OpenCode inventory probe timed out',
likelyCause: 'OpenCode agent inventory did not finish before the runtime budget.',
stderrPreview: 'agent token=sk-secret-value-123456',
hints: ['Check OpenCode agent listing and local OpenCode plugins.'],
},
},
}),
});
execCliMock.mockRejectedValue(error);
const client = new AgentTeamsRuntimeProviderManagementCliClient();
const response = await client.loadView({
runtimeId: 'opencode',
});
expect(response.error?.message).toBe(
'OpenCode inventory probe timed out after 12000ms during opencode agent list'
);
expect(response.error?.diagnostics?.likelyCause).toBe(
'OpenCode agent inventory did not finish before the runtime budget.'
);
expect(response.error?.diagnostics?.stderrPreview).toBe(
'agent token=...redacted'
);
expect(JSON.stringify(response.error?.diagnostics)).not.toContain('sk-secret-value-123456');
});
it('preserves degraded JSON printed to stdout before a desktop timeout', async () => {
const error = new Error(
'Command timed out after 45000ms: /repo/cli-dev runtime providers view --runtime opencode --json --compact'
);
Object.assign(error, {
stdout: JSON.stringify({
schemaVersion: 1,
runtimeId: 'opencode',
error: {
code: 'runtime-unhealthy',
message:
'OpenCode inventory probe timed out after 12000ms during opencode models --verbose',
recoverable: true,
diagnostics: {
summary: 'OpenCode inventory probe timed out',
likelyCause: 'OpenCode model inventory did not finish before the runtime budget.',
command:
'/repo/cli-dev runtime providers view --runtime opencode --json --compact',
stdoutPreview: 'model api_key: sk-secret-value-123456',
hints: ['Check OpenCode model listing and local OpenCode plugins.'],
},
},
}),
stderr: 'outer timeout after runtime json\n',
});
execCliMock.mockRejectedValue(error);
const client = new AgentTeamsRuntimeProviderManagementCliClient();
const response = await client.loadView({
runtimeId: 'opencode',
});
expect(response.error?.message).toBe(
'OpenCode inventory probe timed out after 12000ms during opencode models --verbose'
);
expect(response.error?.diagnostics?.summary).toBe('OpenCode inventory probe timed out');
expect(response.error?.diagnostics?.likelyCause).toBe(
'OpenCode model inventory did not finish before the runtime budget.'
);
expect(response.error?.diagnostics?.stdoutPreview).toBe(
'model api_key: ...redacted'
);
expect(JSON.stringify(response.error?.diagnostics)).not.toContain('sk-secret-value-123456');
});
it('parses the runtime JSON response after noisy brace logs', async () => {
const validResponse = {
schemaVersion: 1,

View file

@ -380,6 +380,108 @@ describe('ClaudeMultimodelBridgeService', () => {
vi.mocked(console.warn).mockClear();
});
it('explains OpenCode provider status timeouts as runtime inventory failures', async () => {
execCliMock.mockImplementation((_binaryPath, args) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
if (normalizedArgs === 'runtime status --json --provider opencode --summary') {
return Promise.reject(
new Error(
'Command timed out after 25000ms: /mock/agent_teams_orchestrator runtime status --json --provider opencode --summary'
)
);
}
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
});
const { ClaudeMultimodelBridgeService } =
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
const service = new ClaudeMultimodelBridgeService();
const provider = await service.getProviderStatus('/mock/agent_teams_orchestrator', 'opencode');
expect(provider).toMatchObject({
providerId: 'opencode',
verificationState: 'error',
statusMessage: 'Provider status unavailable',
});
expect(provider.detailMessage).toContain(
'OpenCode runtime status did not return before the desktop timeout.'
);
expect(provider.detailMessage).toContain(
'not necessarily that OpenCode auth is missing'
);
expect(provider.detailMessage).toContain('provider/model inventory');
expect(provider.detailMessage).toContain('Raw timeout detail: Command timed out after 25000ms');
expect(execCliMock.mock.calls.map((call) => call[1].join(' '))).toEqual([
'runtime status --json --provider opencode --summary',
]);
vi.mocked(console.warn).mockClear();
});
it('maps runtime-side OpenCode degraded status without replacing it with a generic error', async () => {
execCliMock.mockImplementation((_binaryPath, args) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
if (normalizedArgs === 'runtime status --json --provider opencode --summary') {
return Promise.resolve({
stdout: JSON.stringify({
schemaVersion: 2,
providers: {
opencode: {
supported: true,
authenticated: false,
authMethod: null,
verificationState: 'error',
canLoginFromUi: false,
statusMessage: 'OpenCode probe incomplete',
detailMessage:
'OpenCode inventory probe timed out after 12000ms during opencode providers list',
capabilities: {
teamLaunch: false,
oneShot: false,
extensions: {
plugins: { status: 'read-only', ownership: 'provider-scoped' },
mcp: { status: 'read-only', ownership: 'provider-scoped' },
skills: { status: 'read-only', ownership: 'provider-scoped' },
apiKeys: { status: 'read-only', ownership: 'provider-scoped' },
},
},
backend: {
kind: 'opencode-cli',
label: 'OpenCode CLI',
authMethodDetail: null,
},
},
},
}),
stderr: '',
});
}
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
});
const { ClaudeMultimodelBridgeService } =
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
const service = new ClaudeMultimodelBridgeService();
const provider = await service.getProviderStatus('/mock/agent_teams_orchestrator', 'opencode');
expect(provider).toMatchObject({
providerId: 'opencode',
verificationState: 'error',
statusMessage: 'OpenCode probe incomplete',
detailMessage:
'OpenCode inventory probe timed out after 12000ms during opencode providers list',
supported: true,
authenticated: false,
});
expect(provider.detailMessage).not.toContain('Provider status unavailable');
expect(execCliMock.mock.calls.map((call) => call[1].join(' '))).toEqual([
'runtime status --json --provider opencode --summary',
]);
});
it('does not cascade aggregate summary timeouts into slower fallback probes', async () => {
execCliMock.mockImplementation((_binaryPath, args, options) => {
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';