Files
adolf/extensions/memory-wiki/src/lint.test.ts
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

582 lines
19 KiB
TypeScript

// Memory Wiki tests cover lint plugin behavior.
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { lintMemoryWikiVault } from "./lint.js";
import {
renderWikiMarkdown,
WIKI_RAW_SOURCE_MARKER,
WIKI_RELATED_END_MARKER,
WIKI_RELATED_START_MARKER,
} from "./markdown.js";
import { writeMemoryWikiSourceSyncState } from "./source-sync-state.js";
import { createMemoryWikiTestHarness } from "./test-helpers.js";
const { createVault } = createMemoryWikiTestHarness();
function issueCodesForPath(
result: Awaited<ReturnType<typeof lintMemoryWikiVault>>,
pagePath: string,
): string[] {
return result.issues
.filter((issue) => issue.path === pagePath)
.map((issue) => issue.code)
.toSorted();
}
describe("lintMemoryWikiVault", () => {
it("accepts native markdown links that include the relative .md target", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-lint-native-links-",
config: {
vault: { renderMode: "native" },
},
});
await Promise.all(
["entities", "sources"].map((dir) => fs.mkdir(path.join(rootDir, dir), { recursive: true })),
);
await fs.writeFile(
path.join(rootDir, "sources", "alpha.md"),
renderWikiMarkdown({
frontmatter: {
pageType: "source",
id: "source.alpha",
title: "Alpha Source",
},
body: "# Alpha Source\n",
}),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "entities", "alpha.md"),
renderWikiMarkdown({
frontmatter: {
pageType: "entity",
id: "entity.alpha",
title: "Alpha",
sourceIds: ["source.alpha"],
},
body: "# Alpha\n\n[Alpha Source](../sources/alpha.md)\n",
}),
"utf8",
);
const result = await lintMemoryWikiVault(config);
expect(result.issues.map((issue) => issue.code)).not.toContain("broken-wikilink");
});
it("does not report broken-wikilink for [[...]] inside fenced code blocks or inline code", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-lint-fenced-code-",
config: {
vault: { renderMode: "obsidian" },
},
});
await Promise.all(
["entities", "sources"].map((dir) => fs.mkdir(path.join(rootDir, dir), { recursive: true })),
);
await fs.writeFile(
path.join(rootDir, "sources", "code-snippets.md"),
renderWikiMarkdown({
frontmatter: {
pageType: "source",
id: "source.code-snippets",
title: "Code Snippets",
},
body: [
"# Code Snippets",
"",
"Normal text with no wikilinks here.",
"",
"```bash",
'if [[ "$name" == "Alice" ]]; then',
" echo found",
"fi",
"```",
"",
"```scala",
"val result = Future[Option[User]] {",
' collectionName = "users"',
"}",
"```",
"",
'Inline code: `val userId: String` and `[[ "$str" == "test" ]]`.',
].join("\n"),
}),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "entities", "alpha.md"),
renderWikiMarkdown({
frontmatter: {
pageType: "entity",
id: "entity.alpha",
title: "Alpha",
sourceIds: ["source.code-snippets"],
},
body: "# Alpha\n",
}),
"utf8",
);
const result = await lintMemoryWikiVault(config);
const linkIssues = result.issues.filter(
(issue) => issue.path === "sources/code-snippets.md" && issue.code === "broken-wikilink",
);
expect(linkIssues).toHaveLength(0);
});
it("accepts unmanaged raw markdown source pages without page frontmatter", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-lint-raw-sources-",
});
await fs.mkdir(path.join(rootDir, "sources"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "sources", "raw-alpha.md"),
`# Raw Alpha Source\n\n${WIKI_RAW_SOURCE_MARKER}\n\nRaw source notes stay usable as source evidence.\n`,
"utf8",
);
await fs.writeFile(
path.join(rootDir, "sources", "raw-native-frontmatter.md"),
`---\nid: note\n---\n\n# Raw Native Frontmatter\n\n${WIKI_RAW_SOURCE_MARKER}\n\nRaw source notes stay usable as source evidence.\n`,
"utf8",
);
await fs.writeFile(
path.join(rootDir, "sources", "raw-native-frontmatter-copy.md"),
`---\nid: note\n---\n\n# Raw Native Frontmatter Copy\n\n${WIKI_RAW_SOURCE_MARKER}\n\nRaw source notes stay usable as source evidence.\n`,
"utf8",
);
const result = await lintMemoryWikiVault(config);
const rawIssueCodes = issueCodesForPath(result, "sources/raw-alpha.md");
expect(rawIssueCodes).not.toContain("missing-id");
expect(rawIssueCodes).not.toContain("missing-page-type");
expect(rawIssueCodes).not.toContain("stale-page");
expect(
result.issuesByCategory.structure.filter((issue) => issue.path === "sources/raw-alpha.md"),
).toHaveLength(0);
const nativeFrontmatterIssueCodes = issueCodesForPath(
result,
"sources/raw-native-frontmatter.md",
);
expect(nativeFrontmatterIssueCodes).not.toContain("missing-id");
expect(nativeFrontmatterIssueCodes).not.toContain("missing-page-type");
expect(nativeFrontmatterIssueCodes).not.toContain("stale-page");
expect(nativeFrontmatterIssueCodes).toContain("duplicate-id");
expect(issueCodesForPath(result, "sources/raw-native-frontmatter-copy.md")).toContain(
"duplicate-id",
);
});
it("keeps unmarked source pages without frontmatter visible to structure lint", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-lint-unmarked-sources-",
});
await fs.mkdir(path.join(rootDir, "sources"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "sources", "unmarked-alpha.md"),
"# Unmarked Alpha Source\n\nThis page has no raw-source designation.\n",
"utf8",
);
const result = await lintMemoryWikiVault(config);
expect(issueCodesForPath(result, "sources/unmarked-alpha.md")).toEqual(
expect.arrayContaining(["missing-id", "missing-page-type", "stale-page"]),
);
});
it("keeps malformed imported source bodies visible to structure lint", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-lint-malformed-imports-",
});
await fs.mkdir(path.join(rootDir, "sources"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "sources", "bridge-alpha.md"),
[
"# Memory Bridge: Alpha",
"",
"## Bridge Source",
"- Workspace: `/tmp/workspace`",
"- Relative path: `MEMORY.md`",
"",
"## Content",
"alpha bridge body",
"",
"## Notes",
"<!-- openclaw:human:start -->",
"<!-- openclaw:human:end -->",
].join("\n"),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "sources", "unsafe-alpha.md"),
[
"# Unsafe Local Import: alpha.md",
"",
"## Unsafe Local Source",
"- Configured path: `/tmp/private`",
"- Relative path: `alpha.md`",
"",
"## Content",
"alpha unsafe-local body",
"",
"## Notes",
"<!-- openclaw:human:start -->",
"<!-- openclaw:human:end -->",
].join("\n"),
"utf8",
);
const result = await lintMemoryWikiVault(config);
expect(issueCodesForPath(result, "sources/bridge-alpha.md")).toEqual(
expect.arrayContaining(["missing-id", "missing-page-type"]),
);
expect(issueCodesForPath(result, "sources/unsafe-alpha.md")).toEqual(
expect.arrayContaining(["missing-id", "missing-page-type"]),
);
});
it("keeps fully truncated tracked imported source pages visible to structure lint", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-lint-tracked-truncated-imports-",
});
await fs.mkdir(path.join(rootDir, "sources"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "sources", "bridge-truncated.md"),
[
"## Related",
WIKI_RELATED_START_MARKER,
"- No related pages yet.",
WIKI_RELATED_END_MARKER,
"",
].join("\n"),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "sources", "unsafe-truncated.md"),
[
"## Related",
WIKI_RELATED_START_MARKER,
"- No related pages yet.",
WIKI_RELATED_END_MARKER,
"",
].join("\n"),
"utf8",
);
await writeMemoryWikiSourceSyncState(config.vault.path, {
version: 1,
entries: {
bridge: {
group: "bridge",
pagePath: "sources/bridge-truncated.md",
sourcePath: "/tmp/MEMORY.md",
sourceUpdatedAtMs: 1,
sourceSize: 2,
renderFingerprint: "bridge-fingerprint",
},
unsafe: {
group: "unsafe-local",
pagePath: "sources/unsafe-truncated.md",
sourcePath: "/tmp/private/alpha.md",
sourceUpdatedAtMs: 3,
sourceSize: 4,
renderFingerprint: "unsafe-fingerprint",
},
},
});
const result = await lintMemoryWikiVault(config);
expect(issueCodesForPath(result, "sources/bridge-truncated.md")).toEqual(
expect.arrayContaining(["missing-id", "missing-page-type"]),
);
expect(issueCodesForPath(result, "sources/unsafe-truncated.md")).toEqual(
expect.arrayContaining(["missing-id", "missing-page-type"]),
);
});
it("keeps generated source pages with missing frontmatter visible to structure lint", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-lint-generated-source-bodies-",
});
await fs.mkdir(path.join(rootDir, "sources"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "sources", "local-file.md"),
[
"# Local File Source",
"",
"## Source",
"- Type: `local-file`",
"- Path: `/tmp/source.md`",
"",
"## Content",
"source body",
"",
"## Notes",
"<!-- openclaw:human:start -->",
"<!-- openclaw:human:end -->",
"",
].join("\n"),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "sources", "local-file-partial-frontmatter.md"),
renderWikiMarkdown({
frontmatter: {
id: "source.partial",
title: "Partial Source",
},
body: [
WIKI_RAW_SOURCE_MARKER,
"",
"# Local File Source",
"",
"## Source",
"- Type: `local-file`",
"- Path: `/tmp/source.md`",
"",
"## Content",
"source body",
"",
"## Notes",
"<!-- openclaw:human:start -->",
"<!-- openclaw:human:end -->",
"",
].join("\n"),
}),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "sources", "chatgpt-export.md"),
[
"# ChatGPT Export: Alpha",
"",
"## Source",
"- Conversation id: `abc123`",
"- Export file: `/tmp/conversations.json`",
"",
"## Active Branch Transcript",
"### User",
"alpha",
"",
"## Notes",
"<!-- openclaw:human:start -->",
"<!-- openclaw:human:end -->",
"",
].join("\n"),
"utf8",
);
const result = await lintMemoryWikiVault(config);
expect(issueCodesForPath(result, "sources/local-file.md")).toEqual(
expect.arrayContaining(["missing-id", "missing-page-type", "stale-page"]),
);
expect(issueCodesForPath(result, "sources/local-file-partial-frontmatter.md")).toEqual(
expect.arrayContaining(["missing-page-type", "stale-page"]),
);
expect(issueCodesForPath(result, "sources/chatgpt-export.md")).toEqual(
expect.arrayContaining(["missing-id", "missing-page-type", "stale-page"]),
);
});
it("detects duplicate ids, provenance gaps, contradictions, and open questions", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-lint-",
config: {
vault: { renderMode: "obsidian" },
},
});
await Promise.all(
["entities", "concepts", "sources", "syntheses"].map((dir) =>
fs.mkdir(path.join(rootDir, dir), { recursive: true }),
),
);
const duplicate = renderWikiMarkdown({
frontmatter: {
pageType: "entity",
id: "entity.alpha",
title: "Alpha",
contradictions: ["Conflicts with source.beta"],
questions: ["Is Alpha still active?"],
confidence: 0.2,
claims: [
{
id: "claim.alpha.db",
text: "Alpha uses PostgreSQL for production writes.",
confidence: 0.2,
evidence: [],
},
],
},
body: "# Alpha\n\n[[missing-page]]\n",
});
await fs.writeFile(path.join(rootDir, "entities", "alpha.md"), duplicate, "utf8");
await fs.writeFile(path.join(rootDir, "concepts", "alpha.md"), duplicate, "utf8");
await fs.writeFile(
path.join(rootDir, "sources", "bridge-alpha.md"),
renderWikiMarkdown({
frontmatter: {
pageType: "source",
id: "source.bridge.alpha",
title: "Bridge Alpha",
sourceType: "memory-bridge",
},
body: "# Bridge Alpha\n",
}),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "syntheses", "alpha-db.md"),
renderWikiMarkdown({
frontmatter: {
pageType: "synthesis",
id: "synthesis.alpha.db",
title: "Alpha Database",
sourceIds: ["source.bridge.alpha"],
updatedAt: "2025-10-01T00:00:00.000Z",
claims: [
{
id: "claim.alpha.db",
text: "Alpha uses MySQL for production writes.",
status: "contested",
confidence: 0.7,
evidence: [
{
sourceId: "source.bridge.alpha",
lines: "1-3",
updatedAt: "2025-10-01T00:00:00.000Z",
},
],
},
],
},
body: "# Alpha Database\n",
}),
"utf8",
);
const result = await lintMemoryWikiVault(config);
expect(result.issueCount).toBeGreaterThan(0);
expect(result.issues.map((issue) => issue.code)).toContain("duplicate-id");
expect(result.issues.map((issue) => issue.code)).toContain("missing-source-ids");
expect(result.issues.map((issue) => issue.code)).toContain("missing-import-provenance");
expect(result.issues.map((issue) => issue.code)).toContain("broken-wikilink");
expect(result.issues.map((issue) => issue.code)).toContain("contradiction-present");
expect(result.issues.map((issue) => issue.code)).toContain("claim-conflict");
expect(result.issues.map((issue) => issue.code)).toContain("open-question");
expect(result.issues.map((issue) => issue.code)).toContain("low-confidence");
expect(result.issues.map((issue) => issue.code)).toContain("claim-missing-evidence");
expect(result.issues.map((issue) => issue.code)).toContain("claim-low-confidence");
expect(result.issues.map((issue) => issue.code)).toContain("stale-page");
expect(result.issues.map((issue) => issue.code)).toContain("stale-claim");
expect(result.issuesByCategory.contradictions.map((issue) => issue.code)).toContain(
"claim-conflict",
);
expect(result.issuesByCategory["open-questions"].length).toBeGreaterThanOrEqual(2);
expect(result.issuesByCategory.provenance.map((issue) => issue.code)).toContain(
"missing-import-provenance",
);
expect(result.issuesByCategory.provenance.map((issue) => issue.code)).toContain(
"claim-missing-evidence",
);
await expect(fs.readFile(result.reportPath, "utf8")).resolves.toContain("### Errors");
await expect(fs.readFile(result.reportPath, "utf8")).resolves.toContain("### Contradictions");
await expect(fs.readFile(result.reportPath, "utf8")).resolves.toContain("### Open Questions");
});
it("reports unparsable frontmatter as a lint issue instead of failing the whole vault (#96125)", async () => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-lint-invalid-frontmatter-",
});
await fs.mkdir(path.join(rootDir, "syntheses"), { recursive: true });
await fs.writeFile(
path.join(rootDir, "syntheses", "broken.md"),
[
"---",
"pageType: synthesis",
"id: synthesis.broken",
"sourceIds:",
' - **MEMORY.md line 235**:"some quoted, value"',
"---",
"",
"# Broken",
"",
"Body text.",
].join("\n"),
"utf8",
);
await fs.writeFile(
path.join(rootDir, "syntheses", "healthy.md"),
renderWikiMarkdown({
frontmatter: {
pageType: "synthesis",
id: "synthesis.healthy",
title: "Healthy",
sourceIds: ["source.alpha"],
},
body: "# Healthy\n",
}),
"utf8",
);
const result = await lintMemoryWikiVault(config);
expect(issueCodesForPath(result, "syntheses/broken.md")).toEqual(["invalid-frontmatter"]);
expect(issueCodesForPath(result, "syntheses/healthy.md")).not.toContain("invalid-frontmatter");
await expect(fs.readFile(result.reportPath, "utf8")).resolves.toContain(
"Frontmatter failed to parse: Unexpected scalar",
);
});
it.each([
{
name: "syntax-error",
frontmatterLines: [
"pageType: report",
"id: report.lint",
"sourceIds:",
' - **MEMORY.md line 235**:"some quoted, value"',
],
error: "Unexpected scalar",
},
{
name: "sequence-root",
frontmatterLines: ["- pageType: report", " id: report.lint"],
error: "Wiki frontmatter must be a YAML mapping",
},
])(
"rejects a malformed lint report without changing its bytes ($name)",
async ({ frontmatterLines, error }) => {
const { rootDir, config } = await createVault({
prefix: "memory-wiki-lint-malformed-report-",
});
const reportPath = path.join(rootDir, "reports", "lint.md");
await fs.mkdir(path.dirname(reportPath), { recursive: true });
const malformedReport = [
"---",
...frontmatterLines,
"---",
"",
"# Lint Report",
"",
"Existing report body.",
"",
].join("\n");
await fs.writeFile(reportPath, malformedReport, "utf8");
await expect(lintMemoryWikiVault(config)).rejects.toThrow(error);
await expect(fs.readFile(reportPath, "utf8")).resolves.toBe(malformedReport);
},
);
});