@@ -358,6 +358,39 @@ describe("backupVerifyCommand", () => {
358358 }
359359 } ) ;
360360
361+ it ( "accepts root-relative internal hardlink targets from older backups" , async ( ) => {
362+ const tempDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-backup-rootless-linkpath-" ) ) ;
363+ const archivePath = path . join ( tempDir , "backup.tar.gz" ) ;
364+ const rootRelativeTargetPath = "payload/posix/tmp/.openclaw/target.txt" ;
365+ const payloadArchivePath = `${ TEST_ARCHIVE_ROOT } /${ rootRelativeTargetPath } ` ;
366+ const hardlinkArchivePath = `${ TEST_ARCHIVE_ROOT } /payload/posix/tmp/.openclaw/hardlink.txt` ;
367+ try {
368+ const archive = gzipSync (
369+ Buffer . concat ( [
370+ encodeTarEntry ( {
371+ path : `${ TEST_ARCHIVE_ROOT } /manifest.json` ,
372+ contents : `${ JSON . stringify ( createBackupManifest ( payloadArchivePath ) , null , 2 ) } \n` ,
373+ } ) ,
374+ encodeTarEntry ( { path : payloadArchivePath , contents : "payload\n" } ) ,
375+ encodeTarEntry ( {
376+ path : hardlinkArchivePath ,
377+ type : "Link" ,
378+ linkpath : rootRelativeTargetPath ,
379+ } ) ,
380+ Buffer . alloc ( 1024 ) ,
381+ ] ) ,
382+ ) ;
383+ await fs . writeFile ( archivePath , archive ) ;
384+
385+ const runtime = createBackupVerifyRuntime ( ) ;
386+ await expect ( backupVerifyCommand ( runtime , { archive : archivePath } ) ) . resolves . toMatchObject ( {
387+ ok : true ,
388+ } ) ;
389+ } finally {
390+ await fs . rm ( tempDir , { recursive : true , force : true } ) ;
391+ }
392+ } ) ;
393+
361394 it ( "rejects hardlink targets missing from archive entries" , async ( ) => {
362395 const tempDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-backup-missing-linkpath-" ) ) ;
363396 const archivePath = path . join ( tempDir , "broken.tar.gz" ) ;
0 commit comments