refactor(FN-2132): bootstrap canonical memory before existence checks
- Run memory-layer bootstrap before backend exists checks so legacy migrations seed canonical files during upgrades - Preserve first-run MEMORY.md scaffold creation when long-term memory was created from the default migration scaffold - Remove legacy direct file-exists fallback from backend ensure flow and rely on backend canonical paths - Add tests for canonical read precedence in file/qmd backends and migration-preserving project-memory bootstrap
This commit is contained in:
@@ -141,6 +141,18 @@ describe("memory-backend", () => {
|
||||
expect(result.exists).toBe(false);
|
||||
});
|
||||
|
||||
it("reads only from long-term path when both long-term and legacy files exist", async () => {
|
||||
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
|
||||
writeFileSync(legacyMemoryPath(tempDir), "Legacy content", "utf-8");
|
||||
writeFileSync(longTermMemoryPath(tempDir), "New content", "utf-8");
|
||||
|
||||
const backend = new FileMemoryBackend();
|
||||
const result = await backend.read(tempDir);
|
||||
|
||||
expect(result.content).toBe("New content");
|
||||
expect(result.exists).toBe(true);
|
||||
});
|
||||
|
||||
it("should throw MemoryBackendError on read failure", async () => {
|
||||
// Make the long-term memory path a directory so readFile throws EISDIR (not ENOENT)
|
||||
const longTermDir = join(tempDir, ".fusion", "memory", "MEMORY.md");
|
||||
@@ -411,6 +423,19 @@ describe("memory-backend", () => {
|
||||
expect(result.content).toBe("");
|
||||
expect(result.exists).toBe(false);
|
||||
});
|
||||
|
||||
it("reads only from long-term path when both long-term and legacy files exist", async () => {
|
||||
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
|
||||
writeFileSync(legacyMemoryPath(tempDir), "Legacy content", "utf-8");
|
||||
writeFileSync(longTermMemoryPath(tempDir), "New content", "utf-8");
|
||||
|
||||
const backend = new QmdMemoryBackend();
|
||||
const result = await backend.read(tempDir);
|
||||
|
||||
expect(result.content).toBe("New content");
|
||||
expect(result.exists).toBe(true);
|
||||
expect(result.backend).toBe("qmd");
|
||||
});
|
||||
});
|
||||
|
||||
describe("write", () => {
|
||||
|
||||
@@ -176,6 +176,19 @@ describe("project-memory", () => {
|
||||
const content = await readProjectMemory(testDir);
|
||||
expect(content).toBe("");
|
||||
});
|
||||
|
||||
it("reads only from .fusion/memory/MEMORY.md, ignoring legacy path", async () => {
|
||||
await mkdir(join(testDir, ".fusion"), { recursive: true });
|
||||
const legacyContent = "# Legacy Content\n\nOld stuff";
|
||||
await writeFile(join(testDir, LEGACY_MEMORY_FILE_PATH), legacyContent, "utf-8");
|
||||
|
||||
await mkdir(join(testDir, ".fusion", "memory"), { recursive: true });
|
||||
const newContent = "# New Content\n\nNew stuff";
|
||||
await writeFile(memoryPath, newContent, "utf-8");
|
||||
|
||||
const content = await readProjectMemory(testDir);
|
||||
expect(content).toBe(newContent);
|
||||
});
|
||||
});
|
||||
|
||||
// ── buildTriageMemoryInstructions ─────────────────────────────────
|
||||
@@ -285,6 +298,19 @@ describe("project-memory", () => {
|
||||
expect(content).toBe(customContent);
|
||||
});
|
||||
|
||||
it("preserves legacy content during upgrade when only legacy exists", async () => {
|
||||
await mkdir(join(testDir, ".fusion"), { recursive: true });
|
||||
const userContent = "# Legacy\n\nUser content that must be preserved";
|
||||
await writeFile(join(testDir, LEGACY_MEMORY_FILE_PATH), userContent, "utf-8");
|
||||
|
||||
const created = await ensureMemoryFileWithBackend(testDir);
|
||||
|
||||
expect(created).toBe(false);
|
||||
expect(existsSync(memoryPath)).toBe(true);
|
||||
const content = readFileSync(memoryPath, "utf-8");
|
||||
expect(content).toBe(userContent);
|
||||
});
|
||||
|
||||
it("returns false when file already exists", async () => {
|
||||
await ensureMemoryFile(testDir);
|
||||
const created = await ensureMemoryFileWithBackend(testDir);
|
||||
@@ -301,6 +327,7 @@ describe("project-memory", () => {
|
||||
|
||||
expect(created).toBe(true);
|
||||
expect(existsSync(memoryPath)).toBe(true);
|
||||
expect(readFileSync(memoryPath, "utf-8")).toBe(getDefaultMemoryScaffold());
|
||||
});
|
||||
|
||||
it("does not throw for readonly backend (non-fatal bootstrap)", async () => {
|
||||
@@ -452,7 +479,6 @@ describe("project-memory", () => {
|
||||
const created = await ensureMemoryFileWithBackend(testDir, { memoryBackendType: "file" });
|
||||
expect(created).toBe(true);
|
||||
|
||||
// File should exist and have default scaffold content
|
||||
const content = readFileSync(memoryPath, "utf-8");
|
||||
expect(content).toBe(getDefaultMemoryScaffold());
|
||||
});
|
||||
|
||||
@@ -272,17 +272,25 @@ export async function ensureMemoryFileWithBackend(
|
||||
}
|
||||
};
|
||||
|
||||
// Check if memory already exists using the backend
|
||||
// OpenClaw-style memory layers are always bootstrapped for writable memory
|
||||
// backends. `ensureOpenClawMemoryFiles()` handles one-way migration seeding
|
||||
// from the legacy top-level memory file when upgrading older projects.
|
||||
// This runs before existence checks so migrated legacy content is preserved.
|
||||
let createdFromDefaultLongTermScaffold = false;
|
||||
if (backend.capabilities.writable) {
|
||||
const { longTermCreated } = await ensureOpenClawMemoryFiles(rootDir);
|
||||
if (longTermCreated) {
|
||||
const createdContent = await readFile(memoryLongTermPath(rootDir), "utf-8");
|
||||
createdFromDefaultLongTermScaffold =
|
||||
createdContent === getDefaultLongTermMemoryScaffold();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if memory already exists using the backend.
|
||||
// This catches both pre-existing canonical files and newly migrated files.
|
||||
if (backend.exists) {
|
||||
const exists = await backend.exists(rootDir);
|
||||
if (exists) {
|
||||
refreshQmdIfNeeded();
|
||||
return false; // Memory already exists, don't overwrite
|
||||
}
|
||||
} else {
|
||||
// Fall back to direct file check
|
||||
const filePath = memoryFilePath(rootDir);
|
||||
if (existsSync(filePath)) {
|
||||
if (exists && !createdFromDefaultLongTermScaffold) {
|
||||
refreshQmdIfNeeded();
|
||||
return false; // Memory already exists, don't overwrite
|
||||
}
|
||||
@@ -294,13 +302,6 @@ export async function ensureMemoryFileWithBackend(
|
||||
await mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
// OpenClaw-style memory layers are always bootstrapped for writable memory
|
||||
// backends. `ensureOpenClawMemoryFiles()` handles one-way migration seeding
|
||||
// from the legacy top-level memory file when upgrading older projects.
|
||||
if (backend.capabilities.writable) {
|
||||
await ensureOpenClawMemoryFiles(rootDir);
|
||||
}
|
||||
|
||||
// Try to write using the backend
|
||||
try {
|
||||
const result = await backend.write(rootDir, getDefaultMemoryScaffold());
|
||||
|
||||
Reference in New Issue
Block a user