fix: add OpenCode status inventory fallback
This commit is contained in:
parent
22c7cf77d0
commit
e06c24a041
3 changed files with 107 additions and 15 deletions
|
|
@ -883,7 +883,7 @@ export class ClaudeMultimodelBridgeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldUseLegacyProviderTimeoutFallback(providerId: CliProviderId): boolean {
|
private shouldUseLegacyProviderTimeoutFallback(providerId: CliProviderId): boolean {
|
||||||
return providerId === 'anthropic' || providerId === 'codex';
|
return providerId === 'anthropic' || providerId === 'codex' || providerId === 'opencode';
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProviderStatusRuntimeTimeout(
|
private getProviderStatusRuntimeTimeout(
|
||||||
|
|
|
||||||
|
|
@ -421,7 +421,7 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
vi.mocked(console.warn).mockClear();
|
vi.mocked(console.warn).mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('explains OpenCode provider status timeouts as runtime inventory failures', async () => {
|
it('falls back to OpenCode model inventory when provider status times out', async () => {
|
||||||
execCliMock.mockImplementation((_binaryPath, args) => {
|
execCliMock.mockImplementation((_binaryPath, args) => {
|
||||||
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||||
if (normalizedArgs === 'runtime status --json --provider opencode --summary') {
|
if (normalizedArgs === 'runtime status --json --provider opencode --summary') {
|
||||||
|
|
@ -431,6 +431,19 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (normalizedArgs === 'model list --json --provider opencode') {
|
||||||
|
return Promise.resolve({
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
providers: {
|
||||||
|
opencode: {
|
||||||
|
models: [{ id: 'opencode/big-pickle', label: 'Big Pickle' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
stderr: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
|
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
|
||||||
});
|
});
|
||||||
|
|
@ -443,18 +456,18 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
|
|
||||||
expect(provider).toMatchObject({
|
expect(provider).toMatchObject({
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
verificationState: 'error',
|
supported: false,
|
||||||
statusMessage: 'Provider status unavailable',
|
authenticated: false,
|
||||||
|
verificationState: 'unknown',
|
||||||
|
statusMessage: null,
|
||||||
|
models: ['opencode/big-pickle'],
|
||||||
});
|
});
|
||||||
expect(provider.detailMessage).toContain(
|
expect(provider.detailMessage ?? '').not.toContain('OpenCode runtime status did not return');
|
||||||
'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 30000ms');
|
|
||||||
expect(execCliMock.mock.calls.map((call) => call[1].join(' '))).toEqual([
|
expect(execCliMock.mock.calls.map((call) => call[1].join(' '))).toEqual([
|
||||||
'runtime status --json --provider opencode --summary',
|
'runtime status --json --provider opencode --summary',
|
||||||
|
'model list --json --provider opencode',
|
||||||
]);
|
]);
|
||||||
|
expect(execCliMock.mock.calls[0][2]?.timeout).toBe(5000);
|
||||||
vi.mocked(console.warn).mockClear();
|
vi.mocked(console.warn).mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -521,7 +534,7 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to scoped legacy probes for Anthropic and Codex aggregate summary timeouts', async () => {
|
it('falls back to scoped legacy probes for aggregate summary timeouts', 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(' ') : '';
|
||||||
if (
|
if (
|
||||||
|
|
@ -602,6 +615,19 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
stderr: '',
|
stderr: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (normalizedArgs === 'model list --json --provider opencode') {
|
||||||
|
return Promise.resolve({
|
||||||
|
stdout: JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
providers: {
|
||||||
|
opencode: {
|
||||||
|
models: [{ id: 'opencode/big-pickle', label: 'Big Pickle' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
stderr: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
|
return Promise.reject(new Error(`Unexpected execCli call: ${normalizedArgs}`));
|
||||||
});
|
});
|
||||||
|
|
@ -613,10 +639,10 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
const providers = await service.getProviderStatuses('/mock/agent_teams_orchestrator');
|
const providers = await service.getProviderStatuses('/mock/agent_teams_orchestrator');
|
||||||
const calls = execCliMock.mock.calls.map((call) => call[1].join(' '));
|
const calls = execCliMock.mock.calls.map((call) => call[1].join(' '));
|
||||||
|
|
||||||
expect(execCliMock).toHaveBeenCalledTimes(7);
|
expect(execCliMock).toHaveBeenCalledTimes(8);
|
||||||
expect(
|
expect(
|
||||||
execCliMock.mock.calls.map((call) => call[2]?.timeout as number).sort((a, b) => a - b)
|
execCliMock.mock.calls.map((call) => call[2]?.timeout as number).sort((a, b) => a - b)
|
||||||
).toEqual([5000, 5000, 15000, 15000, 25000, 25000, 30000]);
|
).toEqual([5000, 5000, 5000, 15000, 15000, 25000, 25000, 25000]);
|
||||||
expect(calls).toEqual(
|
expect(calls).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
'runtime status --json --provider anthropic --summary',
|
'runtime status --json --provider anthropic --summary',
|
||||||
|
|
@ -626,6 +652,7 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
'model list --json --provider anthropic',
|
'model list --json --provider anthropic',
|
||||||
'auth status --json --provider codex',
|
'auth status --json --provider codex',
|
||||||
'model list --json --provider codex',
|
'model list --json --provider codex',
|
||||||
|
'model list --json --provider opencode',
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
expect(providers.map((provider) => provider.providerId)).toEqual([
|
expect(providers.map((provider) => provider.providerId)).toEqual([
|
||||||
|
|
@ -650,8 +677,11 @@ describe('ClaudeMultimodelBridgeService', () => {
|
||||||
});
|
});
|
||||||
expect(providers[2]).toMatchObject({
|
expect(providers[2]).toMatchObject({
|
||||||
providerId: 'opencode',
|
providerId: 'opencode',
|
||||||
verificationState: 'error',
|
supported: false,
|
||||||
statusMessage: 'Provider status unavailable',
|
authenticated: false,
|
||||||
|
verificationState: 'unknown',
|
||||||
|
statusMessage: null,
|
||||||
|
models: ['opencode/big-pickle'],
|
||||||
});
|
});
|
||||||
expect(vi.mocked(console.warn).mock.calls.map((call) => call.join(' '))).toEqual([
|
expect(vi.mocked(console.warn).mock.calls.map((call) => call.join(' '))).toEqual([
|
||||||
expect.stringContaining(
|
expect.stringContaining(
|
||||||
|
|
|
||||||
|
|
@ -704,6 +704,68 @@ describe('CLI status visibility during completed install state', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders OpenCode inventory fallback with model badges instead of unavailable text', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
storeState.cliInstallerState = 'idle';
|
||||||
|
storeState.openCodeRuntimeStatus = {
|
||||||
|
installed: true,
|
||||||
|
source: 'path',
|
||||||
|
state: 'ready',
|
||||||
|
};
|
||||||
|
storeState.cliStatus = createInstalledCliStatus({
|
||||||
|
flavor: 'agent_teams_orchestrator',
|
||||||
|
displayName: 'Multimodel runtime',
|
||||||
|
supportsSelfUpdate: false,
|
||||||
|
showVersionDetails: false,
|
||||||
|
showBinaryPath: false,
|
||||||
|
authLoggedIn: false,
|
||||||
|
authStatusChecking: false,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
providerId: 'opencode',
|
||||||
|
displayName: 'OpenCode (200+ models)',
|
||||||
|
supported: false,
|
||||||
|
authenticated: false,
|
||||||
|
authMethod: null,
|
||||||
|
verificationState: 'unknown',
|
||||||
|
statusMessage: null,
|
||||||
|
models: ['opencode/big-pickle'],
|
||||||
|
modelAvailability: [],
|
||||||
|
canLoginFromUi: false,
|
||||||
|
capabilities: {
|
||||||
|
teamLaunch: false,
|
||||||
|
oneShot: false,
|
||||||
|
},
|
||||||
|
backend: null,
|
||||||
|
availableBackends: [],
|
||||||
|
modelCatalog: null,
|
||||||
|
modelCatalogRefreshState: 'idle',
|
||||||
|
runtimeCapabilities: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(React.createElement(CliStatusBanner));
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(host.textContent).toContain('OpenCode');
|
||||||
|
expect(host.textContent).toContain('Checking...');
|
||||||
|
expect(host.textContent).toContain('big-pickle');
|
||||||
|
expect(host.textContent).not.toContain('Provider status unavailable');
|
||||||
|
expect(host.textContent).not.toContain('Models unavailable for this runtime build');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps connected provider details visible while a refresh is in flight', async () => {
|
it('keeps connected provider details visible while a refresh is in flight', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
storeState.cliInstallerState = 'idle';
|
storeState.cliInstallerState = 'idle';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue