@@ -311,6 +311,56 @@ describe("root memory repair", () => {
311311 expect ( repairNote ?. [ 1 ] ) . toBe ( "Doctor changes" ) ;
312312 } ) ;
313313
314+ it ( "skips without mutation when legacy memory cannot be archived atomically" , async ( ) => {
315+ const canonicalPath = path . join ( tmpDir , "MEMORY.md" ) ;
316+ const legacyPath = path . join ( tmpDir , "memory.md" ) ;
317+ await fs . writeFile ( canonicalPath , "# Canonical\n" , "utf8" ) ;
318+ await fs . writeFile ( legacyPath , "# Legacy\n" , "utf8" ) ;
319+ const rename = vi
320+ . spyOn ( fs , "rename" )
321+ . mockRejectedValueOnce ( Object . assign ( new Error ( "cross-device rename" ) , { code : "EXDEV" } ) ) ;
322+
323+ try {
324+ const migration = await migrateLegacyRootMemoryFile ( tmpDir ) ;
325+
326+ expect ( migration . changed ) . toBe ( false ) ;
327+ expect ( migration . removedLegacy ) . toBe ( false ) ;
328+ expect ( migration . mergedLegacy ) . toBe ( false ) ;
329+ expect ( migration . archiveError ) . toBe ( true ) ;
330+ await expect ( fs . readFile ( canonicalPath , "utf8" ) ) . resolves . toBe ( "# Canonical\n" ) ;
331+ await expect ( fs . readFile ( legacyPath , "utf8" ) ) . resolves . toBe ( "# Legacy\n" ) ;
332+ } finally {
333+ rename . mockRestore ( ) ;
334+ }
335+ } ) ;
336+
337+ it ( "reports when legacy memory cannot be archived atomically" , async ( ) => {
338+ await fs . writeFile ( path . join ( tmpDir , "MEMORY.md" ) , "# Canonical\n" , "utf8" ) ;
339+ await fs . writeFile ( path . join ( tmpDir , "memory.md" ) , "# Legacy\n" , "utf8" ) ;
340+ const cfg = { agents : { defaults : { workspace : tmpDir } } } as OpenClawConfig ;
341+ const prompter = {
342+ confirmRuntimeRepair : vi . fn ( async ( ) => true ) ,
343+ } as unknown as DoctorPrompter ;
344+ const rename = vi
345+ . spyOn ( fs , "rename" )
346+ . mockRejectedValueOnce ( Object . assign ( new Error ( "cross-device rename" ) , { code : "EXDEV" } ) ) ;
347+
348+ try {
349+ await maybeRepairWorkspaceMemoryHealth ( { cfg, prompter } ) ;
350+ } finally {
351+ rename . mockRestore ( ) ;
352+ }
353+
354+ const repairNote = firstNoteCall ( ) ;
355+ const repairLines = String ( repairNote ?. [ 0 ] ?? "" ) . split ( "\n" ) ;
356+ expect ( repairLines [ 0 ] ) . toBe (
357+ "Workspace memory root repair skipped (legacy memory could not be archived atomically):" ,
358+ ) ;
359+ expect ( repairLines ) . toContain ( `- canonical: ${ path . join ( tmpDir , "MEMORY.md" ) } ` ) ;
360+ expect ( repairLines ) . toContain ( `- legacy: ${ path . join ( tmpDir , "memory.md" ) } ` ) ;
361+ expect ( repairNote ?. [ 1 ] ) . toBe ( "Doctor changes" ) ;
362+ } ) ;
363+
314364 it ( "reports a preserved archive when a failed repair cannot restore legacy" , async ( ) => {
315365 const canonicalPath = path . join ( tmpDir , "MEMORY.md" ) ;
316366 const legacyPath = path . join ( tmpDir , "memory.md" ) ;
0 commit comments