@@ -2953,11 +2953,12 @@ describe("codex manager cli commands", () => {
29532953 expect ( withAccountStorageTransactionMock ) . toHaveBeenCalledTimes ( 1 ) ;
29542954 expect ( storageState . accounts ) . toHaveLength ( 2 ) ;
29552955 expect ( storageState . activeIndex ) . toBe ( 1 ) ;
2956- expect ( storageState . activeIndexByFamily . codex ) . toBe ( 1 ) ;
2957- expect ( setCodexCliActiveSelectionMock ) . toHaveBeenCalledTimes ( 1 ) ;
2958- } ) ;
2956+ expect ( storageState . activeIndexByFamily . codex ) . toBe ( 1 ) ;
2957+ expect ( setCodexCliActiveSelectionMock ) . toHaveBeenCalledTimes ( 1 ) ;
2958+ } ) ;
29592959
29602960 it ( "supports --manual login without launching a browser" , async ( ) => {
2961+ setInteractiveTTY ( true ) ;
29612962 const now = Date . now ( ) ;
29622963 let storageState = {
29632964 version : 3 as const ,
@@ -2999,13 +3000,19 @@ describe("codex manager cli commands", () => {
29993000 promptQuestionMock . mockResolvedValueOnce (
30003001 "http://127.0.0.1:1455/auth/callback?code=oauth-code&state=oauth-state" ,
30013002 ) ;
3003+ const logSpy = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } ) ;
30023004
30033005 const { runCodexMultiAuthCli } = await import ( "../lib/codex-manager.js" ) ;
30043006 const exitCode = await runCodexMultiAuthCli ( [ "auth" , "login" , "--manual" ] ) ;
3007+ const renderedLogs = logSpy . mock . calls . flat ( ) . map ( ( entry ) => String ( entry ) ) ;
30053008
30063009 expect ( exitCode ) . toBe ( 0 ) ;
30073010 expect ( openBrowserUrlMock ) . not . toHaveBeenCalled ( ) ;
30083011 expect ( waitForCodeMock ) . not . toHaveBeenCalled ( ) ;
3012+ expect ( renderedLogs . some ( ( entry ) => entry . includes ( "Callback listener unavailable" ) ) ) . toBe (
3013+ true ,
3014+ ) ;
3015+ expect ( renderedLogs . some ( ( entry ) => entry . includes ( "No callback received" ) ) ) . toBe ( false ) ;
30093016 expect ( storageState . accounts ) . toHaveLength ( 1 ) ;
30103017 } ) ;
30113018
@@ -3058,6 +3065,96 @@ describe("codex manager cli commands", () => {
30583065 expect ( storageState . accounts ) . toHaveLength ( 1 ) ;
30593066 } ) ;
30603067
3068+ it ( "rejects mismatched manual callback state in non-tty mode without persisting login" , async ( ) => {
3069+ setInteractiveTTY ( false ) ;
3070+ let storageState = {
3071+ version : 3 as const ,
3072+ activeIndex : 0 ,
3073+ activeIndexByFamily : { codex : 0 } ,
3074+ accounts : [ ] as Array < Record < string , unknown > > ,
3075+ } ;
3076+ loadAccountsMock . mockImplementation ( async ( ) => structuredClone ( storageState ) ) ;
3077+ saveAccountsMock . mockImplementation ( async ( nextStorage ) => {
3078+ storageState = structuredClone ( nextStorage ) ;
3079+ } ) ;
3080+ promptLoginModeMock . mockResolvedValueOnce ( { mode : "cancel" } ) ;
3081+ promptQuestionMock . mockResolvedValueOnce (
3082+ "http://127.0.0.1:1455/auth/callback?code=oauth-code&state=wrong-state" ,
3083+ ) ;
3084+
3085+ const authModule = await import ( "../lib/auth/auth.js" ) ;
3086+ vi . mocked ( authModule . createAuthorizationFlow ) . mockResolvedValueOnce ( {
3087+ pkce : { challenge : "pkce-challenge" , verifier : "pkce-verifier" } ,
3088+ state : "oauth-state" ,
3089+ url : "https://auth.openai.com/mock" ,
3090+ } ) ;
3091+ const exchangeAuthorizationCodeMock = vi . mocked ( authModule . exchangeAuthorizationCode ) ;
3092+
3093+ const browserModule = await import ( "../lib/auth/browser.js" ) ;
3094+ const openBrowserUrlMock = vi . mocked ( browserModule . openBrowserUrl ) ;
3095+ const serverModule = await import ( "../lib/auth/server.js" ) ;
3096+ vi . mocked ( serverModule . startLocalOAuthServer ) . mockRejectedValueOnce (
3097+ new Error ( "port in use" ) ,
3098+ ) ;
3099+
3100+ const { runCodexMultiAuthCli } = await import ( "../lib/codex-manager.js" ) ;
3101+ const exitCode = await runCodexMultiAuthCli ( [ "auth" , "login" , "--manual" ] ) ;
3102+
3103+ expect ( exitCode ) . toBe ( 0 ) ;
3104+ expect ( promptQuestionMock ) . toHaveBeenCalledWith ( "" ) ;
3105+ expect ( openBrowserUrlMock ) . not . toHaveBeenCalled ( ) ;
3106+ expect ( exchangeAuthorizationCodeMock ) . not . toHaveBeenCalled ( ) ;
3107+ expect ( storageState . accounts ) . toHaveLength ( 0 ) ;
3108+ } ) ;
3109+
3110+ it ( "falls back to pasted manual input when Windows-style callback bind fails" , async ( ) => {
3111+ setInteractiveTTY ( false ) ;
3112+ const now = Date . now ( ) ;
3113+ let storageState = {
3114+ version : 3 as const ,
3115+ activeIndex : 0 ,
3116+ activeIndexByFamily : { codex : 0 } ,
3117+ accounts : [ ] as Array < Record < string , unknown > > ,
3118+ } ;
3119+ loadAccountsMock . mockImplementation ( async ( ) => structuredClone ( storageState ) ) ;
3120+ saveAccountsMock . mockImplementation ( async ( nextStorage ) => {
3121+ storageState = structuredClone ( nextStorage ) ;
3122+ } ) ;
3123+ promptLoginModeMock . mockResolvedValueOnce ( { mode : "cancel" } ) ;
3124+ promptQuestionMock . mockResolvedValueOnce (
3125+ "http://127.0.0.1:1455/auth/callback?code=oauth-code&state=oauth-state" ,
3126+ ) ;
3127+
3128+ const authModule = await import ( "../lib/auth/auth.js" ) ;
3129+ vi . mocked ( authModule . createAuthorizationFlow ) . mockResolvedValueOnce ( {
3130+ pkce : { challenge : "pkce-challenge" , verifier : "pkce-verifier" } ,
3131+ state : "oauth-state" ,
3132+ url : "https://auth.openai.com/mock" ,
3133+ } ) ;
3134+ vi . mocked ( authModule . exchangeAuthorizationCode ) . mockResolvedValueOnce ( {
3135+ type : "success" ,
3136+ access : "access-eacces" ,
3137+ refresh : "refresh-eacces" ,
3138+ expires : now + 7_200_000 ,
3139+ idToken : "id-token-eacces" ,
3140+ multiAccount : true ,
3141+ } ) ;
3142+
3143+ const browserModule = await import ( "../lib/auth/browser.js" ) ;
3144+ const openBrowserUrlMock = vi . mocked ( browserModule . openBrowserUrl ) ;
3145+ const serverModule = await import ( "../lib/auth/server.js" ) ;
3146+ vi . mocked ( serverModule . startLocalOAuthServer ) . mockRejectedValueOnce (
3147+ Object . assign ( new Error ( "permission denied" ) , { code : "EACCES" } ) ,
3148+ ) ;
3149+
3150+ const { runCodexMultiAuthCli } = await import ( "../lib/codex-manager.js" ) ;
3151+ const exitCode = await runCodexMultiAuthCli ( [ "auth" , "login" , "--manual" ] ) ;
3152+
3153+ expect ( exitCode ) . toBe ( 0 ) ;
3154+ expect ( promptQuestionMock ) . toHaveBeenCalledWith ( "" ) ;
3155+ expect ( openBrowserUrlMock ) . not . toHaveBeenCalled ( ) ;
3156+ expect ( storageState . accounts ) . toHaveLength ( 1 ) ;
3157+ } ) ;
30613158 it ( "preserves distinct same-email workspaces when oauth login reuses a refresh token" , async ( ) => {
30623159 const now = Date . now ( ) ;
30633160 let storageState = {
0 commit comments