@@ -74,6 +74,29 @@ vi.mock("../lib/auth/server.js", () => ({
7474 } ) ) ,
7575} ) ) ;
7676
77+ vi . mock ( "../lib/auth/device-code.js" , ( ) => ( {
78+ createDeviceCodeSession : vi . fn ( async ( ) => ( {
79+ type : "ready" as const ,
80+ session : {
81+ verificationUrl : "https://auth.openai.com/codex/device" ,
82+ userCode : "ABCD-EFGH" ,
83+ deviceAuthId : "device-auth-1" ,
84+ intervalSeconds : 1 ,
85+ } ,
86+ } ) ) ,
87+ buildDeviceCodeInstructions : vi . fn (
88+ ( session : { verificationUrl : string ; userCode : string } ) =>
89+ `Open this link and sign in: ${ session . verificationUrl } \nEnter this one-time code: ${ session . userCode } \nThis code expires in about 15 minutes.` ,
90+ ) ,
91+ completeDeviceCodeSession : vi . fn ( async ( ) => ( {
92+ type : "success" as const ,
93+ access : "device-access-token" ,
94+ refresh : "device-refresh-token" ,
95+ expires : Date . now ( ) + 3600_000 ,
96+ idToken : "device-id-token" ,
97+ } ) ) ,
98+ } ) ) ;
99+
77100vi . mock ( "../lib/cli.js" , ( ) => ( {
78101 promptLoginMode : vi . fn ( async ( ) => ( { mode : "add" } ) ) ,
79102 promptAddAnotherAccount : vi . fn ( async ( ) => false ) ,
@@ -554,15 +577,16 @@ describe("OpenAIOAuthPlugin", () => {
554577 expect ( plugin . tool [ "codex-import" ] ) . toBeDefined ( ) ;
555578 } ) ;
556579
557- it ( "has two auth methods" , ( ) => {
558- expect ( plugin . auth . methods ) . toHaveLength ( 2 ) ;
580+ it ( "has three auth methods" , ( ) => {
581+ expect ( plugin . auth . methods ) . toHaveLength ( 3 ) ;
559582 expect ( plugin . auth . methods [ 0 ] . label ) . toBe ( "ChatGPT Plus/Pro MULTI (Codex Subscription)" ) ;
560- expect ( plugin . auth . methods [ 1 ] . label ) . toBe ( "ChatGPT Plus/Pro MULTI (Manual URL Paste)" ) ;
583+ expect ( plugin . auth . methods [ 1 ] . label ) . toBe ( "ChatGPT Plus/Pro MULTI (Device Code)" ) ;
584+ expect ( plugin . auth . methods [ 2 ] . label ) . toBe ( "ChatGPT Plus/Pro MULTI (Manual URL Paste)" ) ;
561585 } ) ;
562586
563587 it ( "rejects manual OAuth callbacks with mismatched state" , async ( ) => {
564588 const authModule = await import ( "../lib/auth/auth.js" ) ;
565- const manualMethod = plugin . auth . methods [ 1 ] as unknown as {
589+ const manualMethod = plugin . auth . methods [ 2 ] as unknown as {
566590 authorize : ( ) => Promise < {
567591 validate : ( input : string ) => string | undefined ;
568592 callback : ( input : string ) => Promise < { type : string ; reason ?: string ; message ?: string } > ;
@@ -578,6 +602,56 @@ describe("OpenAIOAuthPlugin", () => {
578602 expect ( result . reason ) . toBe ( "invalid_response" ) ;
579603 expect ( vi . mocked ( authModule . exchangeAuthorizationCode ) ) . not . toHaveBeenCalled ( ) ;
580604 } ) ;
605+
606+ it ( "suggests device code when browser callback server is unavailable" , async ( ) => {
607+ const serverModule = await import ( "../lib/auth/server.js" ) ;
608+ vi . mocked ( serverModule . startLocalOAuthServer ) . mockResolvedValueOnce ( {
609+ ready : false ,
610+ close : vi . fn ( ) ,
611+ waitForCode : vi . fn ( async ( ) => null ) ,
612+ port : 1455 ,
613+ } ) ;
614+
615+ const autoMethod = plugin . auth . methods [ 0 ] as unknown as {
616+ authorize : ( inputs ?: Record < string , string > ) => Promise < {
617+ instructions : string ;
618+ callback : ( ) => Promise < { type : string ; message ?: string } > ;
619+ } > ;
620+ } ;
621+
622+ const authResult = await autoMethod . authorize ( { loginMode : "add" , accountCount : "1" } ) ;
623+ expect ( authResult . instructions ) . toContain ( "Device Code" ) ;
624+ expect ( authResult . instructions ) . toContain ( "Manual URL Paste" ) ;
625+
626+ const result = await authResult . callback ( ) ;
627+ expect ( result . type ) . toBe ( "failed" ) ;
628+ expect ( result . message ) . toContain ( "Device Code" ) ;
629+ } ) ;
630+
631+ it ( "completes device code login and persists the account" , async ( ) => {
632+ const deviceModule = await import ( "../lib/auth/device-code.js" ) ;
633+ const deviceMethod = plugin . auth . methods [ 1 ] as unknown as {
634+ authorize : ( ) => Promise < {
635+ instructions : string ;
636+ callback : ( ) => Promise < { type : string } > ;
637+ } > ;
638+ } ;
639+
640+ const authResult = await deviceMethod . authorize ( ) ;
641+ expect ( authResult . instructions ) . toContain ( "ABCD-EFGH" ) ;
642+ expect ( authResult . instructions ) . toContain ( "https://auth.openai.com/codex/device" ) ;
643+
644+ const result = await authResult . callback ( ) ;
645+ expect ( result . type ) . toBe ( "success" ) ;
646+ expect ( vi . mocked ( deviceModule . createDeviceCodeSession ) ) . toHaveBeenCalledTimes ( 1 ) ;
647+ expect ( vi . mocked ( deviceModule . completeDeviceCodeSession ) ) . toHaveBeenCalledTimes ( 1 ) ;
648+ expect ( mockStorage . accounts ) . toHaveLength ( 1 ) ;
649+ expect ( mockStorage . accounts [ 0 ] ) . toMatchObject ( {
650+ refreshToken : "device-refresh-token" ,
651+ accessToken : "device-access-token" ,
652+ accountId : "acc-1" ,
653+ } ) ;
654+ } ) ;
581655 } ) ;
582656
583657 describe ( "event handler" , ( ) => {
0 commit comments