@@ -343,6 +343,39 @@ describe("backupVerifyCommand", () => {
343343 }
344344 } ) ;
345345
346+ it ( "rejects hardlink targets missing from archive entries" , async ( ) => {
347+ const tempDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-backup-missing-linkpath-" ) ) ;
348+ const archivePath = path . join ( tempDir , "broken.tar.gz" ) ;
349+ const payloadArchivePath = `${ TEST_ARCHIVE_ROOT } /payload/posix/tmp/.openclaw/target.txt` ;
350+ const hardlinkArchivePath = `${ TEST_ARCHIVE_ROOT } /payload/posix/tmp/.openclaw/hardlink.txt` ;
351+ const missingTargetPath = `${ TEST_ARCHIVE_ROOT } /payload/posix/tmp/.openclaw/missing-target.txt` ;
352+ try {
353+ const archive = gzipSync (
354+ Buffer . concat ( [
355+ encodeTarEntry ( {
356+ path : `${ TEST_ARCHIVE_ROOT } /manifest.json` ,
357+ contents : `${ JSON . stringify ( createBackupManifest ( payloadArchivePath ) , null , 2 ) } \n` ,
358+ } ) ,
359+ encodeTarEntry ( { path : payloadArchivePath , contents : "payload\n" } ) ,
360+ encodeTarEntry ( {
361+ path : hardlinkArchivePath ,
362+ type : "Link" ,
363+ linkpath : missingTargetPath ,
364+ } ) ,
365+ Buffer . alloc ( 1024 ) ,
366+ ] ) ,
367+ ) ;
368+ await fs . writeFile ( archivePath , archive ) ;
369+
370+ const runtime = createBackupVerifyRuntime ( ) ;
371+ await expect ( backupVerifyCommand ( runtime , { archive : archivePath } ) ) . rejects . toThrow (
372+ / h a r d l i n k t a r g e t i s m i s s i n g f r o m a r c h i v e e n t r i e s / i,
373+ ) ;
374+ } finally {
375+ await fs . rm ( tempDir , { recursive : true , force : true } ) ;
376+ }
377+ } ) ;
378+
346379 it ( "ignores payload manifest.json files when locating the backup manifest" , async ( ) => {
347380 const archiveDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-backup-verify-out-" ) ) ;
348381 try {
0 commit comments