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 {
|
||||
return providerId === 'anthropic' || providerId === 'codex';
|
||||
return providerId === 'anthropic' || providerId === 'codex' || providerId === 'opencode';
|
||||
}
|
||||
|
||||
private getProviderStatusRuntimeTimeout(
|
||||
|
|
|
|||
|
|
@ -421,7 +421,7 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
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) => {
|
||||
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||
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}`));
|
||||
});
|
||||
|
|
@ -443,18 +456,18 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
|
||||
expect(provider).toMatchObject({
|
||||
providerId: 'opencode',
|
||||
verificationState: 'error',
|
||||
statusMessage: 'Provider status unavailable',
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
verificationState: 'unknown',
|
||||
statusMessage: null,
|
||||
models: ['opencode/big-pickle'],
|
||||
});
|
||||
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 30000ms');
|
||||
expect(provider.detailMessage ?? '').not.toContain('OpenCode runtime status did not return');
|
||||
expect(execCliMock.mock.calls.map((call) => call[1].join(' '))).toEqual([
|
||||
'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();
|
||||
});
|
||||
|
||||
|
|
@ -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) => {
|
||||
const normalizedArgs = Array.isArray(args) ? args.join(' ') : '';
|
||||
if (
|
||||
|
|
@ -602,6 +615,19 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
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}`));
|
||||
});
|
||||
|
|
@ -613,10 +639,10 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
const providers = await service.getProviderStatuses('/mock/agent_teams_orchestrator');
|
||||
const calls = execCliMock.mock.calls.map((call) => call[1].join(' '));
|
||||
|
||||
expect(execCliMock).toHaveBeenCalledTimes(7);
|
||||
expect(execCliMock).toHaveBeenCalledTimes(8);
|
||||
expect(
|
||||
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.arrayContaining([
|
||||
'runtime status --json --provider anthropic --summary',
|
||||
|
|
@ -626,6 +652,7 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
'model list --json --provider anthropic',
|
||||
'auth status --json --provider codex',
|
||||
'model list --json --provider codex',
|
||||
'model list --json --provider opencode',
|
||||
])
|
||||
);
|
||||
expect(providers.map((provider) => provider.providerId)).toEqual([
|
||||
|
|
@ -650,8 +677,11 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
});
|
||||
expect(providers[2]).toMatchObject({
|
||||
providerId: 'opencode',
|
||||
verificationState: 'error',
|
||||
statusMessage: 'Provider status unavailable',
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
verificationState: 'unknown',
|
||||
statusMessage: null,
|
||||
models: ['opencode/big-pickle'],
|
||||
});
|
||||
expect(vi.mocked(console.warn).mock.calls.map((call) => call.join(' '))).toEqual([
|
||||
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 () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.cliInstallerState = 'idle';
|
||||
|
|
|
|||
Loading…
Reference in a new issue