@@ -272,7 +272,10 @@ const mockSaveFlaggedAccounts = vi.fn(async (nextStorage: typeof mockFlaggedStor
272272 await persistMockFlaggedStorage ( nextStorage ) ;
273273} ) ;
274274
275- vi . mock ( "../lib/storage.js" , ( ) => ( {
275+ vi . mock ( "../lib/storage.js" , async ( ) => {
276+ const actual = await vi . importActual < typeof import ( "../lib/storage.js" ) > ( "../lib/storage.js" ) ;
277+
278+ return {
276279 getStoragePath : ( ) => "/mock/path/accounts.json" ,
277280 loadAccounts : vi . fn ( async ( ) => cloneMockStorage ( ) ) ,
278281 saveAccounts : vi . fn ( async ( nextStorage : typeof mockStorage ) => {
@@ -311,6 +314,7 @@ vi.mock("../lib/storage.js", () => ({
311314 previewImportAccounts : vi . fn ( async ( ) => ( { imported : 2 , skipped : 1 , total : 5 } ) ) ,
312315 createTimestampedBackupPath : vi . fn ( ( prefix ?: string ) => `/tmp/${ prefix ?? "codex-backup" } -20260101-000000.json` ) ,
313316 loadFlaggedAccounts : vi . fn ( async ( ) => cloneMockFlaggedStorage ( ) ) ,
317+ getWorkspaceIdentityKey : vi . fn ( actual . getWorkspaceIdentityKey ) ,
314318 saveFlaggedAccounts : mockSaveFlaggedAccounts ,
315319 withFlaggedAccountStorageTransaction : vi . fn (
316320 async < T > (
@@ -335,7 +339,8 @@ vi.mock("../lib/storage.js", () => ({
335339 }
336340 } ,
337341 formatStorageErrorHint : ( ) => "Check file permissions" ,
338- } ) ) ;
342+ } ;
343+ } ) ;
339344
340345vi . mock ( "../lib/accounts.js" , ( ) => {
341346 class MockAccountManager {
@@ -438,12 +443,15 @@ vi.mock("../lib/accounts.js", () => {
438443 ) ,
439444 extractAccountEmail : vi . fn ( ( ) => "user@example.com" ) ,
440445 extractAccountId : vi . fn ( ( ) => "account-1" ) ,
441- resolveRequestAccountId : ( _storedId : string | undefined , _source : string | undefined , tokenId : string | undefined ) => tokenId ,
446+ resolveRequestAccountId : vi . fn (
447+ ( _storedId : string | undefined , _source : string | undefined , tokenId : string | undefined ) =>
448+ tokenId ,
449+ ) ,
442450 formatAccountLabel : ( _account : unknown , index : number ) => `Account ${ index + 1 } ` ,
443451 formatCooldown : ( ) => null ,
444452 formatWaitTime : ( ms : number ) => `${ Math . round ( ms / 1000 ) } s` ,
445453 sanitizeEmail : ( email : string ) => email ,
446- shouldUpdateAccountIdFromToken : ( ) => true ,
454+ shouldUpdateAccountIdFromToken : vi . fn ( ( ) => true ) ,
447455 parseRateLimitReason : ( ) => "unknown" ,
448456 lookupCodexCliTokensByEmail : vi . fn ( async ( ) => null ) ,
449457 } ;
@@ -2639,18 +2647,16 @@ describe("OpenAIOAuthPlugin fetch handler", () => {
26392647 expect ( removeAccount ) . toHaveBeenCalledTimes ( 1 ) ;
26402648 expect ( removeAccountsWithSameRefreshToken ) . not . toHaveBeenCalled ( ) ;
26412649 expect ( accounts . map ( ( account ) => account . accountId ) ) . toEqual ( [ "org-live" ] ) ;
2642- expect ( vi . mocked ( storageModule . saveFlaggedAccounts ) ) . toHaveBeenCalledTimes ( 1 ) ;
2643- expect ( vi . mocked ( storageModule . saveFlaggedAccounts ) . mock . calls [ 0 ] ?. [ 0 ] ) . toEqual (
2644- expect . objectContaining ( {
2645- accounts : expect . arrayContaining ( [
2646- expect . objectContaining ( {
2647- accountId : "org-dead" ,
2648- organizationId : "org-dead" ,
2649- flaggedReason : "workspace-deactivated" ,
2650- lastError : "deactivated_workspace" ,
2651- } ) ,
2652- ] ) ,
2653- } ) ,
2650+ expect ( vi . mocked ( storageModule . withFlaggedAccountStorageTransaction ) ) . toHaveBeenCalledTimes ( 1 ) ;
2651+ expect ( mockFlaggedStorage . accounts ) . toEqual (
2652+ expect . arrayContaining ( [
2653+ expect . objectContaining ( {
2654+ accountId : "org-dead" ,
2655+ organizationId : "org-dead" ,
2656+ flaggedReason : "workspace-deactivated" ,
2657+ lastError : "deactivated_workspace" ,
2658+ } ) ,
2659+ ] ) ,
26542660 ) ;
26552661 } ) ;
26562662
@@ -3618,16 +3624,137 @@ describe("OpenAIOAuthPlugin persistAccountPool", () => {
36183624 expect ( mockStorage . accounts . some ( ( account ) => account . accountId === "workspace-dead" ) ) . toBe ( false ) ;
36193625 expect ( mockStorage . accounts [ 0 ] ?. organizationId ) . toBe ( "org-shared" ) ;
36203626 expect ( mockStorage . accounts [ 0 ] ?. refreshToken ) . toBe ( "shared-refresh" ) ;
3621- expect ( vi . mocked ( storageModule . saveFlaggedAccounts ) ) . toHaveBeenCalledWith (
3622- expect . objectContaining ( {
3623- accounts : expect . arrayContaining ( [
3624- expect . objectContaining ( {
3625- accountId : "workspace-dead" ,
3626- organizationId : "org-shared" ,
3627- flaggedReason : "token-invalid" ,
3628- } ) ,
3629- ] ) ,
3630- } ) ,
3627+ expect ( vi . mocked ( storageModule . withFlaggedAccountStorageTransaction ) ) . toHaveBeenCalled ( ) ;
3628+ expect ( mockFlaggedStorage . accounts ) . toEqual (
3629+ expect . arrayContaining ( [
3630+ expect . objectContaining ( {
3631+ accountId : "workspace-dead" ,
3632+ organizationId : "org-shared" ,
3633+ flaggedReason : "token-invalid" ,
3634+ } ) ,
3635+ ] ) ,
3636+ ) ;
3637+ } ) ;
3638+
3639+ it ( "removes only the deactivated org-scoped workspace during quota-check cleanup" , async ( ) => {
3640+ const accountsModule = await import ( "../lib/accounts.js" ) ;
3641+ const cliModule = await import ( "../lib/cli.js" ) ;
3642+ const refreshQueueModule = await import ( "../lib/refresh-queue.js" ) ;
3643+ const fetchHelpersModule = await import ( "../lib/request/fetch-helpers.js" ) ;
3644+ const storageModule = await import ( "../lib/storage.js" ) ;
3645+
3646+ mockStorage . accounts = [
3647+ {
3648+ refreshToken : "shared-refresh" ,
3649+ organizationId : "org-shared" ,
3650+ accountId : "workspace-dead" ,
3651+ accountIdSource : "manual" ,
3652+ email : "dead@example.com" ,
3653+ addedAt : 1 ,
3654+ lastUsed : 1 ,
3655+ } ,
3656+ {
3657+ refreshToken : "shared-refresh" ,
3658+ organizationId : "org-shared" ,
3659+ accountId : "workspace-live" ,
3660+ accountIdSource : "manual" ,
3661+ email : "live@example.com" ,
3662+ addedAt : 2 ,
3663+ lastUsed : 2 ,
3664+ } ,
3665+ ] ;
3666+
3667+ vi . mocked ( cliModule . promptLoginMode )
3668+ . mockResolvedValueOnce ( { mode : "check" } )
3669+ . mockResolvedValueOnce ( { mode : "cancel" } ) ;
3670+ vi . mocked ( accountsModule . shouldUpdateAccountIdFromToken ) . mockImplementation (
3671+ ( source : string | undefined , currentAccountId ?: string ) => {
3672+ if ( ! currentAccountId ) return true ;
3673+ if ( ! source ) return true ;
3674+ return source === "token" || source === "id_token" ;
3675+ } ,
3676+ ) ;
3677+ vi . mocked ( accountsModule . resolveRequestAccountId ) . mockImplementation (
3678+ ( storedId : string | undefined , source : string | undefined , tokenId : string | undefined ) => {
3679+ if ( ! storedId ) return tokenId ;
3680+ if ( ! accountsModule . shouldUpdateAccountIdFromToken ( source , storedId ) ) {
3681+ return storedId ;
3682+ }
3683+ return tokenId ?? storedId ;
3684+ } ,
3685+ ) ;
3686+ vi . mocked ( refreshQueueModule . queuedRefresh )
3687+ . mockResolvedValueOnce ( {
3688+ type : "success" ,
3689+ access : "access-dead" ,
3690+ refresh : "shared-refresh" ,
3691+ expires : Date . now ( ) + 300_000 ,
3692+ } )
3693+ . mockResolvedValueOnce ( {
3694+ type : "success" ,
3695+ access : "access-live" ,
3696+ refresh : "shared-refresh" ,
3697+ expires : Date . now ( ) + 300_000 ,
3698+ } ) ;
3699+ vi . mocked ( fetchHelpersModule . createCodexHeaders ) . mockImplementation (
3700+ ( _requestId , _accountId , accessToken ) => {
3701+ const headers = new Headers ( ) ;
3702+ if ( typeof accessToken === "string" && accessToken . length > 0 ) {
3703+ headers . set ( "x-test-access-token" , accessToken ) ;
3704+ }
3705+ return headers ;
3706+ } ,
3707+ ) ;
3708+ globalThis . fetch = vi . fn ( async ( _url , init ) => {
3709+ const headers = new Headers ( init ?. headers ) ;
3710+ const accessToken = headers . get ( "x-test-access-token" ) ;
3711+ if ( accessToken === "access-dead" ) {
3712+ return new Response ( JSON . stringify ( { error : { code : "deactivated_workspace" , message : "workspace dead" } } ) , {
3713+ status : 402 ,
3714+ headers : { "content-type" : "application/json" } ,
3715+ } ) ;
3716+ }
3717+ return new Response ( "" , {
3718+ status : 200 ,
3719+ headers : {
3720+ "x-codex-primary-used-percent" : "20" ,
3721+ "x-codex-primary-window-minutes" : "180" ,
3722+ "x-codex-primary-reset-after-seconds" : "900" ,
3723+ "x-codex-secondary-used-percent" : "10" ,
3724+ "x-codex-secondary-window-minutes" : "10080" ,
3725+ "x-codex-secondary-reset-after-seconds" : "86400" ,
3726+ "x-codex-plan-type" : "plus" ,
3727+ "x-codex-active-limit" : "40" ,
3728+ } ,
3729+ } ) ;
3730+ } ) ;
3731+
3732+ const mockClient = createMockClient ( ) ;
3733+ const { OpenAIOAuthPlugin } = await import ( "../index.js" ) ;
3734+ const plugin = ( await OpenAIOAuthPlugin ( {
3735+ client : mockClient ,
3736+ } as never ) ) as unknown as PluginType ;
3737+ const autoMethod = plugin . auth . methods [ 0 ] as unknown as {
3738+ authorize : ( inputs ?: Record < string , string > ) => Promise < { instructions : string } > ;
3739+ } ;
3740+
3741+ const authResult = await autoMethod . authorize ( ) ;
3742+ expect ( authResult . instructions ) . toBe ( "Authentication cancelled" ) ;
3743+
3744+ expect ( globalThis . fetch ) . toHaveBeenCalledTimes ( 2 ) ;
3745+ expect ( mockStorage . accounts ) . toHaveLength ( 1 ) ;
3746+ expect ( mockStorage . accounts . some ( ( account ) => account . accountId === "workspace-dead" ) ) . toBe ( false ) ;
3747+ expect ( mockStorage . accounts [ 0 ] ?. accountId ) . toBe ( "workspace-live" ) ;
3748+ expect ( vi . mocked ( storageModule . withFlaggedAccountStorageTransaction ) ) . toHaveBeenCalled ( ) ;
3749+ expect ( mockFlaggedStorage . accounts ) . toEqual (
3750+ expect . arrayContaining ( [
3751+ expect . objectContaining ( {
3752+ accountId : "workspace-dead" ,
3753+ organizationId : "org-shared" ,
3754+ flaggedReason : "workspace-deactivated" ,
3755+ lastError : "deactivated_workspace" ,
3756+ } ) ,
3757+ ] ) ,
36313758 ) ;
36323759 } ) ;
36333760} ) ;
0 commit comments