@@ -2198,7 +2198,8 @@ describe("OpenAIOAuthPlugin", () => {
21982198 mockStorage . activeIndexByFamily = { codex : 1 } ;
21992199 mockFlaggedStorage . accounts = [
22002200 {
2201- accountId : "remove-me" ,
2201+ accountId : "flagged-remove-me" ,
2202+ organizationId : "flagged-org" ,
22022203 email : "remove@example.com" ,
22032204 refreshToken : "refresh-remove" ,
22042205 accessToken : "flagged-access-remove" ,
@@ -2277,6 +2278,7 @@ describe("OpenAIOAuthPlugin", () => {
22772278 expect ( backupContent ) . toContain ( "\"idToken\": \"id-remove\"" ) ;
22782279 expect ( backupContent ) . toContain ( "\"accessToken\": \"flagged-access-remove\"" ) ;
22792280 expect ( backupContent ) . toContain ( "\"idToken\": \"flagged-id-remove\"" ) ;
2281+ expect ( mockFlaggedStorage . accounts ) . toHaveLength ( 0 ) ;
22802282 expect ( renameSpy ) . toHaveBeenCalled ( ) ;
22812283 expect ( mkdirSpy ) . toHaveBeenCalled ( ) ;
22822284 } finally {
@@ -2506,6 +2508,176 @@ describe("OpenAIOAuthPlugin", () => {
25062508 expect ( vi . mocked ( storageModule . withAccountAndFlaggedStorageTransaction ) ) . toHaveBeenCalledTimes ( 2 ) ;
25072509 expect ( vi . mocked ( syncModule . syncFromCodexMultiAuth ) ) . toHaveBeenCalledTimes ( 1 ) ;
25082510 } ) ;
2511+
2512+ it ( "rolls back account restore when flagged restore fails" , async ( ) => {
2513+ const cliModule = await import ( "../lib/cli.js" ) ;
2514+ const confirmModule = await import ( "../lib/ui/confirm.js" ) ;
2515+ const configModule = await import ( "../lib/config.js" ) ;
2516+ const loggerModule = await import ( "../lib/logger.js" ) ;
2517+ const storageModule = await import ( "../lib/storage.js" ) ;
2518+ const syncModule = await import ( "../lib/codex-multi-auth-sync.js" ) ;
2519+
2520+ mockStorage . accounts = [
2521+ {
2522+ accountId : "remove-me" ,
2523+ email : "remove@example.com" ,
2524+ refreshToken : "refresh-remove" ,
2525+ accessToken : "access-remove" ,
2526+ idToken : "id-remove" ,
2527+ addedAt : 1 ,
2528+ lastUsed : 1 ,
2529+ } ,
2530+ {
2531+ accountId : "keep-me" ,
2532+ email : "keep@example.com" ,
2533+ refreshToken : "refresh-keep" ,
2534+ accessToken : "access-keep" ,
2535+ idToken : "id-keep" ,
2536+ addedAt : 2 ,
2537+ lastUsed : 2 ,
2538+ } ,
2539+ ] ;
2540+ mockStorage . activeIndex = 1 ;
2541+ mockStorage . activeIndexByFamily = { codex : 1 } ;
2542+ mockFlaggedStorage . accounts = [
2543+ {
2544+ accountId : "flagged-remove-me" ,
2545+ organizationId : "flagged-org" ,
2546+ email : "remove@example.com" ,
2547+ refreshToken : "refresh-remove" ,
2548+ accessToken : "flagged-access-remove" ,
2549+ idToken : "flagged-id-remove" ,
2550+ flaggedAt : 123 ,
2551+ addedAt : 1 ,
2552+ lastUsed : 1 ,
2553+ } ,
2554+ ] ;
2555+
2556+ const { CodexMultiAuthSyncCapacityError } = syncModule ;
2557+ vi . mocked ( configModule . getSyncFromCodexMultiAuthEnabled ) . mockReturnValue ( true ) ;
2558+ vi . mocked ( cliModule . promptLoginMode )
2559+ . mockResolvedValueOnce ( { mode : "experimental-sync-now" } )
2560+ . mockResolvedValueOnce ( { mode : "cancel" } )
2561+ . mockResolvedValue ( { mode : "cancel" } ) ;
2562+ vi . mocked ( cliModule . promptCodexMultiAuthSyncPrune ) . mockResolvedValueOnce ( [ 0 ] ) ;
2563+ vi . mocked ( confirmModule . confirm ) . mockResolvedValue ( true ) ;
2564+ vi . mocked ( syncModule . previewSyncFromCodexMultiAuth )
2565+ . mockRejectedValueOnce (
2566+ new CodexMultiAuthSyncCapacityError ( {
2567+ rootDir : "/tmp/codex-source" ,
2568+ accountsPath : "/tmp/codex-source/openai-codex-accounts.json" ,
2569+ scope : "global" ,
2570+ currentCount : 2 ,
2571+ sourceCount : 1 ,
2572+ sourceDedupedTotal : 1 ,
2573+ dedupedTotal : 3 ,
2574+ maxAccounts : 2 ,
2575+ needToRemove : 1 ,
2576+ importableNewAccounts : 1 ,
2577+ skippedOverlaps : 0 ,
2578+ suggestedRemovals : [
2579+ {
2580+ index : 0 ,
2581+ email : "remove@example.com" ,
2582+ accountLabel : "Remove Me" ,
2583+ refreshToken : "refresh-remove" ,
2584+ organizationId : undefined ,
2585+ accountId : "remove-me" ,
2586+ isCurrentAccount : false ,
2587+ score : 100 ,
2588+ reason : "test removal" ,
2589+ } ,
2590+ ] ,
2591+ } ) ,
2592+ )
2593+ . mockResolvedValueOnce ( {
2594+ rootDir : "/tmp/codex-source" ,
2595+ accountsPath : "/tmp/codex-source/openai-codex-accounts.json" ,
2596+ scope : "global" ,
2597+ imported : 1 ,
2598+ skipped : 0 ,
2599+ total : 2 ,
2600+ } ) ;
2601+ vi . mocked ( syncModule . syncFromCodexMultiAuth ) . mockRejectedValueOnce (
2602+ new Error ( "sync failed after prune" ) ,
2603+ ) ;
2604+
2605+ const prunedAccounts = {
2606+ version : 3 as const ,
2607+ accounts : [
2608+ {
2609+ accountId : "keep-me" ,
2610+ email : "keep@example.com" ,
2611+ refreshToken : "refresh-keep" ,
2612+ accessToken : "access-keep" ,
2613+ idToken : "id-keep" ,
2614+ addedAt : 2 ,
2615+ lastUsed : 2 ,
2616+ } ,
2617+ ] ,
2618+ activeIndex : 0 ,
2619+ activeIndexByFamily : { codex : 0 } ,
2620+ } ;
2621+ const prunedFlagged = {
2622+ version : 1 as const ,
2623+ accounts : [ ] as Array < ( typeof mockFlaggedStorage . accounts ) [ number ] > ,
2624+ } ;
2625+ let transactionCalls = 0 ;
2626+ vi . mocked ( storageModule . withAccountAndFlaggedStorageTransaction ) . mockImplementation (
2627+ async ( callback ) => {
2628+ transactionCalls += 1 ;
2629+ if ( transactionCalls === 2 ) {
2630+ return await callback (
2631+ {
2632+ accounts : cloneMockStorage ( ) ,
2633+ flagged : cloneMockFlaggedStorage ( ) ,
2634+ } ,
2635+ {
2636+ accounts : async ( nextStorage : typeof mockStorage ) => {
2637+ mockStorage . version = nextStorage . version ;
2638+ mockStorage . accounts = nextStorage . accounts . map ( cloneAccount ) ;
2639+ mockStorage . activeIndex = nextStorage . activeIndex ;
2640+ mockStorage . activeIndexByFamily = { ...nextStorage . activeIndexByFamily } ;
2641+ } ,
2642+ flagged : async ( _nextStorage : typeof mockFlaggedStorage ) => {
2643+ throw new Error ( "flagged restore failed" ) ;
2644+ } ,
2645+ } ,
2646+ ) ;
2647+ }
2648+ return await callback (
2649+ {
2650+ accounts : cloneMockStorage ( ) ,
2651+ flagged : cloneMockFlaggedStorage ( ) ,
2652+ } ,
2653+ {
2654+ accounts : async ( nextStorage : typeof mockStorage ) => {
2655+ mockStorage . version = nextStorage . version ;
2656+ mockStorage . accounts = nextStorage . accounts . map ( cloneAccount ) ;
2657+ mockStorage . activeIndex = nextStorage . activeIndex ;
2658+ mockStorage . activeIndexByFamily = { ...nextStorage . activeIndexByFamily } ;
2659+ } ,
2660+ flagged : async ( nextStorage : typeof mockFlaggedStorage ) => {
2661+ mockFlaggedStorage . version = nextStorage . version ;
2662+ mockFlaggedStorage . accounts = nextStorage . accounts . map ( cloneFlaggedAccount ) ;
2663+ } ,
2664+ } ,
2665+ ) ;
2666+ } ,
2667+ ) ;
2668+
2669+ const autoMethod = plugin . auth . methods [ 0 ] as unknown as {
2670+ authorize : ( inputs ?: Record < string , string > ) => Promise < { instructions : string } > ;
2671+ } ;
2672+
2673+ const result = await autoMethod . authorize ( ) ;
2674+ expect ( result . instructions ) . toBe ( "Authentication cancelled" ) ;
2675+ expect ( mockStorage ) . toMatchObject ( prunedAccounts ) ;
2676+ expect ( mockFlaggedStorage ) . toEqual ( prunedFlagged ) ;
2677+ expect ( vi . mocked ( loggerModule . logWarn ) ) . toHaveBeenCalledWith (
2678+ expect . stringContaining ( "Failed to restore sync prune backup: flagged restore failed" ) ,
2679+ ) ;
2680+ } ) ;
25092681 } ) ;
25102682} ) ;
25112683
0 commit comments