Files
openclaw/extensions/memory-wiki/src/vault.test.ts
Yuval Dinodia 623a015928 fix(memory-wiki): preserve user edits when rolling back ChatGPT imports (#116517)
* fix(memory-wiki): preserve user edits when rolling back ChatGPT imports

Rollback deleted created pages and overwrote updated pages unconditionally,
destroying content the user added after the import with no recovery copy.
Import runs now record a content hash of each written page after vault
compile, and rollback preserves any page whose current content no longer
matches into the run's recovered directory before deleting or restoring.
Legacy run records without hashes preserve unconditionally.

Fixes #116457

* fix(memory-wiki): move pages aside atomically during rollback

Rollback now renames the live page into the recovery location before
inspecting it, so a concurrent external save cannot land between the
content read and the delete or snapshot restore. Matching pages drop
the moved copy; mismatching pages keep it as the recovery file.

* fix(memory-wiki): make rollback snapshot restore collision-safe

The snapshot restore wrote directly to the page path after the
move-aside, so a page recreated by an editor in that window was
overwritten with no recovery copy. Restore now creates the snapshot
exclusively and on collision moves the recreated page aside and
retries; recovery filenames are uniqued so a second move-aside cannot
clobber an earlier preserved copy.

* fix(memory-wiki): record rollback hashes at write time

* fix(memory-wiki): make ChatGPT rollback retry-safe

* fix(memory-wiki): fence ChatGPT rollback phases

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-07-31 08:01:52 +00:00

85 lines
3.1 KiB
TypeScript

// Memory Wiki tests cover vault plugin behavior.
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { configureMemoryWikiCompiledCacheStore } from "./compiled-cache.js";
import { createMemoryWikiTestHarness } from "./test-helpers.js";
import { activateExistingMemoryWikiVault, initializeMemoryWikiVault } from "./vault.js";
const { configureCompiledCacheStore, createVault } = createMemoryWikiTestHarness();
describe("initializeMemoryWikiVault", () => {
it("creates the wiki layout and seed files", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-",
config: {
vault: {
renderMode: "obsidian",
},
},
});
const result = await initializeMemoryWikiVault(config, {
nowMs: Date.UTC(2026, 3, 5, 12, 0, 0),
});
expect(result.created).toBe(true);
await Promise.all(
["sources", "entities", "concepts", "syntheses", "reports"].map(async (relativeDir) => {
const dirStat = await fs.stat(path.join(rootDir, relativeDir));
expect(dirStat.isDirectory()).toBe(true);
}),
);
await expect(fs.readFile(path.join(rootDir, "AGENTS.md"), "utf8")).resolves.toContain(
"Memory Wiki Agent Guide",
);
await expect(fs.readFile(path.join(rootDir, "WIKI.md"), "utf8")).resolves.toContain(
"Render mode: `obsidian`",
);
await expect(fs.readFile(path.join(rootDir, "WIKI.md"), "utf8")).resolves.toContain(
"snapshots live in OpenClaw plugin state",
);
await expect(fs.access(path.join(rootDir, ".openclaw-wiki", "cache"))).rejects.toThrow(
/ENOENT/,
);
await expect(fs.access(path.join(rootDir, ".openclaw-wiki", "state.json"))).rejects.toThrow(
/ENOENT/,
);
await expect(fs.access(path.join(rootDir, ".openclaw-wiki", "locks"))).rejects.toThrow(
/ENOENT/,
);
});
it("is idempotent when the vault already exists", async () => {
const { config } = await createVault({
prefix: "memory-wiki-",
});
await initializeMemoryWikiVault(config);
const second = await initializeMemoryWikiVault(config);
expect(second.created).toBe(false);
expect(second.createdDirectories).toHaveLength(0);
expect(second.createdFiles).toHaveLength(0);
});
it("reactivates an existing vault without recreating scaffold or rewriting source pages", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-",
initialize: true,
});
const sourcePath = path.join(rootDir, "sources", "preserved.md");
const source = "# Preserved\n";
const removedScaffoldPath = path.join(rootDir, "AGENTS.md");
await fs.writeFile(sourcePath, source, "utf8");
await fs.rm(removedScaffoldPath);
configureMemoryWikiCompiledCacheStore(undefined);
configureCompiledCacheStore();
await activateExistingMemoryWikiVault(config);
await expect(fs.readFile(sourcePath, "utf8")).resolves.toBe(source);
await expect(fs.access(removedScaffoldPath)).rejects.toMatchObject({ code: "ENOENT" });
});
});