@@ -3016,6 +3016,118 @@ describe("codex manager cli commands", () => {
30163016 expect ( storageState . accounts ) . toHaveLength ( 1 ) ;
30173017 } ) ;
30183018
3019+ it ( "supports interactive manual login selection without waiting for a callback" , async ( ) => {
3020+ setInteractiveTTY ( true ) ;
3021+ const now = Date . now ( ) ;
3022+ let storageState = {
3023+ version : 3 as const ,
3024+ activeIndex : 0 ,
3025+ activeIndexByFamily : { codex : 0 } ,
3026+ accounts : [ ] as Array < Record < string , unknown > > ,
3027+ } ;
3028+ loadAccountsMock . mockImplementation ( async ( ) => structuredClone ( storageState ) ) ;
3029+ saveAccountsMock . mockImplementation ( async ( nextStorage ) => {
3030+ storageState = structuredClone ( nextStorage ) ;
3031+ } ) ;
3032+ promptLoginModeMock . mockResolvedValueOnce ( { mode : "cancel" } ) ;
3033+ promptAddAnotherAccountMock . mockResolvedValue ( false ) ;
3034+ selectMock . mockResolvedValueOnce ( "manual" ) ;
3035+
3036+ const authModule = await import ( "../lib/auth/auth.js" ) ;
3037+ vi . mocked ( authModule . createAuthorizationFlow ) . mockResolvedValueOnce ( {
3038+ pkce : { challenge : "pkce-challenge" , verifier : "pkce-verifier" } ,
3039+ state : "oauth-state" ,
3040+ url : "https://auth.openai.com/mock" ,
3041+ } ) ;
3042+ vi . mocked ( authModule . exchangeAuthorizationCode ) . mockResolvedValueOnce ( {
3043+ type : "success" ,
3044+ access : "access-manual-choice" ,
3045+ refresh : "refresh-manual-choice" ,
3046+ expires : now + 7_200_000 ,
3047+ idToken : "id-token-manual-choice" ,
3048+ multiAccount : true ,
3049+ } ) ;
3050+
3051+ const browserModule = await import ( "../lib/auth/browser.js" ) ;
3052+ const openBrowserUrlMock = vi . mocked ( browserModule . openBrowserUrl ) ;
3053+ const serverModule = await import ( "../lib/auth/server.js" ) ;
3054+ const waitForCodeMock = vi . fn ( async ( ) => ( { code : "oauth-code" } ) ) ;
3055+ vi . mocked ( serverModule . startLocalOAuthServer ) . mockResolvedValueOnce ( {
3056+ ready : true ,
3057+ waitForCode : waitForCodeMock ,
3058+ close : vi . fn ( ) ,
3059+ } ) ;
3060+ promptQuestionMock . mockResolvedValueOnce (
3061+ "http://127.0.0.1:1455/auth/callback?code=oauth-code&state=oauth-state" ,
3062+ ) ;
3063+ const logSpy = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } ) ;
3064+
3065+ const { runCodexMultiAuthCli } = await import ( "../lib/codex-manager.js" ) ;
3066+ const exitCode = await runCodexMultiAuthCli ( [ "auth" , "login" ] ) ;
3067+ const renderedLogs = logSpy . mock . calls . flat ( ) . map ( ( entry ) => String ( entry ) ) ;
3068+
3069+ expect ( exitCode ) . toBe ( 0 ) ;
3070+ expect ( selectMock ) . toHaveBeenCalled ( ) ;
3071+ expect ( openBrowserUrlMock ) . not . toHaveBeenCalled ( ) ;
3072+ expect ( waitForCodeMock ) . not . toHaveBeenCalled ( ) ;
3073+ expect ( renderedLogs . some ( ( entry ) => entry . includes ( "Callback listener unavailable" ) ) ) . toBe (
3074+ true ,
3075+ ) ;
3076+ expect ( renderedLogs . some ( ( entry ) => entry . includes ( "No callback received" ) ) ) . toBe ( false ) ;
3077+ expect ( storageState . accounts ) . toHaveLength ( 1 ) ;
3078+ } ) ;
3079+
3080+ it ( "falls back to pasted callback input when browser launch is suppressed" , async ( ) => {
3081+ setInteractiveTTY ( true ) ;
3082+ const now = Date . now ( ) ;
3083+ let storageState = {
3084+ version : 3 as const ,
3085+ activeIndex : 0 ,
3086+ activeIndexByFamily : { codex : 0 } ,
3087+ accounts : [ ] as Array < Record < string , unknown > > ,
3088+ } ;
3089+ loadAccountsMock . mockImplementation ( async ( ) => structuredClone ( storageState ) ) ;
3090+ saveAccountsMock . mockImplementation ( async ( nextStorage ) => {
3091+ storageState = structuredClone ( nextStorage ) ;
3092+ } ) ;
3093+ promptLoginModeMock . mockResolvedValueOnce ( { mode : "cancel" } ) ;
3094+ promptAddAnotherAccountMock . mockResolvedValue ( false ) ;
3095+ promptQuestionMock . mockResolvedValueOnce (
3096+ "http://127.0.0.1:1455/auth/callback?code=oauth-code&state=oauth-state" ,
3097+ ) ;
3098+
3099+ const authModule = await import ( "../lib/auth/auth.js" ) ;
3100+ vi . mocked ( authModule . createAuthorizationFlow ) . mockResolvedValueOnce ( {
3101+ pkce : { challenge : "pkce-challenge" , verifier : "pkce-verifier" } ,
3102+ state : "oauth-state" ,
3103+ url : "https://auth.openai.com/mock" ,
3104+ } ) ;
3105+ vi . mocked ( authModule . exchangeAuthorizationCode ) . mockResolvedValueOnce ( {
3106+ type : "success" ,
3107+ access : "access-suppressed" ,
3108+ refresh : "refresh-suppressed" ,
3109+ expires : now + 7_200_000 ,
3110+ idToken : "id-token-suppressed" ,
3111+ multiAccount : true ,
3112+ } ) ;
3113+
3114+ const browserModule = await import ( "../lib/auth/browser.js" ) ;
3115+ const openBrowserUrlMock = vi . mocked ( browserModule . openBrowserUrl ) ;
3116+ vi . mocked ( browserModule . isBrowserLaunchSuppressed ) . mockReturnValueOnce ( true ) ;
3117+ const serverModule = await import ( "../lib/auth/server.js" ) ;
3118+ const startLocalOAuthServerMock = vi . mocked ( serverModule . startLocalOAuthServer ) ;
3119+ startLocalOAuthServerMock . mockRejectedValueOnce ( new Error ( "suppressed browser mode" ) ) ;
3120+
3121+ const { runCodexMultiAuthCli } = await import ( "../lib/codex-manager.js" ) ;
3122+ const exitCode = await runCodexMultiAuthCli ( [ "auth" , "login" ] ) ;
3123+
3124+ expect ( exitCode ) . toBe ( 0 ) ;
3125+ expect ( promptQuestionMock ) . toHaveBeenCalled ( ) ;
3126+ expect ( openBrowserUrlMock ) . not . toHaveBeenCalled ( ) ;
3127+ expect ( startLocalOAuthServerMock ) . toHaveBeenCalledTimes ( 1 ) ;
3128+ expect ( storageState . accounts ) . toHaveLength ( 1 ) ;
3129+ } ) ;
3130+
30193131 it ( "accepts manual callback input in non-tty mode when --manual is set" , async ( ) => {
30203132 setInteractiveTTY ( false ) ;
30213133 const now = Date . now ( ) ;
0 commit comments