@@ -11,6 +11,7 @@ import type {
1111const {
1212 apiKeyHasPreferredMock,
1313 apiKeyLookupMock,
14+ binaryClearCacheMock,
1415 binaryResolveMock,
1516 detectLocalAccountStateMock,
1617 getCachedShellEnvMock,
@@ -24,12 +25,15 @@ const {
2425 readAccountMock,
2526 readAccountSnapshotMock,
2627 readRateLimitsMock,
28+ resolveInteractiveShellEnvBestEffortMock,
2729} = vi . hoisted ( ( ) => ( {
2830 binaryResolveMock : vi . fn ( ) ,
31+ binaryClearCacheMock : vi . fn ( ) ,
2932 apiKeyHasPreferredMock : vi . fn ( ) ,
3033 apiKeyLookupMock : vi . fn ( ) ,
3134 detectLocalAccountStateMock : vi . fn ( ) ,
3235 getCachedShellEnvMock : vi . fn ( ) ,
36+ resolveInteractiveShellEnvBestEffortMock : vi . fn ( ) ,
3337 readAccountMock : vi . fn ( ) ,
3438 readAccountSnapshotMock : vi . fn ( ) ,
3539 readRateLimitsMock : vi . fn ( ) ,
@@ -71,12 +75,14 @@ vi.mock('../../../../src/main/utils/shellEnv', async (importOriginal) => {
7175 return {
7276 ...actual ,
7377 getCachedShellEnv : getCachedShellEnvMock ,
78+ resolveInteractiveShellEnvBestEffort : resolveInteractiveShellEnvBestEffortMock ,
7479 } ;
7580} ) ;
7681
7782vi . mock ( '../../../../src/main/services/infrastructure/codexAppServer' , ( ) => ( {
7883 CodexBinaryResolver : {
7984 resolve : binaryResolveMock ,
85+ clearCache : binaryClearCacheMock ,
8086 } ,
8187 CodexAppServerSessionFactory : class MockCodexAppServerSessionFactory { } ,
8288 JsonRpcStdioClient : class MockJsonRpcStdioClient { } ,
@@ -231,6 +237,9 @@ describe('createCodexAccountFeature', () => {
231237 delete process . env . OPENAI_API_KEY ;
232238 delete process . env . CODEX_API_KEY ;
233239 binaryResolveMock . mockResolvedValue ( '/usr/local/bin/codex' ) ;
240+ binaryClearCacheMock . mockReset ( ) ;
241+ resolveInteractiveShellEnvBestEffortMock . mockReset ( ) ;
242+ resolveInteractiveShellEnvBestEffortMock . mockResolvedValue ( { } ) ;
234243 apiKeyHasPreferredMock . mockResolvedValue ( false ) ;
235244 apiKeyLookupMock . mockResolvedValue ( null ) ;
236245 detectLocalAccountStateMock . mockResolvedValue ( {
@@ -360,6 +369,69 @@ describe('createCodexAccountFeature', () => {
360369 }
361370 } ) ;
362371
372+ it ( 'retries Codex binary discovery after cold shell env resolves before publishing runtime-missing' , async ( ) => {
373+ binaryResolveMock . mockResolvedValueOnce ( null ) . mockResolvedValue ( '/usr/local/bin/codex' ) ;
374+ resolveInteractiveShellEnvBestEffortMock . mockResolvedValue ( {
375+ PATH : '/usr/local/bin:/usr/bin:/bin' ,
376+ } ) ;
377+ readAccountMock . mockResolvedValue ( {
378+ account : createAccountResponse ( ) ,
379+ initialize : {
380+ codexHome : '/Users/test/.codex' ,
381+ platformFamily : 'unix' ,
382+ platformOs : 'macos' ,
383+ } ,
384+ } ) ;
385+
386+ const feature = createCodexAccountFeature ( {
387+ logger : createLoggerPort ( ) ,
388+ configManager : createConfigManager ( 'chatgpt' ) ,
389+ } ) ;
390+
391+ try {
392+ const snapshot = await feature . refreshSnapshot ( ) ;
393+
394+ expect ( resolveInteractiveShellEnvBestEffortMock ) . toHaveBeenCalledWith (
395+ expect . objectContaining ( {
396+ timeoutMs : 12_000 ,
397+ fallbackEnv : process . env ,
398+ } )
399+ ) ;
400+ expect ( binaryClearCacheMock ) . toHaveBeenCalledTimes ( 1 ) ;
401+ expect ( binaryResolveMock ) . toHaveBeenCalledTimes ( 2 ) ;
402+ expect ( snapshot . appServerState ) . toBe ( 'healthy' ) ;
403+ expect ( snapshot . launchReadinessState ) . toBe ( 'ready_chatgpt' ) ;
404+ expect ( snapshot . launchIssueMessage ) . toBeNull ( ) ;
405+ } finally {
406+ await feature . dispose ( ) ;
407+ }
408+ } ) ;
409+
410+ it ( 'still reports runtime-missing after the cold binary retry cannot find Codex' , async ( ) => {
411+ binaryResolveMock . mockResolvedValue ( null ) ;
412+ resolveInteractiveShellEnvBestEffortMock . mockResolvedValue ( {
413+ PATH : '/usr/bin:/bin' ,
414+ } ) ;
415+
416+ const feature = createCodexAccountFeature ( {
417+ logger : createLoggerPort ( ) ,
418+ configManager : createConfigManager ( 'chatgpt' ) ,
419+ } ) ;
420+
421+ try {
422+ const snapshot = await feature . refreshSnapshot ( ) ;
423+
424+ expect ( resolveInteractiveShellEnvBestEffortMock ) . toHaveBeenCalledTimes ( 1 ) ;
425+ expect ( binaryClearCacheMock ) . toHaveBeenCalledTimes ( 1 ) ;
426+ expect ( binaryResolveMock ) . toHaveBeenCalledTimes ( 2 ) ;
427+ expect ( snapshot . appServerState ) . toBe ( 'runtime-missing' ) ;
428+ expect ( snapshot . launchReadinessState ) . toBe ( 'runtime_missing' ) ;
429+ expect ( snapshot . launchIssueMessage ) . toContain ( 'Codex CLI not found' ) ;
430+ } finally {
431+ await feature . dispose ( ) ;
432+ }
433+ } ) ;
434+
363435 it ( 'reuses a fresh refresh snapshot when the request does not need stronger data' , async ( ) => {
364436 readAccountMock . mockResolvedValue ( {
365437 account : createAccountResponse ( ) ,
0 commit comments