Skip to content

Commit e552c76

Browse files
steipetecxbAsDev
andauthored
fix(doctor): report atomic archive failures
Co-authored-by: 陈宪彪0668000387 <[email protected]>
1 parent 99b8964 commit e552c76

2 files changed

Lines changed: 79 additions & 4 deletions

File tree

src/commands/doctor-workspace.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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");

src/commands/doctor-workspace.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ export type RootMemoryMigrationResult = {
169169
readLimitExceeded?: boolean;
170170
/** True when the repair was skipped because a file could not be read. */
171171
readError?: boolean;
172+
/** True when the legacy file could not be archived atomically. */
173+
archiveError?: boolean;
172174
};
173175

174176
async function moveLegacyRootMemoryFileToArchive(params: {
@@ -252,10 +254,22 @@ export async function migrateLegacyRootMemoryFile(
252254
} catch (err) {
253255
return skippedForReadFailure(err);
254256
}
255-
const archivedLegacyPath = await moveLegacyRootMemoryFileToArchive({
256-
workspaceDir: detection.workspaceDir,
257-
legacyPath: detection.legacyPath,
258-
});
257+
let archivedLegacyPath: string;
258+
try {
259+
archivedLegacyPath = await moveLegacyRootMemoryFileToArchive({
260+
workspaceDir: detection.workspaceDir,
261+
legacyPath: detection.legacyPath,
262+
});
263+
} catch {
264+
return {
265+
changed: false,
266+
canonicalPath: detection.canonicalPath,
267+
legacyPath: detection.legacyPath,
268+
removedLegacy: false,
269+
mergedLegacy: false,
270+
archiveError: true,
271+
};
272+
}
259273
let canonicalText: string;
260274
let legacyText: string;
261275
try {
@@ -368,6 +382,17 @@ export async function maybeRepairWorkspaceMemoryHealth(params: {
368382
);
369383
return;
370384
}
385+
if (migration.archiveError) {
386+
note(
387+
[
388+
"Workspace memory root repair skipped (legacy memory could not be archived atomically):",
389+
`- canonical: ${migration.canonicalPath}`,
390+
`- legacy: ${migration.legacyPath}`,
391+
].join("\n"),
392+
"Doctor changes",
393+
);
394+
return;
395+
}
371396
if (!migration.changed) {
372397
return;
373398
}

0 commit comments

Comments
 (0)