Files
openclaw/extensions/memory-wiki/src/source-page-shared.test.ts
Yuval Dinodia 0ec12df245 fix(memory-wiki): preserve human notes block on source re-ingest (#95614)
* fix(memory-wiki): preserve human notes block on source re-ingest

Re-ingesting an existing source regenerated the page with an empty
wrote inside the human-managed markers. This broke the documented
contract that human note blocks are preserved, and diverged from the
synthesis and chatgpt-import writers that already preserve the block.

When a source page already exists, read it and re-inject its human Notes
block before writing. The block is located by scanning past the fenced
the content, then taking the first human start marker and the last end
marker, so the whole Notes block is preserved verbatim even when the
source content or the note text contains the markers or Markdown
headings. The same preservation is applied to writeImportedSourcePage so
the bridge and unsafe-local source-update writers keep notes too. New
page creation is unchanged.

Adds regressions for plain re-ingest, marker text in source content,
marker text inside the note, a heading inside the note, and an imported
source page update.

* fix(memory-wiki): preserve notes on CRLF source pages
2026-06-23 00:33:45 +00:00

184 lines
6.0 KiB
TypeScript

// Memory Wiki tests cover source page shared plugin behavior.
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { renderMarkdownFence, renderWikiMarkdown } from "./markdown.js";
import { writeImportedSourcePage } from "./source-page-shared.js";
function buildSourcePage(raw: string, updatedAt: string): string {
return renderWikiMarkdown({
frontmatter: {
pageType: "source",
id: "source.imported",
title: "imported",
sourceType: "memory-unsafe-local",
status: "active",
updatedAt,
},
body: [
"# imported",
"",
"## Content",
renderMarkdownFence(raw, "text"),
"",
"## Notes",
"<!-- openclaw:human:start -->",
"<!-- openclaw:human:end -->",
"",
].join("\n"),
});
}
describe("writeImportedSourcePage", () => {
let suiteRoot: string;
beforeEach(async () => {
suiteRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-source-page-"));
});
afterEach(async () => {
vi.useRealTimers();
await fs.rm(suiteRoot, { recursive: true, force: true });
});
it("falls back when the source mtime is outside the Date range", async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-01T12:00:00.000Z"));
const sourcePath = path.join(suiteRoot, "source.txt");
await fs.writeFile(sourcePath, "source body", "utf8");
const state: Parameters<typeof writeImportedSourcePage>[0]["state"] = {
entries: {},
version: 1,
};
const result = await writeImportedSourcePage({
vaultRoot: suiteRoot,
syncKey: "unsafe:source",
sourcePath,
sourceUpdatedAtMs: 8_700_000_000_000_000,
sourceSize: 11,
renderFingerprint: "fingerprint",
pagePath: "pages/source.md",
group: "unsafe-local",
state,
buildRendered: (raw, updatedAt) => `updatedAt: ${updatedAt}\n${raw}`,
});
await expect(fs.readFile(path.join(suiteRoot, "pages/source.md"), "utf8")).resolves.toBe(
"updatedAt: 2026-05-01T12:00:00.000Z\nsource body",
);
expect(result).toEqual({ pagePath: "pages/source.md", changed: true, created: true });
expect(state.entries["unsafe:source"]?.sourceUpdatedAtMs).toBe(8_700_000_000_000_000);
});
it("preserves the human Notes block when an imported source page is updated", async () => {
const sourcePath = path.join(suiteRoot, "imported.txt");
const pagePath = "sources/imported.md";
const state: Parameters<typeof writeImportedSourcePage>[0]["state"] = {
entries: {},
version: 1,
};
await fs.writeFile(sourcePath, "first body", "utf8");
await writeImportedSourcePage({
vaultRoot: suiteRoot,
syncKey: "bridge:imported",
sourcePath,
sourceUpdatedAtMs: Date.UTC(2026, 4, 1),
sourceSize: 10,
renderFingerprint: "fp-1",
pagePath,
group: "bridge",
state,
buildRendered: buildSourcePage,
});
const absPage = path.join(suiteRoot, pagePath);
const userNote = "IMPORTED PAGE NOTE";
const edited = (await fs.readFile(absPage, "utf8")).replace(
"<!-- openclaw:human:start -->\n<!-- openclaw:human:end -->",
`<!-- openclaw:human:start -->\n${userNote}\n<!-- openclaw:human:end -->`,
);
await fs.writeFile(absPage, edited, "utf8");
await fs.writeFile(sourcePath, "second body changed", "utf8");
const result = await writeImportedSourcePage({
vaultRoot: suiteRoot,
syncKey: "bridge:imported",
sourcePath,
sourceUpdatedAtMs: Date.UTC(2026, 4, 2),
sourceSize: 19,
renderFingerprint: "fp-2",
pagePath,
group: "bridge",
state,
buildRendered: buildSourcePage,
});
const after = await fs.readFile(absPage, "utf8");
expect(result.changed).toBe(true);
expect(after).toContain("second body changed");
expect(after).toContain(userNote);
});
it("preserves CRLF human notes without copying marker comments from existing imported content", async () => {
const sourcePath = path.join(suiteRoot, "imported-crlf.txt");
const pagePath = "sources/imported-crlf.md";
const state: Parameters<typeof writeImportedSourcePage>[0]["state"] = {
entries: {},
version: 1,
};
const sourceWithMarkers = [
"first imported body",
"<!-- openclaw:human:start -->",
"OLD IMPORTED SOURCE MARKER PAYLOAD",
"<!-- openclaw:human:end -->",
"",
].join("\n");
await fs.writeFile(sourcePath, sourceWithMarkers, "utf8");
await writeImportedSourcePage({
vaultRoot: suiteRoot,
syncKey: "bridge:imported-crlf",
sourcePath,
sourceUpdatedAtMs: Date.UTC(2026, 4, 1),
sourceSize: sourceWithMarkers.length,
renderFingerprint: "fp-1",
pagePath,
group: "bridge",
state,
buildRendered: buildSourcePage,
});
const absPage = path.join(suiteRoot, pagePath);
const userNote = "CRLF IMPORTED PAGE NOTE";
const edited = (await fs.readFile(absPage, "utf8")).replace(
"<!-- openclaw:human:start -->\n<!-- openclaw:human:end -->",
`<!-- openclaw:human:start -->\n${userNote}\n<!-- openclaw:human:end -->`,
);
await fs.writeFile(absPage, edited.replace(/\n/g, "\r\n"), "utf8");
await fs.writeFile(sourcePath, "second imported body without marker comments", "utf8");
const result = await writeImportedSourcePage({
vaultRoot: suiteRoot,
syncKey: "bridge:imported-crlf",
sourcePath,
sourceUpdatedAtMs: Date.UTC(2026, 4, 2),
sourceSize: 44,
renderFingerprint: "fp-2",
pagePath,
group: "bridge",
state,
buildRendered: buildSourcePage,
});
const after = await fs.readFile(absPage, "utf8");
const notesBlock = after.slice(after.indexOf("## Notes"));
expect(result.changed).toBe(true);
expect(after).toContain("second imported body without marker comments");
expect(notesBlock).toContain(userNote);
expect(notesBlock).not.toContain("OLD IMPORTED SOURCE MARKER PAYLOAD");
});
});