test(runtime): cover opencode timeout diagnostics
This commit is contained in:
parent
7e6ebce093
commit
71bfab1758
2 changed files with 277 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(' ') : '';
|
||||
|
|
|
|||
Loading…
Reference in a new issue