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 () => {
|
it('parses the runtime JSON response after noisy brace logs', async () => {
|
||||||
const validResponse = {
|
const validResponse = {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,108 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
vi.mocked(console.warn).mockClear();
|
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 () => {
|
it('does not cascade aggregate summary timeouts into slower fallback probes', async () => {
|
||||||
execCliMock.mockImplementation((_binaryPath, args, options) => {
|
execCliMock.mockImplementation((_binaryPath, args, options) => {
|
||||||
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue