fix(opencode): preserve loading state during runtime hydration
This commit is contained in:
parent
94b97c4930
commit
6a9f281eca
8 changed files with 517 additions and 11 deletions
|
|
@ -122,9 +122,14 @@ function patchCachedProviderStatus(providerStatus: CliProviderStatus | null): vo
|
|||
return;
|
||||
}
|
||||
|
||||
const nextProviders = cachedStatus.value.providers.map((provider) =>
|
||||
provider.providerId === providerStatus.providerId ? providerStatus : provider
|
||||
const hasProvider = cachedStatus.value.providers.some(
|
||||
(provider) => provider.providerId === providerStatus.providerId
|
||||
);
|
||||
const nextProviders = hasProvider
|
||||
? cachedStatus.value.providers.map((provider) =>
|
||||
provider.providerId === providerStatus.providerId ? providerStatus : provider
|
||||
)
|
||||
: [...cachedStatus.value.providers, providerStatus];
|
||||
const authenticatedProvider = nextProviders.find((provider) => provider.authenticated) ?? null;
|
||||
|
||||
cachedStatus = {
|
||||
|
|
|
|||
|
|
@ -499,6 +499,10 @@ export class CliInstallerService {
|
|||
providerId: 'gemini',
|
||||
displayName: 'Gemini',
|
||||
},
|
||||
{
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
},
|
||||
] as const
|
||||
).map((provider) => ({
|
||||
...provider,
|
||||
|
|
@ -510,7 +514,7 @@ export class CliInstallerService {
|
|||
statusMessage: 'Checking...',
|
||||
models: [],
|
||||
modelAvailability: [],
|
||||
canLoginFromUi: true,
|
||||
canLoginFromUi: provider.providerId !== 'opencode',
|
||||
capabilities: {
|
||||
teamLaunch: false,
|
||||
oneShot: false,
|
||||
|
|
|
|||
|
|
@ -86,6 +86,19 @@ function getSelectedRuntimeBackendOption(
|
|||
);
|
||||
}
|
||||
|
||||
export function isProviderInventoryOnlyFallback(provider: CliProviderStatus): boolean {
|
||||
return (
|
||||
provider.supported === false &&
|
||||
provider.authenticated === false &&
|
||||
provider.authMethod === null &&
|
||||
provider.verificationState === 'unknown' &&
|
||||
provider.models.length > 0 &&
|
||||
provider.backend == null &&
|
||||
(provider.availableBackends?.length ?? 0) === 0 &&
|
||||
provider.capabilities.teamLaunch === false
|
||||
);
|
||||
}
|
||||
|
||||
export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus): boolean {
|
||||
return provider.providerId === 'codex';
|
||||
}
|
||||
|
|
@ -146,6 +159,10 @@ export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): s
|
|||
}
|
||||
|
||||
export function formatProviderStatusText(provider: CliProviderStatus): string {
|
||||
if (isProviderInventoryOnlyFallback(provider)) {
|
||||
return 'Checking...';
|
||||
}
|
||||
|
||||
const selectedBackendOption = getSelectedRuntimeBackendOption(provider);
|
||||
|
||||
if (provider.providerId === 'codex') {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,12 @@ import { isVersionOlder, normalizeVersion } from '@shared/utils/version';
|
|||
import { create } from 'zustand';
|
||||
|
||||
import { createChangeReviewSlice } from './slices/changeReviewSlice';
|
||||
import { createCliInstallerSlice } from './slices/cliInstallerSlice';
|
||||
import {
|
||||
createCliInstallerSlice,
|
||||
getIncompleteMultimodelProviderIds,
|
||||
getModelOnlyFallbackProviderIds,
|
||||
mergeCliStatusPreservingHydratedProviders,
|
||||
} from './slices/cliInstallerSlice';
|
||||
import { createConfigSlice } from './slices/configSlice';
|
||||
import { createConnectionSlice } from './slices/connectionSlice';
|
||||
import { createContextSlice } from './slices/contextSlice';
|
||||
|
|
@ -50,6 +55,7 @@ import type { AppState } from './types';
|
|||
import type {
|
||||
ActiveToolCall,
|
||||
CliInstallerProgress,
|
||||
CliProviderId,
|
||||
LeadContextUsage,
|
||||
ScheduleChangeEvent,
|
||||
TeamChangeEvent,
|
||||
|
|
@ -1456,7 +1462,31 @@ export function initializeNotificationListeners(): () => void {
|
|||
break;
|
||||
case 'status':
|
||||
if (progress.status) {
|
||||
useStore.setState({ cliStatus: progress.status });
|
||||
let modelOnlyFallbackProviderIds: CliProviderId[] = [];
|
||||
useStore.setState((state) => {
|
||||
const nextStatus = mergeCliStatusPreservingHydratedProviders(
|
||||
state.cliStatus,
|
||||
progress.status!
|
||||
);
|
||||
const incompleteProviderIds = getIncompleteMultimodelProviderIds(nextStatus);
|
||||
modelOnlyFallbackProviderIds = getModelOnlyFallbackProviderIds(nextStatus);
|
||||
|
||||
return {
|
||||
cliStatus: nextStatus,
|
||||
cliProviderStatusLoading:
|
||||
incompleteProviderIds.length > 0
|
||||
? {
|
||||
...state.cliProviderStatusLoading,
|
||||
...Object.fromEntries(
|
||||
incompleteProviderIds.map((providerId) => [providerId, true])
|
||||
),
|
||||
}
|
||||
: state.cliProviderStatusLoading,
|
||||
};
|
||||
});
|
||||
for (const providerId of modelOnlyFallbackProviderIds) {
|
||||
void useStore.getState().fetchCliProviderStatus(providerId, { silent: false });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,11 +67,28 @@ export function createLoadingMultimodelCliStatus(): CliInstallationStatus {
|
|||
};
|
||||
}
|
||||
|
||||
function isModelOnlyFallbackProviderStatus(provider: CliProviderStatus): boolean {
|
||||
return (
|
||||
provider.supported === false &&
|
||||
provider.authenticated === false &&
|
||||
provider.authMethod === null &&
|
||||
provider.verificationState === 'unknown' &&
|
||||
provider.models.length > 0 &&
|
||||
provider.backend == null &&
|
||||
(provider.availableBackends?.length ?? 0) === 0 &&
|
||||
provider.capabilities.teamLaunch === false
|
||||
);
|
||||
}
|
||||
|
||||
function isHydratedMultimodelProviderStatus(provider: CliProviderStatus | undefined): boolean {
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isModelOnlyFallbackProviderStatus(provider)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(
|
||||
provider.supported === false &&
|
||||
provider.authenticated === false &&
|
||||
|
|
@ -80,11 +97,81 @@ function isHydratedMultimodelProviderStatus(provider: CliProviderStatus | undefi
|
|||
provider.statusMessage === 'Checking...' &&
|
||||
provider.models.length === 0 &&
|
||||
provider.backend == null &&
|
||||
(provider.availableBackends?.length ?? 0) === 0 &&
|
||||
provider.connection == null
|
||||
(provider.availableBackends?.length ?? 0) === 0
|
||||
);
|
||||
}
|
||||
|
||||
export function getIncompleteMultimodelProviderIds(
|
||||
status: CliInstallationStatus | null
|
||||
): CliProviderId[] {
|
||||
if (!status || status.flavor !== 'agent_teams_orchestrator' || !status.installed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return status.providers
|
||||
.filter((provider) => !isHydratedMultimodelProviderStatus(provider))
|
||||
.map((provider) => provider.providerId);
|
||||
}
|
||||
|
||||
export function getModelOnlyFallbackProviderIds(
|
||||
status: CliInstallationStatus | null
|
||||
): CliProviderId[] {
|
||||
if (!status || status.flavor !== 'agent_teams_orchestrator' || !status.installed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return status.providers
|
||||
.filter((provider) => isModelOnlyFallbackProviderStatus(provider))
|
||||
.map((provider) => provider.providerId);
|
||||
}
|
||||
|
||||
export function mergeCliStatusPreservingHydratedProviders(
|
||||
current: CliInstallationStatus | null,
|
||||
incoming: CliInstallationStatus
|
||||
): CliInstallationStatus {
|
||||
if (
|
||||
!current ||
|
||||
current.flavor !== 'agent_teams_orchestrator' ||
|
||||
incoming.flavor !== 'agent_teams_orchestrator'
|
||||
) {
|
||||
return incoming;
|
||||
}
|
||||
|
||||
const currentProvidersById = new Map(
|
||||
current.providers.map((provider) => [provider.providerId, provider])
|
||||
);
|
||||
const incomingProviderIds = new Set(incoming.providers.map((provider) => provider.providerId));
|
||||
const providers = incoming.providers.map((incomingProvider) => {
|
||||
const currentProvider = currentProvidersById.get(incomingProvider.providerId);
|
||||
if (
|
||||
currentProvider &&
|
||||
isHydratedMultimodelProviderStatus(currentProvider) &&
|
||||
!isHydratedMultimodelProviderStatus(incomingProvider)
|
||||
) {
|
||||
return currentProvider;
|
||||
}
|
||||
return incomingProvider;
|
||||
});
|
||||
|
||||
for (const currentProvider of current.providers) {
|
||||
if (
|
||||
!incomingProviderIds.has(currentProvider.providerId) &&
|
||||
isHydratedMultimodelProviderStatus(currentProvider)
|
||||
) {
|
||||
providers.push(currentProvider);
|
||||
}
|
||||
}
|
||||
|
||||
const authenticatedProvider = providers.find((provider) => provider.authenticated) ?? null;
|
||||
|
||||
return {
|
||||
...incoming,
|
||||
providers,
|
||||
authLoggedIn: providers.some((provider) => provider.authenticated),
|
||||
authMethod: authenticatedProvider?.authMethod ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Slice Interface
|
||||
// =============================================================================
|
||||
|
|
@ -178,8 +265,13 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
|
|||
return {};
|
||||
}
|
||||
|
||||
const mergedMetadata = mergeCliStatusPreservingHydratedProviders(
|
||||
state.cliStatus,
|
||||
metadata
|
||||
);
|
||||
|
||||
return {
|
||||
cliStatus: metadata,
|
||||
cliStatus: mergedMetadata,
|
||||
cliStatusLoading: false,
|
||||
cliProviderStatusLoading: {},
|
||||
cliStatusError: state.cliStatusError,
|
||||
|
|
@ -207,7 +299,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
|
|||
|
||||
return {
|
||||
cliStatus: {
|
||||
...metadata,
|
||||
...mergeCliStatusPreservingHydratedProviders(state.cliStatus, metadata),
|
||||
launchError: metadata.launchError ?? null,
|
||||
authStatusChecking: metadata.installed && pendingProviderIds.length > 0,
|
||||
},
|
||||
|
|
@ -270,7 +362,10 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
|
|||
if (epoch !== cliStatusEpoch) {
|
||||
return;
|
||||
}
|
||||
set({ cliStatus: status, cliProviderStatusLoading: {} });
|
||||
set((state) => ({
|
||||
cliStatus: mergeCliStatusPreservingHydratedProviders(state.cliStatus, status),
|
||||
cliProviderStatusLoading: {},
|
||||
}));
|
||||
if (status.installed) {
|
||||
for (const provider of status.providers) {
|
||||
void get().fetchCliProviderStatus(provider.providerId, {
|
||||
|
|
|
|||
|
|
@ -128,6 +128,34 @@ describe('CliInstallerService', () => {
|
|||
expect(status.updateAvailable).toBe(false);
|
||||
});
|
||||
|
||||
it('includes OpenCode in unavailable multimodel bootstrap status', async () => {
|
||||
allowConsoleLogs();
|
||||
vi.mocked(getConfiguredCliFlavor).mockReturnValue('agent_teams_orchestrator');
|
||||
vi.mocked(getCliFlavorUiOptions).mockReturnValue({
|
||||
displayName: 'agent_teams_orchestrator',
|
||||
supportsSelfUpdate: false,
|
||||
showVersionDetails: false,
|
||||
showBinaryPath: false,
|
||||
});
|
||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue(null);
|
||||
|
||||
const status = await service.getStatus();
|
||||
const openCodeStatus = status.providers.find((provider) => provider.providerId === 'opencode');
|
||||
|
||||
expect(status.providers.map((provider) => provider.providerId)).toEqual([
|
||||
'anthropic',
|
||||
'codex',
|
||||
'gemini',
|
||||
'opencode',
|
||||
]);
|
||||
expect(openCodeStatus).toMatchObject({
|
||||
displayName: 'OpenCode',
|
||||
supported: false,
|
||||
statusMessage: 'Runtime not found.',
|
||||
canLoginFromUi: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not mark the CLI installed when the version probe cannot confirm the binary', async () => {
|
||||
allowConsoleLogs();
|
||||
vi.mocked(ClaudeBinaryResolver.resolve).mockResolvedValue('/usr/local/bin/claude');
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
getProviderConnectionModeSummary,
|
||||
getProviderCredentialSummary,
|
||||
getProviderCurrentRuntimeSummary,
|
||||
isProviderInventoryOnlyFallback,
|
||||
isConnectionManagedRuntimeProvider,
|
||||
shouldShowProviderConnectAction,
|
||||
} from '@renderer/components/runtime/providerConnectionUi';
|
||||
|
|
@ -134,6 +135,41 @@ function createCodexProvider(
|
|||
};
|
||||
}
|
||||
|
||||
function createOpenCodeProvider(
|
||||
overrides?: Partial<CliProviderStatus>
|
||||
): CliProviderStatus {
|
||||
return {
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
verificationState: 'verified',
|
||||
statusMessage: null,
|
||||
detailMessage: null,
|
||||
models: ['opencode/minimax-m2.5-free'],
|
||||
modelAvailability: [],
|
||||
modelVerificationState: 'idle',
|
||||
canLoginFromUi: false,
|
||||
capabilities: {
|
||||
teamLaunch: true,
|
||||
oneShot: false,
|
||||
extensions: createDefaultCliExtensionCapabilities(),
|
||||
},
|
||||
selectedBackendId: null,
|
||||
resolvedBackendId: null,
|
||||
availableBackends: [],
|
||||
externalRuntimeDiagnostics: [],
|
||||
backend: {
|
||||
kind: 'opencode-cli',
|
||||
label: 'OpenCode CLI',
|
||||
authMethodDetail: 'ok',
|
||||
},
|
||||
connection: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('providerConnectionUi', () => {
|
||||
it('hides Anthropic preferred auth summary once the provider is already authenticated', () => {
|
||||
const provider = createAnthropicProvider({
|
||||
|
|
@ -297,6 +333,34 @@ describe('providerConnectionUi', () => {
|
|||
expect(formatProviderStatusText(provider)).toBe('Codex native ready');
|
||||
});
|
||||
|
||||
it('treats OpenCode inventory-only fallback as still loading', () => {
|
||||
const provider = createOpenCodeProvider({
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
verificationState: 'unknown',
|
||||
statusMessage: null,
|
||||
models: ['opencode/minimax-m2.5-free'],
|
||||
capabilities: {
|
||||
teamLaunch: false,
|
||||
oneShot: false,
|
||||
extensions: createDefaultCliExtensionCapabilities(),
|
||||
},
|
||||
backend: null,
|
||||
connection: {
|
||||
supportsOAuth: false,
|
||||
supportsApiKey: false,
|
||||
configurableAuthModes: [],
|
||||
configuredAuthMode: null,
|
||||
apiKeyConfigured: false,
|
||||
apiKeySource: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(isProviderInventoryOnlyFallback(provider)).toBe(true);
|
||||
expect(formatProviderStatusText(provider)).toBe('Checking...');
|
||||
});
|
||||
|
||||
it('surfaces degraded ChatGPT verification warnings instead of flattening them to ready', () => {
|
||||
const provider = createCodexProvider({
|
||||
authenticated: false,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ vi.mock('@renderer/api', () => ({
|
|||
|
||||
import { api } from '@renderer/api';
|
||||
import { useStore } from '@renderer/store';
|
||||
import {
|
||||
getIncompleteMultimodelProviderIds,
|
||||
getModelOnlyFallbackProviderIds,
|
||||
mergeCliStatusPreservingHydratedProviders,
|
||||
} from '@renderer/store/slices/cliInstallerSlice';
|
||||
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
||||
|
||||
import type { CliInstallationStatus } from '@shared/types';
|
||||
|
|
@ -78,7 +83,14 @@ function createMultimodelProvider(
|
|||
extensions: createDefaultCliExtensionCapabilities(),
|
||||
},
|
||||
backend: null,
|
||||
connection: null,
|
||||
connection: {
|
||||
supportsOAuth: false,
|
||||
supportsApiKey: false,
|
||||
configurableAuthModes: [],
|
||||
configuredAuthMode: null,
|
||||
apiKeyConfigured: false,
|
||||
apiKeySource: null,
|
||||
},
|
||||
selectedBackendId: null,
|
||||
resolvedBackendId: null,
|
||||
availableBackends: [],
|
||||
|
|
@ -86,6 +98,29 @@ function createMultimodelProvider(
|
|||
};
|
||||
}
|
||||
|
||||
function createMultimodelStatus(
|
||||
providers: CliInstallationStatus['providers']
|
||||
): CliInstallationStatus {
|
||||
const authenticatedProvider = providers.find((provider) => provider.authenticated) ?? null;
|
||||
|
||||
return {
|
||||
flavor: 'agent_teams_orchestrator',
|
||||
displayName: 'Multimodel runtime',
|
||||
supportsSelfUpdate: false,
|
||||
showVersionDetails: false,
|
||||
showBinaryPath: true,
|
||||
installed: true,
|
||||
installedVersion: '0.0.3',
|
||||
binaryPath: '/Users/belief/.agent-teams/runtime-cache/0.0.3/darwin-arm64/claude-multimodel',
|
||||
latestVersion: null,
|
||||
updateAvailable: false,
|
||||
authLoggedIn: providers.some((provider) => provider.authenticated),
|
||||
authStatusChecking: false,
|
||||
authMethod: authenticatedProvider?.authMethod ?? null,
|
||||
providers,
|
||||
};
|
||||
}
|
||||
|
||||
describe('cliInstallerSlice', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -115,6 +150,145 @@ describe('cliInstallerSlice', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('mergeCliStatusPreservingHydratedProviders', () => {
|
||||
it('does not let model-only OpenCode fallback overwrite hydrated runtime status', () => {
|
||||
const current = createMultimodelStatus([
|
||||
createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
models: ['opencode/minimax-m2.5-free'],
|
||||
canLoginFromUi: false,
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
}),
|
||||
]);
|
||||
const incoming = createMultimodelStatus([
|
||||
createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
verificationState: 'unknown',
|
||||
statusMessage: null,
|
||||
models: ['opencode/minimax-m2.5-free'],
|
||||
canLoginFromUi: false,
|
||||
capabilities: {
|
||||
teamLaunch: false,
|
||||
oneShot: false,
|
||||
extensions: createDefaultCliExtensionCapabilities(),
|
||||
},
|
||||
backend: null,
|
||||
availableBackends: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
const merged = mergeCliStatusPreservingHydratedProviders(current, incoming);
|
||||
|
||||
expect(merged.providers.find((provider) => provider.providerId === 'opencode')).toMatchObject({
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
});
|
||||
});
|
||||
|
||||
it('classifies model-only OpenCode fallback as incomplete for progress events', () => {
|
||||
const status = createMultimodelStatus([
|
||||
createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
verificationState: 'unknown',
|
||||
statusMessage: null,
|
||||
models: ['opencode/minimax-m2.5-free'],
|
||||
canLoginFromUi: false,
|
||||
capabilities: {
|
||||
teamLaunch: false,
|
||||
oneShot: false,
|
||||
extensions: createDefaultCliExtensionCapabilities(),
|
||||
},
|
||||
backend: null,
|
||||
availableBackends: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(getIncompleteMultimodelProviderIds(status)).toEqual(['opencode']);
|
||||
expect(getModelOnlyFallbackProviderIds(status)).toEqual(['opencode']);
|
||||
});
|
||||
|
||||
it('keeps connection-enriched checking placeholders incomplete until provider hydration finishes', () => {
|
||||
const status = createMultimodelStatus([
|
||||
createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
verificationState: 'unknown',
|
||||
statusMessage: 'Checking...',
|
||||
models: [],
|
||||
canLoginFromUi: false,
|
||||
capabilities: {
|
||||
teamLaunch: false,
|
||||
oneShot: false,
|
||||
extensions: createDefaultCliExtensionCapabilities(),
|
||||
},
|
||||
backend: null,
|
||||
availableBackends: [],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(getIncompleteMultimodelProviderIds(status)).toEqual(['opencode']);
|
||||
expect(getModelOnlyFallbackProviderIds(status)).toEqual([]);
|
||||
});
|
||||
|
||||
it('still allows real OpenCode runtime errors to replace previous ready status', () => {
|
||||
const current = createMultimodelStatus([
|
||||
createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
models: ['opencode/minimax-m2.5-free'],
|
||||
canLoginFromUi: false,
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
}),
|
||||
]);
|
||||
const incoming = createMultimodelStatus([
|
||||
createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
verificationState: 'error',
|
||||
statusMessage: 'Runtime not found.',
|
||||
models: [],
|
||||
canLoginFromUi: false,
|
||||
capabilities: {
|
||||
teamLaunch: false,
|
||||
oneShot: false,
|
||||
extensions: createDefaultCliExtensionCapabilities(),
|
||||
},
|
||||
backend: null,
|
||||
}),
|
||||
]);
|
||||
|
||||
const merged = mergeCliStatusPreservingHydratedProviders(current, incoming);
|
||||
|
||||
expect(merged.providers.find((provider) => provider.providerId === 'opencode')).toMatchObject({
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
verificationState: 'error',
|
||||
statusMessage: 'Runtime not found.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchCliStatus', () => {
|
||||
it('updates cliStatus from API', async () => {
|
||||
const mockStatus: CliInstallationStatus = {
|
||||
|
|
@ -412,6 +586,95 @@ describe('cliInstallerSlice', () => {
|
|||
statusMessage: 'ChatGPT account ready',
|
||||
});
|
||||
});
|
||||
|
||||
it('refreshes OpenCode when bootstrap metadata only has fallback models', async () => {
|
||||
const mockStatus: CliInstallationStatus = {
|
||||
flavor: 'agent_teams_orchestrator',
|
||||
displayName: 'Multimodel runtime',
|
||||
supportsSelfUpdate: false,
|
||||
showVersionDetails: false,
|
||||
showBinaryPath: true,
|
||||
installed: true,
|
||||
installedVersion: '0.0.3',
|
||||
binaryPath: '/Users/belief/.agent-teams/runtime-cache/0.0.3/darwin-arm64/claude-multimodel',
|
||||
latestVersion: null,
|
||||
updateAvailable: false,
|
||||
authLoggedIn: true,
|
||||
authStatusChecking: true,
|
||||
authMethod: 'oauth_token',
|
||||
providers: [
|
||||
createMultimodelProvider({
|
||||
providerId: 'anthropic',
|
||||
displayName: 'Anthropic',
|
||||
authenticated: true,
|
||||
authMethod: 'oauth_token',
|
||||
statusMessage: 'Connected',
|
||||
}),
|
||||
createMultimodelProvider({
|
||||
providerId: 'codex',
|
||||
displayName: 'Codex',
|
||||
statusMessage: 'Codex unavailable',
|
||||
}),
|
||||
createMultimodelProvider({
|
||||
providerId: 'gemini',
|
||||
displayName: 'Gemini',
|
||||
statusMessage: 'Ready',
|
||||
}),
|
||||
createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
verificationState: 'unknown',
|
||||
statusMessage: null,
|
||||
models: ['opencode/minimax-m2.5-free'],
|
||||
canLoginFromUi: false,
|
||||
capabilities: {
|
||||
teamLaunch: false,
|
||||
oneShot: false,
|
||||
extensions: createDefaultCliExtensionCapabilities(),
|
||||
},
|
||||
backend: null,
|
||||
availableBackends: [],
|
||||
}),
|
||||
],
|
||||
};
|
||||
vi.mocked(api.cliInstaller.getStatus).mockResolvedValue(mockStatus);
|
||||
vi.mocked(api.cliInstaller.getProviderStatus).mockImplementation(async (providerId) => {
|
||||
if (providerId === 'opencode') {
|
||||
return createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
statusMessage: null,
|
||||
models: ['opencode/minimax-m2.5-free'],
|
||||
canLoginFromUi: false,
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
});
|
||||
}
|
||||
throw new Error(`Unexpected provider status request for ${providerId}`);
|
||||
});
|
||||
|
||||
await useStore.getState().bootstrapCliStatus({ multimodelEnabled: true });
|
||||
|
||||
expect(api.cliInstaller.getProviderStatus).toHaveBeenCalledTimes(1);
|
||||
expect(api.cliInstaller.getProviderStatus).toHaveBeenCalledWith('opencode');
|
||||
expect(useStore.getState().cliProviderStatusLoading).toEqual({
|
||||
anthropic: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
});
|
||||
expect(useStore.getState().cliStatus?.providers.find((provider) => provider.providerId === 'opencode'))
|
||||
.toMatchObject({
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('installCli', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue