@@ -26,6 +26,7 @@ const promptQuestionMock = vi.fn();
2626const detectOcChatgptMultiAuthTargetMock = vi . fn ( ) ;
2727const normalizeAccountStorageMock = vi . fn ( ( value ) => value ) ;
2828const withAccountStorageTransactionMock = vi . fn ( ) ;
29+ const withAccountAndFlaggedStorageTransactionMock = vi . fn ( ) ;
2930
3031vi . mock ( "../lib/logger.js" , ( ) => ( {
3132 createLogger : vi . fn ( ( ) => ( {
@@ -94,6 +95,8 @@ vi.mock("../lib/storage.js", async () => {
9495 loadFlaggedAccounts : loadFlaggedAccountsMock ,
9596 saveAccounts : saveAccountsMock ,
9697 saveFlaggedAccounts : saveFlaggedAccountsMock ,
98+ withAccountAndFlaggedStorageTransaction :
99+ withAccountAndFlaggedStorageTransactionMock ,
97100 withAccountStorageTransaction : withAccountStorageTransactionMock ,
98101 setStoragePath : setStoragePathMock ,
99102 getStoragePath : getStoragePathMock ,
@@ -412,6 +415,7 @@ describe("codex manager cli commands", () => {
412415 loadFlaggedAccountsMock . mockReset ( ) ;
413416 saveAccountsMock . mockReset ( ) ;
414417 saveFlaggedAccountsMock . mockReset ( ) ;
418+ withAccountAndFlaggedStorageTransactionMock . mockReset ( ) ;
415419 withAccountStorageTransactionMock . mockReset ( ) ;
416420 queuedRefreshMock . mockReset ( ) ;
417421 setCodexCliActiveSelectionMock . mockReset ( ) ;
@@ -455,6 +459,34 @@ describe("codex manager cli commands", () => {
455459 ) ;
456460 } ,
457461 ) ;
462+ withAccountAndFlaggedStorageTransactionMock . mockImplementation (
463+ async ( handler ) => {
464+ const current = await loadAccountsMock ( ) ;
465+ let snapshot =
466+ current == null
467+ ? {
468+ version : 3 ,
469+ accounts : [ ] ,
470+ activeIndex : 0 ,
471+ activeIndexByFamily : { } ,
472+ }
473+ : structuredClone ( current ) ;
474+ return handler (
475+ structuredClone ( snapshot ) ,
476+ async ( storage : unknown , flaggedStorage : unknown ) => {
477+ const previousSnapshot = structuredClone ( snapshot ) ;
478+ await saveAccountsMock ( storage ) ;
479+ try {
480+ await saveFlaggedAccountsMock ( flaggedStorage ) ;
481+ snapshot = structuredClone ( storage ) ;
482+ } catch ( error ) {
483+ await saveAccountsMock ( previousSnapshot ) ;
484+ throw error ;
485+ }
486+ } ,
487+ ) ;
488+ } ,
489+ ) ;
458490 loadDashboardDisplaySettingsMock . mockResolvedValue ( {
459491 showPerAccountRows : true ,
460492 showQuotaDetails : true ,
@@ -642,7 +674,7 @@ describe("codex manager cli commands", () => {
642674 ] ) ;
643675
644676 expect ( exitCode ) . toBe ( 0 ) ;
645- expect ( withAccountStorageTransactionMock ) . toHaveBeenCalledTimes ( 1 ) ;
677+ expect ( withAccountAndFlaggedStorageTransactionMock ) . toHaveBeenCalledTimes ( 1 ) ;
646678 expect ( saveAccountsMock ) . toHaveBeenCalledWith (
647679 expect . objectContaining ( {
648680 accounts : expect . arrayContaining ( [
@@ -699,7 +731,7 @@ describe("codex manager cli commands", () => {
699731 ] ) ;
700732
701733 expect ( exitCode ) . toBe ( 0 ) ;
702- expect ( withAccountStorageTransactionMock ) . toHaveBeenCalledTimes ( 1 ) ;
734+ expect ( withAccountAndFlaggedStorageTransactionMock ) . toHaveBeenCalledTimes ( 1 ) ;
703735 const savedStorage = saveAccountsMock . mock . calls . at ( - 1 ) ?. [ 0 ] ;
704736 expect ( savedStorage ) . toEqual (
705737 expect . objectContaining ( {
@@ -715,6 +747,70 @@ describe("codex manager cli commands", () => {
715747 extractAccountIdMock . mockImplementation ( ( ) => "acc_test" ) ;
716748 } ) ;
717749
750+ it ( "rolls back active storage when flagged persistence fails during recovery" , async ( ) => {
751+ const now = Date . now ( ) ;
752+ loadFlaggedAccountsMock . mockResolvedValueOnce ( {
753+ version : 1 ,
754+ accounts : [
755+ {
756+ refreshToken : "flagged-refresh" ,
757+ accountId : "acc_flagged" ,
758+ email : "flagged@example.com" ,
759+ addedAt : now - 1_000 ,
760+ lastUsed : now - 1_000 ,
761+ flaggedAt : now - 5_000 ,
762+ } ,
763+ ] ,
764+ } ) ;
765+ loadAccountsMock . mockResolvedValueOnce ( {
766+ version : 3 ,
767+ activeIndex : 0 ,
768+ activeIndexByFamily : { codex : 0 } ,
769+ accounts : [
770+ {
771+ refreshToken : "refresh-existing" ,
772+ accountId : "acc_existing" ,
773+ email : "existing@example.com" ,
774+ addedAt : now - 10_000 ,
775+ lastUsed : now - 10_000 ,
776+ } ,
777+ ] ,
778+ } ) ;
779+ queuedRefreshMock . mockResolvedValueOnce ( {
780+ type : "success" ,
781+ access : "access-restored" ,
782+ refresh : "refresh-restored" ,
783+ expires : now + 3_600_000 ,
784+ } ) ;
785+ saveFlaggedAccountsMock . mockRejectedValueOnce (
786+ new Error ( "flagged write failed" ) ,
787+ ) ;
788+ const { runCodexMultiAuthCli } = await import ( "../lib/codex-manager.js" ) ;
789+
790+ await expect (
791+ runCodexMultiAuthCli ( [ "auth" , "verify-flagged" , "--json" ] ) ,
792+ ) . rejects . toThrow ( "flagged write failed" ) ;
793+ expect ( withAccountAndFlaggedStorageTransactionMock ) . toHaveBeenCalledTimes (
794+ 1 ,
795+ ) ;
796+ expect ( saveAccountsMock ) . toHaveBeenCalledTimes ( 2 ) ;
797+ expect ( saveAccountsMock . mock . calls [ 0 ] ?. [ 0 ] ) . toEqual (
798+ expect . objectContaining ( {
799+ accounts : expect . arrayContaining ( [
800+ expect . objectContaining ( { refreshToken : "refresh-existing" } ) ,
801+ expect . objectContaining ( { refreshToken : "refresh-restored" } ) ,
802+ ] ) ,
803+ } ) ,
804+ ) ;
805+ expect ( saveAccountsMock . mock . calls [ 1 ] ?. [ 0 ] ) . toEqual (
806+ expect . objectContaining ( {
807+ accounts : [
808+ expect . objectContaining ( { refreshToken : "refresh-existing" } ) ,
809+ ] ,
810+ } ) ,
811+ ) ;
812+ } ) ;
813+
718814 it ( "keeps flagged account when verification still fails" , async ( ) => {
719815 const now = Date . now ( ) ;
720816 loadFlaggedAccountsMock . mockResolvedValueOnce ( {
0 commit comments