@@ -311,7 +311,10 @@ describe("cleanupStaleTempArchives", () => {
311311 await fs . writeFile ( finalArchive , "final archive" , "utf8" ) ;
312312
313313 const log = vi . fn ( ) ;
314- await cleanupStaleTempArchives ( { outputPath, log } ) ;
314+ // Use a future nowMs so all newly-written files appear older than the
315+ // default 30 s staleness threshold.
316+ const futureNow = Date . now ( ) + 60_000 ;
317+ await cleanupStaleTempArchives ( { outputPath, log, nowMs : futureNow } ) ;
315318
316319 // Stale files removed
317320 await expect ( fs . access ( stale1 ) ) . rejects . toMatchObject ( { code : "ENOENT" } ) ;
@@ -338,7 +341,7 @@ describe("cleanupStaleTempArchives", () => {
338341 await fs . writeFile ( outputPath , "final archive" , "utf8" ) ;
339342
340343 const log = vi . fn ( ) ;
341- await cleanupStaleTempArchives ( { outputPath, log } ) ;
344+ await cleanupStaleTempArchives ( { outputPath, log, nowMs : Date . now ( ) + 60_000 } ) ;
342345
343346 // Final archive preserved
344347 await expect ( fs . access ( outputPath ) ) . resolves . toBeUndefined ( ) ;
@@ -375,7 +378,8 @@ describe("cleanupStaleTempArchives", () => {
375378 await fs . writeFile ( myStale , "my stale" , "utf8" ) ;
376379
377380 const log = vi . fn ( ) ;
378- await cleanupStaleTempArchives ( { outputPath, log } ) ;
381+ const futureNow = Date . now ( ) + 60_000 ;
382+ await cleanupStaleTempArchives ( { outputPath, log, nowMs : futureNow } ) ;
379383
380384 // Only our .tmp removed
381385 await expect ( fs . access ( myStale ) ) . rejects . toMatchObject ( { code : "ENOENT" } ) ;
@@ -398,14 +402,80 @@ describe("cleanupStaleTempArchives", () => {
398402 await fs . writeFile ( stale1 , largeContent ) ;
399403
400404 const log = vi . fn ( ) ;
401- await cleanupStaleTempArchives ( { outputPath, log } ) ;
405+ const futureNow = Date . now ( ) + 60_000 ;
406+ await cleanupStaleTempArchives ( { outputPath, log, nowMs : futureNow } ) ;
402407
403408 expect ( log ) . toHaveBeenCalledTimes ( 1 ) ;
404409 expect ( log . mock . calls [ 0 ] [ 0 ] ) . toContain ( "(2.0MB)" ) ;
405410 } finally {
406411 await fs . rm ( dir , { recursive : true , force : true } ) ;
407412 }
408413 } ) ;
414+
415+ it ( "skips temp archives whose mtime is within the staleness threshold" , async ( ) => {
416+ const dir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-backup-cleanup-" ) ) ;
417+ try {
418+ const outputPath = path . join ( dir , "backup.tar.gz" ) ;
419+ // This file was just created — it simulates an active backup writer
420+ const activeTmp = `${ outputPath } .a1b2c3d4-e5f6-7890-abcd-ef1234567890.tmp` ;
421+ // This file is much older — it simulates a stale leftover
422+ const staleTmp = `${ outputPath } .12345678-90ab-cdef-1234-567890abcdef.tmp` ;
423+
424+ await fs . writeFile ( activeTmp , "active backup" , "utf8" ) ;
425+ await fs . writeFile ( staleTmp , "stale leftover" , "utf8" ) ;
426+
427+ // Set the stale file's mtime far in the past
428+ const staleMtime = new Date ( Date . now ( ) - 120_000 ) ; // 2 minutes ago
429+ await fs . utimes ( staleTmp , staleMtime , staleMtime ) ;
430+
431+ const log = vi . fn ( ) ;
432+ // Use current time as nowMs — the active file (mtime ~now) should be
433+ // within the default 30 s threshold and skipped; the stale file
434+ // (mtime 2 min ago) should be removed.
435+ await cleanupStaleTempArchives ( { outputPath, log } ) ;
436+
437+ // Active file preserved
438+ await expect ( fs . access ( activeTmp ) ) . resolves . toBeUndefined ( ) ;
439+ // Stale file removed
440+ await expect ( fs . access ( staleTmp ) ) . rejects . toMatchObject ( { code : "ENOENT" } ) ;
441+
442+ expect ( log ) . toHaveBeenCalledTimes ( 1 ) ;
443+ expect ( log . mock . calls [ 0 ] [ 0 ] ) . toContain ( "Backup cleaned up 1 stale temp archive" ) ;
444+ expect ( log . mock . calls [ 0 ] [ 0 ] ) . toContain ( "1 active temp archive skipped" ) ;
445+ } finally {
446+ await fs . rm ( dir , { recursive : true , force : true } ) ;
447+ }
448+ } ) ;
449+
450+ it ( "removes all matching temp archives when none are recent" , async ( ) => {
451+ const dir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-backup-cleanup-" ) ) ;
452+ try {
453+ const outputPath = path . join ( dir , "backup.tar.gz" ) ;
454+ const tmp1 = `${ outputPath } .a1b2c3d4-e5f6-7890-abcd-ef1234567890.tmp` ;
455+ const tmp2 = `${ outputPath } .12345678-90ab-cdef-1234-567890abcdef.tmp` ;
456+
457+ await fs . writeFile ( tmp1 , "stale 1" , "utf8" ) ;
458+ await fs . writeFile ( tmp2 , "stale 2" , "utf8" ) ;
459+
460+ // Make both files very old
461+ const oldTime = new Date ( Date . now ( ) - 300_000 ) ; // 5 minutes ago
462+ await fs . utimes ( tmp1 , oldTime , oldTime ) ;
463+ await fs . utimes ( tmp2 , oldTime , oldTime ) ;
464+
465+ const log = vi . fn ( ) ;
466+ await cleanupStaleTempArchives ( { outputPath, log } ) ;
467+
468+ await expect ( fs . access ( tmp1 ) ) . rejects . toMatchObject ( { code : "ENOENT" } ) ;
469+ await expect ( fs . access ( tmp2 ) ) . rejects . toMatchObject ( { code : "ENOENT" } ) ;
470+
471+ expect ( log ) . toHaveBeenCalledTimes ( 1 ) ;
472+ expect ( log . mock . calls [ 0 ] [ 0 ] ) . toContain ( "Backup cleaned up 2 stale temp archives" ) ;
473+ // No active-skipped note when nothing was skipped
474+ expect ( log . mock . calls [ 0 ] [ 0 ] ) . not . toContain ( "active temp archive" ) ;
475+ } finally {
476+ await fs . rm ( dir , { recursive : true , force : true } ) ;
477+ }
478+ } ) ;
409479} ) ;
410480
411481describe ( "buildExtensionsNodeModulesFilter" , ( ) => {
0 commit comments