From 92a3124e3f723d3326e56e9b7554a56f0f44a956 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 19 Apr 2026 19:46:00 +0300 Subject: [PATCH] fix(runtime): preserve codex-native lane state truth --- .../runtime/providerConnectionUi.ts | 39 ++++++ .../ClaudeMultimodelBridgeService.test.ts | 61 ++++++++++ .../runtime/providerConnectionUi.test.ts | 115 ++++++++++++++++++ 3 files changed, 215 insertions(+) diff --git a/src/renderer/components/runtime/providerConnectionUi.ts b/src/renderer/components/runtime/providerConnectionUi.ts index a5ce047d..ec04902d 100644 --- a/src/renderer/components/runtime/providerConnectionUi.ts +++ b/src/renderer/components/runtime/providerConnectionUi.ts @@ -76,6 +76,24 @@ function isCodexNativeLane(provider: CliProviderStatus): boolean { ); } +function getSelectedRuntimeBackendOption( + provider: CliProviderStatus +): NonNullable[number] | null { + const options = provider.availableBackends ?? []; + if (options.length === 0) { + return null; + } + + const selectedBackendId = provider.selectedBackendId ?? null; + const resolvedBackendId = provider.resolvedBackendId ?? null; + + return ( + options.find((option) => option.id === selectedBackendId) ?? + options.find((option) => option.id === resolvedBackendId) ?? + null + ); +} + export function isConnectionManagedRuntimeProvider(provider: CliProviderStatus): boolean { return provider.providerId === 'codex' && (provider.availableBackends?.length ?? 0) === 0; } @@ -106,6 +124,27 @@ export function getProviderCurrentRuntimeSummary(provider: CliProviderStatus): s } export function formatProviderStatusText(provider: CliProviderStatus): string { + const selectedBackendOption = getSelectedRuntimeBackendOption(provider); + + if ( + isCodexNativeLane(provider) && + selectedBackendOption && + selectedBackendOption.state && + selectedBackendOption.state !== 'ready' + ) { + return ( + selectedBackendOption.statusMessage ?? provider.statusMessage ?? 'Codex native unavailable' + ); + } + + if ( + isCodexNativeLane(provider) && + selectedBackendOption?.audience === 'internal' && + selectedBackendOption.statusMessage + ) { + return selectedBackendOption.statusMessage; + } + if (!provider.supported) { return provider.statusMessage ?? 'Unavailable in current runtime'; } diff --git a/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts b/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts index fc2d55dd..fabf3d7a 100644 --- a/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts +++ b/test/main/services/runtime/ClaudeMultimodelBridgeService.test.ts @@ -506,4 +506,65 @@ describe('ClaudeMultimodelBridgeService', () => { statusMessage: 'Ready for internal use', }); }); + + it('preserves codex-native runtime-missing rollout states from runtime status payloads', async () => { + execCliMock.mockResolvedValue({ + stdout: JSON.stringify({ + providers: { + codex: { + supported: true, + authenticated: false, + authMethod: null, + verificationState: 'unknown', + canLoginFromUi: false, + statusMessage: 'Codex native runtime not ready', + detailMessage: 'Codex native runtime requires the codex CLI binary to be installed.', + selectedBackendId: 'codex-native', + resolvedBackendId: null, + availableBackends: [ + { + id: 'codex-native', + label: 'Codex native', + selectable: false, + recommended: false, + available: false, + state: 'runtime-missing', + audience: 'internal', + statusMessage: 'Codex CLI not found', + detailMessage: 'Install the codex CLI before enabling the lane.', + }, + ], + capabilities: { + teamLaunch: true, + oneShot: true, + extensions: { + plugins: { status: 'unsupported', ownership: 'shared', reason: 'Phase 1' }, + mcp: { status: 'unsupported', ownership: 'shared', reason: 'Phase 1' }, + skills: { status: 'unsupported', ownership: 'shared', reason: 'Phase 1' }, + apiKeys: { status: 'supported', ownership: 'shared', reason: null }, + }, + }, + backend: null, + }, + }, + }), + stderr: '', + exitCode: 0, + }); + + const { ClaudeMultimodelBridgeService } = + await import('@main/services/runtime/ClaudeMultimodelBridgeService'); + const service = new ClaudeMultimodelBridgeService(); + + const codex = await service.getProviderStatus('/mock/agent_teams_orchestrator', 'codex'); + + expect(codex.availableBackends?.find((backend) => backend.id === 'codex-native')).toMatchObject({ + id: 'codex-native', + selectable: false, + available: false, + state: 'runtime-missing', + audience: 'internal', + statusMessage: 'Codex CLI not found', + }); + }); }); diff --git a/test/renderer/components/runtime/providerConnectionUi.test.ts b/test/renderer/components/runtime/providerConnectionUi.test.ts index b65abd29..ae759a6f 100644 --- a/test/renderer/components/runtime/providerConnectionUi.test.ts +++ b/test/renderer/components/runtime/providerConnectionUi.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { + formatProviderStatusText, getProviderConnectionModeSummary, getProviderCredentialSummary, getProviderCurrentRuntimeSummary, @@ -252,4 +253,118 @@ describe('providerConnectionUi', () => { expect(getProviderCredentialSummary(provider)).toBe('Saved API key available in Manage'); }); + + it('keeps locked codex-native lanes visible instead of flattening them to connected-via-api-key', () => { + const provider = createCodexProvider({ + authenticated: true, + authMethod: 'api_key', + statusMessage: 'Codex native runtime ready', + selectedBackendId: 'codex-native', + resolvedBackendId: 'codex-native', + availableBackends: [ + { + id: 'codex-native', + label: 'Codex native', + description: 'Use codex exec JSON mode.', + selectable: false, + recommended: false, + available: true, + state: 'locked', + audience: 'internal', + statusMessage: 'Ready but locked', + detailMessage: 'Internal rollout only.', + }, + ], + backend: { + kind: 'codex-native', + label: 'Codex native', + }, + }); + + expect(formatProviderStatusText(provider)).toBe('Ready but locked'); + }); + + it('keeps internal codex-native ready state explicit instead of showing a generic auth label', () => { + const provider = createCodexProvider({ + authenticated: true, + authMethod: 'api_key', + statusMessage: 'Codex native runtime ready', + selectedBackendId: 'codex-native', + resolvedBackendId: 'codex-native', + availableBackends: [ + { + id: 'codex-native', + label: 'Codex native', + description: 'Use codex exec JSON mode.', + selectable: true, + recommended: false, + available: true, + state: 'ready', + audience: 'internal', + statusMessage: 'Ready for internal use', + detailMessage: 'Internal rollout only.', + }, + ], + backend: { + kind: 'codex-native', + label: 'Codex native', + }, + }); + + expect(formatProviderStatusText(provider)).toBe('Ready for internal use'); + }); + + it('surfaces native auth-required state from the selected backend option', () => { + const provider = createCodexProvider({ + authenticated: false, + authMethod: null, + statusMessage: 'Codex native runtime not ready', + selectedBackendId: 'codex-native', + resolvedBackendId: null, + availableBackends: [ + { + id: 'codex-native', + label: 'Codex native', + description: 'Use codex exec JSON mode.', + selectable: false, + recommended: false, + available: false, + state: 'authentication-required', + audience: 'internal', + statusMessage: 'Authentication required', + detailMessage: 'Set CODEX_API_KEY.', + }, + ], + backend: null, + }); + + expect(formatProviderStatusText(provider)).toBe('Authentication required'); + }); + + it('surfaces native runtime-missing state from the selected backend option', () => { + const provider = createCodexProvider({ + authenticated: false, + authMethod: null, + statusMessage: 'Codex native runtime not ready', + selectedBackendId: 'codex-native', + resolvedBackendId: null, + availableBackends: [ + { + id: 'codex-native', + label: 'Codex native', + description: 'Use codex exec JSON mode.', + selectable: false, + recommended: false, + available: false, + state: 'runtime-missing', + audience: 'internal', + statusMessage: 'Codex CLI not found', + detailMessage: 'Install the codex CLI before enabling the lane.', + }, + ], + backend: null, + }); + + expect(formatProviderStatusText(provider)).toBe('Codex CLI not found'); + }); });