@@ -2167,11 +2167,89 @@ describe("OpenAIOAuthPlugin", () => {
21672167 }
21682168 } ) ;
21692169
2170+ it ( "prunes stale overlap cleanup backups after a successful cleanup" , async ( ) => {
2171+ const cliModule = await import ( "../lib/cli.js" ) ;
2172+ const confirmModule = await import ( "../lib/ui/confirm.js" ) ;
2173+ const syncModule = await import ( "../lib/codex-multi-auth-sync.js" ) ;
2174+ const { promises : nodeFsPromises } = await import ( "node:fs" ) ;
2175+ const logSpy = vi . spyOn ( console , "log" ) . mockImplementation ( ( ) => { } ) ;
2176+
2177+ mockStorage . accounts = [
2178+ {
2179+ accountId : "existing-account" ,
2180+ email : "existing@example.com" ,
2181+ refreshToken : "existing-refresh" ,
2182+ addedAt : 1 ,
2183+ lastUsed : 1 ,
2184+ } ,
2185+ ] ;
2186+
2187+ vi . mocked ( cliModule . promptLoginMode )
2188+ . mockResolvedValueOnce ( { mode : "experimental-cleanup-overlaps" } )
2189+ . mockResolvedValueOnce ( { mode : "cancel" } )
2190+ . mockResolvedValue ( { mode : "cancel" } ) ;
2191+ vi . mocked ( confirmModule . confirm ) . mockResolvedValueOnce ( true ) ;
2192+ vi . mocked ( syncModule . previewCodexMultiAuthSyncedOverlapCleanup ) . mockResolvedValueOnce ( {
2193+ before : 3 ,
2194+ after : 2 ,
2195+ removed : 1 ,
2196+ updated : 0 ,
2197+ } ) ;
2198+ vi . mocked ( syncModule . cleanupCodexMultiAuthSyncedOverlaps ) . mockResolvedValueOnce ( {
2199+ before : 3 ,
2200+ after : 2 ,
2201+ removed : 1 ,
2202+ updated : 0 ,
2203+ } ) ;
2204+
2205+ const staleBackupPath = "/tmp/codex-maintenance-overlap-backup-20240201-000000.json" ;
2206+ const normalizePath = ( value : string ) => value . replace ( / \\ / g, "/" ) ;
2207+ const readdirSpy = vi . spyOn ( nodeFsPromises , "readdir" ) . mockResolvedValue ( [
2208+ {
2209+ name : "codex-maintenance-overlap-backup-20260101-000000.json" ,
2210+ isFile : ( ) => true ,
2211+ } ,
2212+ {
2213+ name : "codex-maintenance-overlap-backup-20240201-000000.json" ,
2214+ isFile : ( ) => true ,
2215+ } ,
2216+ ] as never ) ;
2217+ const statSpy = vi . spyOn ( nodeFsPromises , "stat" ) . mockImplementation ( async ( path ) => {
2218+ return {
2219+ mtimeMs :
2220+ normalizePath ( String ( path ) ) === staleBackupPath ? Date . now ( ) - 8 * 24 * 60 * 60 * 1000 : Date . now ( ) ,
2221+ } as never ;
2222+ } ) ;
2223+ const unlinkSpy = vi . spyOn ( nodeFsPromises , "unlink" ) . mockResolvedValue ( undefined ) ;
2224+
2225+ try {
2226+ const autoMethod = plugin . auth . methods [ 0 ] as unknown as {
2227+ authorize : ( inputs ?: Record < string , string > ) => Promise < { instructions : string } > ;
2228+ } ;
2229+
2230+ const result = await autoMethod . authorize ( ) ;
2231+ expect ( result . instructions ) . toBe ( "Authentication cancelled" ) ;
2232+ expect ( vi . mocked ( syncModule . cleanupCodexMultiAuthSyncedOverlaps ) ) . toHaveBeenCalledWith (
2233+ "/tmp/codex-maintenance-overlap-backup-20260101-000000.json" ,
2234+ ) ;
2235+ expect ( unlinkSpy . mock . calls . map ( ( [ path ] ) => normalizePath ( String ( path ) ) ) ) . toEqual ( [ staleBackupPath ] ) ;
2236+
2237+ const output = logSpy . mock . calls . flat ( ) . join ( "\n" ) ;
2238+ expect ( output ) . toContain ( "Cleanup complete." ) ;
2239+ } finally {
2240+ logSpy . mockRestore ( ) ;
2241+ readdirSpy . mockRestore ( ) ;
2242+ statSpy . mockRestore ( ) ;
2243+ unlinkSpy . mockRestore ( ) ;
2244+ }
2245+ } ) ;
2246+
21702247 it ( "writes a restorable sync prune backup before removing accounts" , async ( ) => {
21712248 const cliModule = await import ( "../lib/cli.js" ) ;
21722249 const confirmModule = await import ( "../lib/ui/confirm.js" ) ;
21732250 const configModule = await import ( "../lib/config.js" ) ;
21742251 const syncModule = await import ( "../lib/codex-multi-auth-sync.js" ) ;
2252+ const storageModule = await import ( "../lib/storage.js" ) ;
21752253 const { promises : nodeFsPromises } = await import ( "node:fs" ) ;
21762254
21772255 mockStorage . accounts = [
@@ -2218,6 +2296,9 @@ describe("OpenAIOAuthPlugin", () => {
22182296 . mockResolvedValue ( { mode : "cancel" } ) ;
22192297 vi . mocked ( cliModule . promptCodexMultiAuthSyncPrune ) . mockResolvedValueOnce ( [ 0 ] ) ;
22202298 vi . mocked ( confirmModule . confirm ) . mockResolvedValue ( true ) ;
2299+ vi . mocked ( storageModule . createTimestampedBackupPath ) . mockImplementation (
2300+ ( prefix ?: string ) => `\\tmp\\${ prefix ?? "codex-backup" } -20260101-000000.json` ,
2301+ ) ;
22212302 vi . mocked ( syncModule . previewSyncFromCodexMultiAuth )
22222303 . mockRejectedValueOnce (
22232304 new CodexMultiAuthSyncCapacityError ( {
@@ -2262,6 +2343,53 @@ describe("OpenAIOAuthPlugin", () => {
22622343 . spyOn ( nodeFsPromises , "rename" )
22632344 . mockRejectedValueOnce ( Object . assign ( new Error ( "rename locked" ) , { code : "EPERM" } ) )
22642345 . mockResolvedValueOnce ( undefined ) ;
2346+ const normalizePath = ( value : string ) => value . replace ( / \\ / g, "/" ) ;
2347+ const now = Date . now ( ) ;
2348+ const statTimes = new Map < string , number > ( [
2349+ [ "/tmp/codex-sync-prune-backup-z.json" , now - 3_000 ] ,
2350+ [ "/tmp/codex-sync-prune-backup-y.json" , now - 2_000 ] ,
2351+ [ "/tmp/codex-sync-prune-backup-a.json" , now - 1_000 ] ,
2352+ ] ) ;
2353+ const readdirSpy = vi
2354+ . spyOn ( nodeFsPromises , "readdir" )
2355+ . mockResolvedValueOnce ( [
2356+ {
2357+ name : "codex-sync-prune-backup-20260101-000000.json" ,
2358+ isFile : ( ) => true ,
2359+ } ,
2360+ {
2361+ name : "codex-sync-prune-backup-z.json" ,
2362+ isFile : ( ) => true ,
2363+ } ,
2364+ {
2365+ name : "codex-sync-prune-backup-y.json" ,
2366+ isFile : ( ) => true ,
2367+ } ,
2368+ {
2369+ name : "codex-sync-prune-backup-a.json" ,
2370+ isFile : ( ) => true ,
2371+ } ,
2372+ ] as never )
2373+ . mockResolvedValueOnce ( [
2374+ {
2375+ name : "codex-sync-prune-backup-z.json" ,
2376+ isFile : ( ) => true ,
2377+ } ,
2378+ {
2379+ name : "codex-sync-prune-backup-y.json" ,
2380+ isFile : ( ) => true ,
2381+ } ,
2382+ {
2383+ name : "codex-sync-prune-backup-a.json" ,
2384+ isFile : ( ) => true ,
2385+ } ,
2386+ ] as never ) ;
2387+ const statSpy = vi . spyOn ( nodeFsPromises , "stat" ) . mockImplementation ( async ( path ) => {
2388+ return {
2389+ mtimeMs : statTimes . get ( normalizePath ( String ( path ) ) ) ?? Date . now ( ) ,
2390+ } as never ;
2391+ } ) ;
2392+ const unlinkSpy = vi . spyOn ( nodeFsPromises , "unlink" ) . mockResolvedValue ( undefined ) ;
22652393
22662394 try {
22672395 const autoMethod = plugin . auth . methods [ 0 ] as unknown as {
@@ -2285,10 +2413,18 @@ describe("OpenAIOAuthPlugin", () => {
22852413 expect ( mockFlaggedStorage . accounts ) . toHaveLength ( 0 ) ;
22862414 expect ( renameSpy ) . toHaveBeenCalledTimes ( 2 ) ;
22872415 expect ( mkdirSpy ) . toHaveBeenCalled ( ) ;
2416+ const normalizedUnlinks = unlinkSpy . mock . calls . map ( ( [ path ] ) => normalizePath ( String ( path ) ) ) ;
2417+ expect ( normalizedUnlinks ) . toContain ( "/tmp/codex-sync-prune-backup-20260101-000000.json" ) ;
2418+ expect ( normalizedUnlinks . filter ( ( path ) => path . endsWith ( "codex-sync-prune-backup-z.json" ) ) ) . toHaveLength ( 2 ) ;
2419+ expect ( normalizedUnlinks . some ( ( path ) => path . endsWith ( "codex-sync-prune-backup-a.json" ) ) ) . toBe ( false ) ;
2420+ expect ( normalizedUnlinks . some ( ( path ) => path . endsWith ( "codex-sync-prune-backup-y.json" ) ) ) . toBe ( false ) ;
22882421 } finally {
22892422 mkdirSpy . mockRestore ( ) ;
22902423 writeSpy . mockRestore ( ) ;
22912424 renameSpy . mockRestore ( ) ;
2425+ readdirSpy . mockRestore ( ) ;
2426+ statSpy . mockRestore ( ) ;
2427+ unlinkSpy . mockRestore ( ) ;
22922428 }
22932429 } ) ;
22942430
0 commit comments