@@ -3016,6 +3016,67 @@ 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+
30193080 it ( "falls back to pasted callback input when browser launch is suppressed" , async ( ) => {
30203081 setInteractiveTTY ( true ) ;
30213082 const now = Date . now ( ) ;
0 commit comments