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
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
582 lines
19 KiB
TypeScript
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);
|
|
},
|
|
);
|
|
});
|