fix(codex): keep catalog hydration in loading state
This commit is contained in:
parent
dd47dfb8a2
commit
3265920ec6
9 changed files with 362 additions and 51 deletions
|
|
@ -2,7 +2,10 @@ import { useEffect, useMemo, useState } from 'react';
|
|||
|
||||
import { useAppTranslation } from '@features/localization/renderer';
|
||||
import { isElectronMode } from '@renderer/api';
|
||||
import { formatProviderStatusText } from '@renderer/components/runtime/providerConnectionUi';
|
||||
import {
|
||||
formatProviderStatusText,
|
||||
shouldMaskCodexNegativeBootstrapState,
|
||||
} from '@renderer/components/runtime/providerConnectionUi';
|
||||
import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice';
|
||||
import { filterMainScreenCliProviders } from '@renderer/utils/geminiUiFreeze';
|
||||
import { isTeamProviderModelVerificationPending } from '@renderer/utils/teamModelAvailability';
|
||||
|
|
@ -37,19 +40,6 @@ function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boo
|
|||
return providerLoading || isTeamProviderModelVerificationPending(provider.providerId, provider);
|
||||
}
|
||||
|
||||
function shouldMaskCodexNegativeBootstrapState(
|
||||
sourceProvider: CliProviderStatus | null,
|
||||
mergedProvider: CliProviderStatus
|
||||
): boolean {
|
||||
return (
|
||||
sourceProvider?.providerId === 'codex' &&
|
||||
sourceProvider.statusMessage === 'Checking...' &&
|
||||
mergedProvider.providerId === 'codex' &&
|
||||
mergedProvider.connection?.codex?.launchReadinessState === 'missing_auth' &&
|
||||
mergedProvider.connection.codex.login.status === 'idle'
|
||||
);
|
||||
}
|
||||
|
||||
function getActivityToneStyles(tone: 'loading' | 'checked' | 'error'): {
|
||||
borderColor: string;
|
||||
backgroundColor: string;
|
||||
|
|
@ -139,7 +129,9 @@ function useProviderActivityDisplay({
|
|||
const loading =
|
||||
isProviderCardLoading(provider, cliProviderStatusLoading[provider.providerId] === true) ||
|
||||
(provider.providerId === 'codex' && codexSnapshotPending) ||
|
||||
shouldMaskCodexNegativeBootstrapState(sourceProvider, provider);
|
||||
shouldMaskCodexNegativeBootstrapState(sourceProvider, provider, {
|
||||
providerLoading: cliProviderStatusLoading[provider.providerId] === true,
|
||||
});
|
||||
|
||||
return {
|
||||
provider,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import {
|
|||
isConnectionManagedRuntimeProvider,
|
||||
isOpenCodeCatalogHydrating,
|
||||
isProviderInventoryOnlyFallback,
|
||||
shouldMaskCodexNegativeBootstrapState,
|
||||
shouldShowProviderConnectAction,
|
||||
shouldShowProviderStatusSkeleton,
|
||||
} from '@renderer/components/runtime/providerConnectionUi';
|
||||
|
|
@ -478,19 +479,6 @@ function isCodexSnapshotPending(
|
|||
return provider.providerId === 'codex' && codexSnapshotPending;
|
||||
}
|
||||
|
||||
function shouldMaskCodexNegativeBootstrapState(
|
||||
sourceProvider: CliProviderStatus | null,
|
||||
mergedProvider: CliProviderStatus
|
||||
): boolean {
|
||||
return (
|
||||
sourceProvider?.providerId === 'codex' &&
|
||||
sourceProvider.statusMessage === 'Checking...' &&
|
||||
mergedProvider.providerId === 'codex' &&
|
||||
mergedProvider.connection?.codex?.launchReadinessState === 'missing_auth' &&
|
||||
mergedProvider.connection.codex.login.status === 'idle'
|
||||
);
|
||||
}
|
||||
|
||||
function getProviderStatusColor(statusText: string, authenticated: boolean): string {
|
||||
if (statusText === 'Checking...') {
|
||||
return 'var(--color-text-secondary)';
|
||||
|
|
@ -991,7 +979,8 @@ const InstalledBanner = ({
|
|||
const sourceProvider = sourceProviderMap.get(provider.providerId) ?? null;
|
||||
const maskNegativeBootstrapState = shouldMaskCodexNegativeBootstrapState(
|
||||
sourceProvider,
|
||||
provider
|
||||
provider,
|
||||
{ providerLoading }
|
||||
);
|
||||
const showSkeleton =
|
||||
shouldShowProviderStatusSkeleton(provider, providerLoading) ||
|
||||
|
|
|
|||
|
|
@ -240,6 +240,60 @@ export function isOpenCodeCatalogHydrating(
|
|||
);
|
||||
}
|
||||
|
||||
export function shouldMaskCodexNegativeBootstrapState(
|
||||
sourceProvider:
|
||||
| Pick<
|
||||
CliProviderStatus,
|
||||
| 'providerId'
|
||||
| 'authenticated'
|
||||
| 'supported'
|
||||
| 'verificationState'
|
||||
| 'statusMessage'
|
||||
| 'models'
|
||||
| 'backend'
|
||||
| 'modelCatalogRefreshState'
|
||||
>
|
||||
| null
|
||||
| undefined,
|
||||
mergedProvider: Pick<CliProviderStatus, 'providerId' | 'connection' | 'modelCatalogRefreshState'>,
|
||||
options: { providerLoading?: boolean } = {}
|
||||
): boolean {
|
||||
if (
|
||||
mergedProvider.providerId !== 'codex' ||
|
||||
mergedProvider.connection?.codex?.launchReadinessState !== 'missing_auth' ||
|
||||
mergedProvider.connection.codex.login.status !== 'idle'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (options.providerLoading || mergedProvider.modelCatalogRefreshState === 'loading') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sourceProvider?.providerId !== 'codex') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sourceProvider.modelCatalogRefreshState === 'loading') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
sourceProvider.statusMessage === 'Checking...' ||
|
||||
sourceProvider.statusMessage === CLI_PROVIDER_STATUS_DEFERRED_MESSAGE
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
sourceProvider.supported === false &&
|
||||
sourceProvider.authenticated === false &&
|
||||
sourceProvider.verificationState === 'unknown' &&
|
||||
sourceProvider.models.length === 0 &&
|
||||
sourceProvider.backend == null
|
||||
);
|
||||
}
|
||||
|
||||
function hasKnownProviderStatus(
|
||||
provider: Pick<
|
||||
CliProviderStatus,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import {
|
|||
getProviderDisconnectAction,
|
||||
isConnectionManagedRuntimeProvider,
|
||||
isOpenCodeCatalogHydrating,
|
||||
shouldMaskCodexNegativeBootstrapState,
|
||||
shouldShowProviderConnectAction,
|
||||
shouldShowProviderStatusSkeleton,
|
||||
} from '@renderer/components/runtime/providerConnectionUi';
|
||||
|
|
@ -93,19 +94,6 @@ function isCodexSnapshotPending(
|
|||
return provider.providerId === 'codex' && codexSnapshotPending;
|
||||
}
|
||||
|
||||
function shouldMaskCodexNegativeBootstrapState(
|
||||
sourceProvider: CliProviderStatus | null,
|
||||
mergedProvider: CliProviderStatus
|
||||
): boolean {
|
||||
return (
|
||||
sourceProvider?.providerId === 'codex' &&
|
||||
sourceProvider.statusMessage === 'Checking...' &&
|
||||
mergedProvider.providerId === 'codex' &&
|
||||
mergedProvider.connection?.codex?.launchReadinessState === 'missing_auth' &&
|
||||
mergedProvider.connection.codex.login.status === 'idle'
|
||||
);
|
||||
}
|
||||
|
||||
function getProviderStatusColor(statusText: string, authenticated: boolean): string {
|
||||
if (statusText === 'Checking...') {
|
||||
return 'var(--color-text-secondary)';
|
||||
|
|
@ -498,7 +486,8 @@ export const CliStatusSection = (): React.JSX.Element | null => {
|
|||
loadingCliProviderMap.get(provider.providerId) ?? null;
|
||||
const maskNegativeBootstrapState = shouldMaskCodexNegativeBootstrapState(
|
||||
sourceProvider,
|
||||
provider
|
||||
provider,
|
||||
{ providerLoading }
|
||||
);
|
||||
const effectiveShowSkeleton = showSkeleton || maskNegativeBootstrapState;
|
||||
const statusText = effectiveShowSkeleton
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ const OPENCODE_PROVIDER_INSTALL_REFRESH_ATTEMPTS = 3;
|
|||
const OPENCODE_PROVIDER_INSTALL_REFRESH_RETRY_DELAY_MS = 700;
|
||||
const CODEX_PROVIDER_INSTALL_REFRESH_ATTEMPTS = 3;
|
||||
const CODEX_PROVIDER_INSTALL_REFRESH_RETRY_DELAY_MS = 700;
|
||||
const CODEX_CATALOG_LOADING_REFRESH_ATTEMPTS = 3;
|
||||
const CODEX_CATALOG_LOADING_REFRESH_RETRY_DELAY_MS = 2_000;
|
||||
|
||||
export const MULTIMODEL_PROVIDER_IDS: CliProviderId[] = isGeminiUiFrozen()
|
||||
? ['anthropic', 'codex', 'opencode']
|
||||
|
|
@ -168,6 +170,15 @@ function getProviderStatus(
|
|||
return status?.providers.find((provider) => provider.providerId === providerId);
|
||||
}
|
||||
|
||||
function isCodexCatalogLoadingSnapshot(provider: CliProviderStatus | undefined): boolean {
|
||||
return (
|
||||
provider?.providerId === 'codex' &&
|
||||
provider.modelCatalog == null &&
|
||||
provider.modelCatalogRefreshState === 'loading' &&
|
||||
provider.runtimeCapabilities?.modelCatalog?.dynamic === true
|
||||
);
|
||||
}
|
||||
|
||||
function hasOpenCodeModels(provider: CliProviderStatus | undefined): boolean {
|
||||
return (
|
||||
provider?.providerId === 'opencode' &&
|
||||
|
|
@ -747,9 +758,60 @@ let cliStatusInFlight: Promise<void> | null = null;
|
|||
const cliProviderStatusInFlight = new Map<string, Promise<void>>();
|
||||
let cliStatusEpoch = 0;
|
||||
const cliProviderStatusSeq = new Map<CliProviderId, number>();
|
||||
const codexCatalogLoadingRefreshAttempts = new Map<CliProviderId, number>();
|
||||
const codexCatalogLoadingRefreshTimers = new Map<CliProviderId, ReturnType<typeof setTimeout>>();
|
||||
let openCodeRuntimeStatusInFlight: Promise<void> | null = null;
|
||||
let codexRuntimeStatusInFlight: Promise<void> | null = null;
|
||||
|
||||
function clearCodexCatalogLoadingRefresh(providerId: CliProviderId): void {
|
||||
const timer = codexCatalogLoadingRefreshTimers.get(providerId);
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
codexCatalogLoadingRefreshTimers.delete(providerId);
|
||||
}
|
||||
codexCatalogLoadingRefreshAttempts.delete(providerId);
|
||||
}
|
||||
|
||||
function scheduleCodexCatalogLoadingRefresh(
|
||||
get: () => Pick<CliInstallerSlice, 'cliStatus' | 'fetchCliProviderStatus'>,
|
||||
providerId: CliProviderId
|
||||
): void {
|
||||
const provider = getProviderStatus(get().cliStatus, providerId);
|
||||
if (!isCodexCatalogLoadingSnapshot(provider)) {
|
||||
clearCodexCatalogLoadingRefresh(providerId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (codexCatalogLoadingRefreshTimers.has(providerId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attempts = codexCatalogLoadingRefreshAttempts.get(providerId) ?? 0;
|
||||
if (attempts >= CODEX_CATALOG_LOADING_REFRESH_ATTEMPTS) {
|
||||
return;
|
||||
}
|
||||
|
||||
codexCatalogLoadingRefreshAttempts.set(providerId, attempts + 1);
|
||||
const timer = setTimeout(() => {
|
||||
codexCatalogLoadingRefreshTimers.delete(providerId);
|
||||
const latestProvider = getProviderStatus(get().cliStatus, providerId);
|
||||
if (!isCodexCatalogLoadingSnapshot(latestProvider)) {
|
||||
codexCatalogLoadingRefreshAttempts.delete(providerId);
|
||||
return;
|
||||
}
|
||||
|
||||
void get().fetchCliProviderStatus(providerId, { silent: true });
|
||||
}, CODEX_CATALOG_LOADING_REFRESH_RETRY_DELAY_MS);
|
||||
(timer as ReturnType<typeof setTimeout> & { unref?: () => void }).unref?.();
|
||||
codexCatalogLoadingRefreshTimers.set(providerId, timer);
|
||||
}
|
||||
|
||||
function scheduleCodexCatalogLoadingRefreshes(
|
||||
get: () => Pick<CliInstallerSlice, 'cliStatus' | 'fetchCliProviderStatus'>
|
||||
): void {
|
||||
scheduleCodexCatalogLoadingRefresh(get, 'codex');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Slice Creator
|
||||
// =============================================================================
|
||||
|
|
@ -877,6 +939,8 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
|
|||
};
|
||||
});
|
||||
|
||||
scheduleCodexCatalogLoadingRefreshes(get);
|
||||
|
||||
if (!metadata.installed) {
|
||||
if (epoch === cliStatusEpoch) {
|
||||
set({
|
||||
|
|
@ -948,6 +1012,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
|
|||
cliProviderStatusLoading: {},
|
||||
};
|
||||
});
|
||||
scheduleCodexCatalogLoadingRefreshes(get);
|
||||
if (status.installed) {
|
||||
for (const provider of status.providers) {
|
||||
if (!isActiveMultimodelProviderId(provider.providerId)) {
|
||||
|
|
@ -1085,6 +1150,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
|
|||
cliProviderStatusLoading: nextLoading,
|
||||
};
|
||||
});
|
||||
scheduleCodexCatalogLoadingRefresh(get, providerId);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : `Failed to refresh ${providerId} status`;
|
||||
|
|
@ -1174,6 +1240,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
|
|||
},
|
||||
};
|
||||
});
|
||||
clearCodexCatalogLoadingRefresh(providerId);
|
||||
} finally {
|
||||
cliProviderStatusInFlight.delete(requestKey);
|
||||
}
|
||||
|
|
@ -1184,6 +1251,7 @@ export const createCliInstallerSlice: StateCreator<AppState, [], [], CliInstalle
|
|||
},
|
||||
|
||||
invalidateCliStatus: async () => {
|
||||
clearCodexCatalogLoadingRefresh('codex');
|
||||
await api.cliInstaller?.invalidateStatus();
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -166,7 +166,13 @@ export function isTeamProviderModelVerificationPending(
|
|||
return true;
|
||||
}
|
||||
|
||||
if (providerStatus.verificationState === 'error') {
|
||||
const verificationState = providerStatus.verificationState as
|
||||
| 'verified'
|
||||
| 'unknown'
|
||||
| 'offline'
|
||||
| 'error'
|
||||
| undefined;
|
||||
if (verificationState === 'error' || providerStatus.modelCatalogRefreshState === 'error') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -174,14 +180,11 @@ export function isTeamProviderModelVerificationPending(
|
|||
const statusMessagePending =
|
||||
statusMessage === 'checking...' ||
|
||||
statusMessage === CLI_PROVIDER_STATUS_DEFERRED_MESSAGE.toLowerCase();
|
||||
if (providerStatus.verificationState !== 'error' && statusMessagePending) {
|
||||
if (statusMessagePending) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
providerStatus.verificationState !== 'error' &&
|
||||
providerStatus.modelCatalogRefreshState === 'loading'
|
||||
) {
|
||||
if (providerStatus.modelCatalogRefreshState === 'loading') {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3045,6 +3045,83 @@ describe('CLI status visibility during completed install state', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('keeps Codex on checking while its model catalog is loading and the live snapshot is only a negative auth result', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.cliInstallerState = 'idle';
|
||||
codexAccountHookState.snapshot = {
|
||||
preferredAuthMode: 'chatgpt',
|
||||
effectiveAuthMode: null,
|
||||
launchAllowed: false,
|
||||
launchIssueMessage: 'Reconnect ChatGPT to refresh the current Codex subscription session.',
|
||||
launchReadinessState: 'missing_auth',
|
||||
appServerState: 'healthy',
|
||||
appServerStatusMessage: null,
|
||||
managedAccount: null,
|
||||
apiKey: {
|
||||
available: true,
|
||||
source: 'environment',
|
||||
sourceLabel: 'Detected from OPENAI_API_KEY',
|
||||
},
|
||||
requiresOpenaiAuth: true,
|
||||
localAccountArtifactsPresent: true,
|
||||
localActiveChatgptAccountPresent: true,
|
||||
login: {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
startedAt: null,
|
||||
},
|
||||
rateLimits: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
storeState.cliStatus = createInstalledCliStatus({
|
||||
flavor: 'agent_teams_orchestrator',
|
||||
displayName: 'agent_teams_orchestrator',
|
||||
supportsSelfUpdate: false,
|
||||
showVersionDetails: false,
|
||||
showBinaryPath: false,
|
||||
authLoggedIn: false,
|
||||
providers: [
|
||||
createCodexNativeRolloutProvider({
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
verificationState: 'unknown',
|
||||
statusMessage: 'Reconnect ChatGPT to refresh the current Codex subscription session.',
|
||||
models: [],
|
||||
modelCatalog: null,
|
||||
modelCatalogRefreshState: 'loading',
|
||||
runtimeCapabilities: {
|
||||
modelCatalog: {
|
||||
dynamic: true,
|
||||
source: 'app-server',
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
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('Checking...');
|
||||
expect(host.textContent).not.toContain(
|
||||
'Reconnect ChatGPT to refresh the current Codex subscription session.'
|
||||
);
|
||||
expect(host.textContent).not.toContain(
|
||||
'Models unavailable for this runtime build'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('explains missing Codex limits when ChatGPT mode is selected but Codex is not logged in', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.cliInstallerState = 'idle';
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
isConnectionManagedRuntimeProvider,
|
||||
isOpenCodeCatalogHydrating,
|
||||
isProviderInventoryOnlyFallback,
|
||||
shouldMaskCodexNegativeBootstrapState,
|
||||
shouldShowProviderConnectAction,
|
||||
} from '@renderer/components/runtime/providerConnectionUi';
|
||||
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
||||
|
|
@ -52,7 +53,8 @@ function createAnthropicProvider(
|
|||
}
|
||||
|
||||
function createCodexProvider(
|
||||
overrides?: Partial<CliProviderStatus['connection']> & {
|
||||
overrides?: Partial<CliProviderStatus> &
|
||||
Partial<CliProviderStatus['connection']> & {
|
||||
authenticated?: boolean;
|
||||
authMethod?: string | null;
|
||||
selectedBackendId?: string | null;
|
||||
|
|
@ -71,7 +73,10 @@ function createCodexProvider(
|
|||
authMethod: overrides?.authMethod ?? 'api_key',
|
||||
verificationState: 'verified',
|
||||
statusMessage: overrides?.statusMessage ?? 'Codex native ready',
|
||||
models: ['gpt-5-codex'],
|
||||
models: overrides?.models ?? ['gpt-5-codex'],
|
||||
modelCatalog: overrides?.modelCatalog,
|
||||
modelCatalogRefreshState: overrides?.modelCatalogRefreshState,
|
||||
runtimeCapabilities: overrides?.runtimeCapabilities,
|
||||
canLoginFromUi: overrides?.canLoginFromUi ?? false,
|
||||
capabilities: {
|
||||
teamLaunch: true,
|
||||
|
|
@ -604,6 +609,42 @@ describe('providerConnectionUi', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('masks stale Codex reconnect state while provider catalog hydration is still loading', () => {
|
||||
const provider = createCodexProvider({
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
models: [],
|
||||
modelCatalogRefreshState: 'loading',
|
||||
runtimeCapabilities: {
|
||||
modelCatalog: {
|
||||
dynamic: true,
|
||||
source: 'app-server',
|
||||
},
|
||||
},
|
||||
codex: {
|
||||
preferredAuthMode: 'chatgpt',
|
||||
effectiveAuthMode: null,
|
||||
appServerState: 'healthy',
|
||||
appServerStatusMessage: null,
|
||||
managedAccount: null,
|
||||
requiresOpenaiAuth: true,
|
||||
localAccountArtifactsPresent: true,
|
||||
localActiveChatgptAccountPresent: true,
|
||||
login: {
|
||||
status: 'idle',
|
||||
error: null,
|
||||
startedAt: null,
|
||||
},
|
||||
rateLimits: null,
|
||||
launchAllowed: false,
|
||||
launchIssueMessage: 'Reconnect ChatGPT to refresh the current Codex subscription session.',
|
||||
launchReadinessState: 'missing_auth',
|
||||
},
|
||||
});
|
||||
|
||||
expect(shouldMaskCodexNegativeBootstrapState(provider, provider)).toBe(true);
|
||||
});
|
||||
|
||||
it('surfaces native auth-required state from the selected backend option', () => {
|
||||
const provider = createCodexProvider({
|
||||
authenticated: false,
|
||||
|
|
|
|||
|
|
@ -1773,6 +1773,104 @@ describe('cliInstallerSlice', () => {
|
|||
expect(provider?.modelCatalog?.defaultModelId).toBe('gpt-5.4');
|
||||
});
|
||||
|
||||
it('retries Codex provider refresh when dynamic catalog hydration remains loading', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const loadingProvider = createMultimodelProvider({
|
||||
providerId: 'codex',
|
||||
displayName: 'Codex',
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
statusMessage: 'Reconnect ChatGPT to refresh the current Codex subscription session.',
|
||||
models: [],
|
||||
modelCatalog: null,
|
||||
modelCatalogRefreshState: 'loading',
|
||||
runtimeCapabilities: {
|
||||
modelCatalog: {
|
||||
dynamic: true,
|
||||
source: 'app-server',
|
||||
},
|
||||
},
|
||||
backend: { kind: 'codex-native', label: 'Codex native' },
|
||||
});
|
||||
const readyProvider = createMultimodelProvider({
|
||||
providerId: 'codex',
|
||||
displayName: 'Codex',
|
||||
authenticated: true,
|
||||
authMethod: 'chatgpt',
|
||||
statusMessage: 'ChatGPT account ready',
|
||||
models: ['gpt-5.4'],
|
||||
modelCatalogRefreshState: 'ready',
|
||||
modelCatalog: {
|
||||
schemaVersion: 1,
|
||||
providerId: 'codex',
|
||||
source: 'app-server',
|
||||
status: 'ready',
|
||||
fetchedAt: '2026-05-17T00:00:00.000Z',
|
||||
staleAt: '2026-05-17T00:10:00.000Z',
|
||||
defaultModelId: 'gpt-5.4',
|
||||
defaultLaunchModel: 'gpt-5.4',
|
||||
models: [
|
||||
{
|
||||
id: 'gpt-5.4',
|
||||
launchModel: 'gpt-5.4',
|
||||
displayName: 'GPT-5.4',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: ['medium'],
|
||||
defaultReasoningEffort: 'medium',
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: false,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
},
|
||||
],
|
||||
diagnostics: {
|
||||
configReadState: 'skipped',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
runtimeCapabilities: {
|
||||
modelCatalog: {
|
||||
dynamic: true,
|
||||
source: 'app-server',
|
||||
},
|
||||
},
|
||||
backend: { kind: 'codex-native', label: 'Codex native' },
|
||||
});
|
||||
|
||||
useStore.setState({
|
||||
cliStatus: createMultimodelStatus([loadingProvider]),
|
||||
});
|
||||
vi.mocked(api.cliInstaller.getProviderStatus)
|
||||
.mockResolvedValueOnce(loadingProvider)
|
||||
.mockResolvedValueOnce(readyProvider);
|
||||
|
||||
await useStore.getState().fetchCliProviderStatus('codex');
|
||||
|
||||
expect(api.cliInstaller.getProviderStatus).toHaveBeenCalledTimes(1);
|
||||
expect(
|
||||
useStore
|
||||
.getState()
|
||||
.cliStatus?.providers.find((provider) => provider.providerId === 'codex')
|
||||
?.modelCatalogRefreshState
|
||||
).toBe('loading');
|
||||
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
|
||||
expect(api.cliInstaller.getProviderStatus).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
useStore
|
||||
.getState()
|
||||
.cliStatus?.providers.find((provider) => provider.providerId === 'codex')
|
||||
).toMatchObject({
|
||||
authenticated: true,
|
||||
statusMessage: 'ChatGPT account ready',
|
||||
models: ['gpt-5.4'],
|
||||
modelCatalogRefreshState: 'ready',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps cached OpenCode model list when summary refresh only reports big-pickle', async () => {
|
||||
const currentProvider = createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
|
|
|
|||
Loading…
Reference in a new issue