This commit is contained in:
2026-07-12 21:26:08 +08:00
commit 9dd41afd48
502 changed files with 129901 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
const { describe, it } = require("node:test");
// DEPRECATED: Covers the legacy wash template; remove in the same retirement cycle as templates/graph-styles/wash/.
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const GRAPH_WASH_PATH = path.resolve(__dirname, "../../templates/graph-styles/wash/graph-wash.js");
const GRAPH_WASH_SOURCE = fs.readFileSync(GRAPH_WASH_PATH, "utf8");
const GRAPH_WASH_BOOTSTRAP_SOURCE = GRAPH_WASH_SOURCE.match(
/const helpers = window\.WikiGraphWashHelpers;[\s\S]*?const safeLocalStorage = createSafeStorage\(rawLocalStorage, console\.warn\);/
)[0];
describe("graph-wash bootstrap", () => {
it("exports helpers to window even when CommonJS exists", () => {
const helpersSource = fs.readFileSync(path.resolve(__dirname, "../../templates/graph-styles/wash/graph-wash-helpers.js"), "utf8");
const sandbox = {
module: { exports: {} },
exports: {},
require,
console,
Intl,
window: {}
};
vm.createContext(sandbox);
vm.runInContext(helpersSource, sandbox, { filename: "graph-wash-helpers.js" });
assert.equal(typeof sandbox.module.exports.truncateLabel, "function");
assert.equal(typeof sandbox.window.WikiGraphWashHelpers.truncateLabel, "function");
});
it("logs and exits when helpers are missing", () => {
const errors = [];
const sandbox = {
window: {},
console: {
error: (...args) => errors.push(args.join(" "))
}
};
vm.createContext(sandbox);
vm.runInContext(GRAPH_WASH_SOURCE, sandbox, { filename: GRAPH_WASH_PATH });
assert.deepEqual(errors, ["[wiki] graph-wash-helpers.js is missing or failed to load"]);
});
it("passes null to createSafeStorage when localStorage getter throws", () => {
let capturedStorage;
const sandbox = {
window: {
WikiGraphWashHelpers: {
truncateLabel: () => ({ text: "", truncated: false }),
cardDims: () => ({ w: 72, h: 36 }),
createSafeStorage: (storage) => {
capturedStorage = storage;
return { get: () => null, set: () => {} };
},
getWikiStorageNamespace: () => "llm-wiki:test:abc",
defaultQueue: () => ({ version: 1, favorites: [], notes: [], recentNoteIds: [] }),
normalizeQueue: (queue) => queue,
toggleQueueFavorite: (queue) => queue,
appendQueueNote: (queue) => queue,
summarizeQueue: () => ({ favorite_count: 0, note_count: 0, recent_items: [] }),
buildAtlasModel: () => ({ meta: {}, nodes: [], edges: [] }),
deriveAtlasLayout: () => ({ nodePositions: {}, bounds: { minX: 0, minY: 0, maxX: 0, maxY: 0 } }),
resolveAtlasVisibleSnapshot: () => ({ nodes: [], edges: [], nodeIds: new Set(), labelNodeIds: {} }),
resolveAtlasSelectedNodeId: () => null,
atlasConfidenceLabel: (value) => value,
atlasTypeLabel: (value) => value,
atlasNodeKind: (value) => value,
stripAtlasMarkdown: (value) => value,
defaultLearning: () => ({ version: 1, entry: { recommended_start_node_id: null, recommended_start_reason: null, default_mode: "global" }, views: { path: { enabled: false, start_node_id: null, node_ids: [], degraded: true }, community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: false, degraded: true }, global: { enabled: true, node_ids: [], degraded: false } }, communities: [], degraded: { path_to_community: true, community_to_global: true } }),
normalizeLearning: () => ({ version: 1, entry: { recommended_start_node_id: null, recommended_start_reason: null, default_mode: "global" }, views: { path: { enabled: false, start_node_id: null, node_ids: [], degraded: true }, community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: false, degraded: true }, global: { enabled: true, node_ids: [], degraded: false } }, communities: [], degraded: { path_to_community: true, community_to_global: true } }),
resolveInitialMode: () => "global",
getVisibleNodeIds: () => [],
getVisibleLinks: () => [],
shouldAutoOpenDrawer: () => false
},
get localStorage() {
throw new Error("blocked");
}
},
document: {
getElementById: (id) => {
if (id === "graph-data") {
return { textContent: '{"nodes":[],"edges":[],"insights":{}}' };
}
if (id === "atlas" || id === "node-layer" || id === "edge-layer") {
return {};
}
return null;
}
},
d3: {
select: () => ({ node: () => null })
},
console: {
warn: () => {}
},
JSON,
Object
};
vm.createContext(sandbox);
vm.runInContext(`(function () {\n${GRAPH_WASH_BOOTSTRAP_SOURCE}\n})();`, sandbox, { filename: GRAPH_WASH_PATH });
assert.equal(capturedStorage, null);
});
});
+296
View File
@@ -0,0 +1,296 @@
const { describe, it } = require("node:test");
// DEPRECATED: Covers the legacy wash helpers; remove in the same retirement cycle as templates/graph-styles/wash/.
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const vm = require("node:vm");
const {
splitLabelGraphemes,
labelCharWidth,
measureLabelWidth,
truncateLabel,
cardDims,
createSafeStorage
} = require("../../templates/graph-styles/wash/graph-wash-helpers");
const LABEL_CJK_WIDTH = 15;
const LABEL_LATIN_WIDTH = 8.5;
const LABEL_MIN_WIDTH = 72;
const LABEL_MAX_WIDTH = 180;
const LABEL_ELLIPSIS = "…";
const HELPERS_PATH = path.resolve(__dirname, "../../templates/graph-styles/wash/graph-wash-helpers.js");
function loadHelpersWith(overrides = {}) {
const source = fs.readFileSync(HELPERS_PATH, "utf8");
const sandbox = {
module: { exports: {} },
exports: {},
require,
console,
Intl,
window: {},
...overrides
};
vm.createContext(sandbox);
vm.runInContext(source, sandbox, { filename: HELPERS_PATH });
return sandbox;
}
// --- splitLabelGraphemes ---
describe("splitLabelGraphemes", () => {
it("splits empty string", () => {
assert.deepEqual(splitLabelGraphemes(""), []);
});
it("splits ASCII", () => {
assert.deepEqual(splitLabelGraphemes("abc"), ["a", "b", "c"]);
});
it("splits CJK characters", () => {
assert.deepEqual(splitLabelGraphemes("中文"), ["中", "文"]);
});
it("does not corrupt emoji with surrogate pairs", () => {
const result = splitLabelGraphemes("a👨‍👩‍👧‍👦b");
assert.ok(result.includes("👨‍👩‍👧‍👦"), "family emoji kept as single grapheme");
assert.equal(result[0], "a");
assert.equal(result[result.length - 1], "b");
});
it("does not corrupt surrogate pairs", () => {
const result = splitLabelGraphemes("𠮷");
// Whether it's 1 grapheme (with Intl.Segmenter) or split into code points,
// the result must not contain unmatched surrogate halves
assert.ok(!/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(result.join("")));
});
it("falls back when Intl.Segmenter is unavailable", () => {
const sandbox = loadHelpersWith({ Intl: {} });
const fallbackHelpers = sandbox.module.exports;
assert.deepEqual(Array.from(fallbackHelpers.splitLabelGraphemes("abc")), ["a", "b", "c"]);
assert.deepEqual(Array.from(fallbackHelpers.splitLabelGraphemes("中文")), ["中", "文"]);
assert.deepEqual(Array.from(fallbackHelpers.splitLabelGraphemes("👨‍👩‍👧‍👦")), ["👨‍👩‍👧‍👦"]);
const truncated = fallbackHelpers.truncateLabel("节点A👨‍👩‍👧‍👦AlphaBeta超长标签" + "超".repeat(20), 120);
assert.equal(truncated.truncated, true);
assert.ok(truncated.text.endsWith(LABEL_ELLIPSIS));
assert.ok(
!/\uD800(?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(truncated.text),
"fallback path keeps surrogate pairs intact"
);
const emojiBoundary = fallbackHelpers.truncateLabel("节点A👨‍👩‍👧‍👦AlphaBeta超长标签" + "超".repeat(20), 90);
assert.ok(emojiBoundary.text.endsWith(LABEL_ELLIPSIS));
assert.ok(
!emojiBoundary.text.startsWith("") && !emojiBoundary.text.includes("" + LABEL_ELLIPSIS),
"fallback path should not cut through a ZWJ sequence"
);
assert.ok(
!emojiBoundary.text.includes("👨‍") || emojiBoundary.text.includes("👨‍👩‍👧‍👦"),
"fallback path should keep the family emoji intact if it is included"
);
});
});
// --- labelCharWidth ---
describe("labelCharWidth", () => {
it("returns CJK width for CJK character", () => {
assert.equal(labelCharWidth("中"), LABEL_CJK_WIDTH);
});
it("returns Latin width for Latin character", () => {
assert.equal(labelCharWidth("a"), LABEL_LATIN_WIDTH);
});
it("returns Latin width for digit", () => {
assert.equal(labelCharWidth("5"), LABEL_LATIN_WIDTH);
});
it("returns Latin width for punctuation", () => {
assert.equal(labelCharWidth("-"), LABEL_LATIN_WIDTH);
});
});
// --- measureLabelWidth ---
describe("measureLabelWidth", () => {
it("returns 0 for empty array", () => {
assert.equal(measureLabelWidth([]), 0);
});
it("returns correct width for single grapheme", () => {
assert.equal(measureLabelWidth(["a"]), LABEL_LATIN_WIDTH);
});
it("sums mixed CJK and Latin widths", () => {
const width = measureLabelWidth(["中", "a", "文"]);
assert.equal(width, LABEL_CJK_WIDTH * 2 + LABEL_LATIN_WIDTH);
});
});
// --- truncateLabel ---
describe("truncateLabel", () => {
it("handles empty string", () => {
const r = truncateLabel("", 100);
assert.equal(r.text, "");
assert.equal(r.truncated, false);
});
it("handles null", () => {
const r = truncateLabel(null, 100);
assert.equal(r.text, "");
assert.equal(r.truncated, false);
});
it("handles undefined", () => {
const r = truncateLabel(undefined, 100);
assert.equal(r.text, "");
assert.equal(r.truncated, false);
});
it("does not truncate short label", () => {
const r = truncateLabel("短标签", 120);
assert.equal(r.truncated, false);
assert.equal(r.text, "短标签");
});
it("truncates long label with ellipsis", () => {
const longLabel = "超".repeat(30);
const r = truncateLabel(longLabel, 100);
assert.equal(r.truncated, true);
assert.ok(r.text.endsWith(LABEL_ELLIPSIS));
});
it("does not corrupt emoji when truncating", () => {
const r = truncateLabel("节点A👨‍👩‍👧‍👦AlphaBeta超长标签" + "超".repeat(20), 120);
assert.equal(r.truncated, true);
assert.ok(!r.text.includes("undefined"));
assert.ok(
!/\uD800(?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(r.text),
"no unmatched surrogate halves"
);
});
it("handles CJK + Latin mix", () => {
const r = truncateLabel("中文English混合标签" + "超".repeat(20), 120);
assert.equal(r.truncated, true);
assert.ok(r.text.endsWith(LABEL_ELLIPSIS));
});
it("respects maxWidth exactly at boundary", () => {
const label = "a".repeat(10);
const r = truncateLabel(label, 1000);
assert.equal(r.truncated, false);
assert.equal(r.text, label);
});
});
// --- cardDims ---
describe("cardDims", () => {
it("returns dimensions for short label", () => {
const r = cardDims({ id: "1", label: "短", type: "entity" });
assert.ok(r.w >= LABEL_MIN_WIDTH);
assert.ok(r.w <= LABEL_MAX_WIDTH);
assert.equal(r.h, 36);
});
it("caps width at LABEL_MAX_WIDTH for long label", () => {
const r = cardDims({ id: "1", label: "超".repeat(30), type: "entity" });
assert.equal(r.w, LABEL_MAX_WIDTH);
});
it("enforces LABEL_MIN_WIDTH for empty label", () => {
const r = cardDims({ id: "1", label: "", type: "entity" });
assert.equal(r.w, LABEL_MIN_WIDTH);
});
it("taller for topic type", () => {
const r = cardDims({ id: "1", label: "T", type: "topic" });
assert.equal(r.h, 40);
});
it("shorter for source type", () => {
const r = cardDims({ id: "1", label: "S", type: "source" });
assert.equal(r.h, 32);
});
it("returns dimensions for generic entity", () => {
const r = cardDims({ id: "1", label: "X", type: "entity" });
assert.ok(r.w > 0);
assert.ok(r.h > 0);
});
});
// --- browser export ---
describe("browser export", () => {
it("exports helpers to window when CommonJS is unavailable", () => {
const source = fs.readFileSync(HELPERS_PATH, "utf8");
const sandbox = {
console,
Intl,
window: {}
};
vm.createContext(sandbox);
vm.runInContext(source, sandbox, { filename: HELPERS_PATH });
assert.equal(typeof sandbox.window.WikiGraphWashHelpers, "object");
assert.equal(typeof sandbox.window.WikiGraphWashHelpers.truncateLabel, "function");
assert.equal(typeof sandbox.window.WikiGraphWashHelpers.cardDims, "function");
assert.equal(typeof sandbox.window.WikiGraphWashHelpers.createSafeStorage, "function");
});
});
// --- createSafeStorage ---
describe("createSafeStorage", () => {
it("gets and sets normally", () => {
const store = {};
const storage = createSafeStorage({
getItem: (k) => store[k],
setItem: (k, v) => { store[k] = v; }
});
storage.set("k", "v");
assert.equal(storage.get("k"), "v");
});
it("returns null when get throws", () => {
const logs = [];
const storage = createSafeStorage({
getItem: () => { throw new Error("boom"); },
setItem: () => {}
}, (...args) => logs.push(args));
assert.equal(storage.get("k"), null);
assert.equal(logs.length, 1);
});
it("swallows set errors", () => {
const logs = [];
const storage = createSafeStorage({
getItem: () => null,
setItem: () => { throw new Error("boom"); }
}, (...args) => logs.push(args));
storage.set("k", "v");
assert.equal(logs.length, 1);
});
it("handles null logger", () => {
const storage = createSafeStorage({
getItem: () => { throw new Error("boom"); },
setItem: () => { throw new Error("boom"); }
}, null);
assert.equal(storage.get("k"), null);
storage.set("k", "v");
});
it("handles null storage", () => {
const storage = createSafeStorage(null, null);
assert.equal(storage.get("k"), null);
storage.set("k", "v");
});
});
+260
View File
@@ -0,0 +1,260 @@
const { describe, it } = require("node:test");
// DEPRECATED: Covers the legacy wash helpers; remove in the same retirement cycle as templates/graph-styles/wash/.
const assert = require("node:assert/strict");
const {
defaultLearning,
normalizeLearning,
resolveInitialMode,
getCommunityNodeIds,
getVisibleNodeIds,
getVisibleLinks,
buildSearchIndex,
applySearchToNodeIds,
filterLinksByTypes,
applyFocusMode,
resolveVisibleSnapshot,
shouldAutoOpenDrawer
} = require("../../templates/graph-styles/wash/graph-wash-helpers");
describe("defaultLearning", () => {
it("returns a stable empty learning structure", () => {
const d = defaultLearning();
assert.equal(d.version, 1);
assert.equal(d.entry.default_mode, "global");
assert.equal(d.entry.recommended_start_node_id, null);
assert.equal(d.views.path.enabled, false);
assert.equal(d.views.path.degraded, true);
assert.equal(d.views.community.enabled, false);
assert.equal(d.views.community.degraded, true);
assert.equal(d.views.global.enabled, true);
assert.equal(d.views.global.degraded, false);
assert.deepEqual(d.communities, []);
assert.equal(d.degraded.path_to_community, true);
assert.equal(d.degraded.community_to_global, true);
});
});
describe("normalizeLearning", () => {
it("returns default when input is null", () => {
const n = normalizeLearning(null);
assert.equal(n.version, 1);
assert.equal(n.entry.default_mode, "global");
});
it("returns default when input is undefined", () => {
const n = normalizeLearning(undefined);
assert.equal(n.version, 1);
});
it("preserves valid learning data", () => {
const raw = {
version: 1,
entry: { recommended_start_node_id: "A", recommended_start_reason: "community_hub", default_mode: "path" },
views: {
path: { enabled: true, start_node_id: "A", node_ids: ["A", "B"], degraded: false },
community: { enabled: true, community_id: "c1", label: "Community 1", node_ids: ["A", "B", "C"], is_weak: false, degraded: false },
global: { enabled: true, node_ids: ["A", "B", "C"], degraded: false }
},
communities: [{ id: "c1", label: "Community 1", node_count: 3, source_count: 1, is_primary: true }],
degraded: { path_to_community: false, community_to_global: false }
};
const n = normalizeLearning(raw);
assert.equal(n.entry.recommended_start_node_id, "A");
assert.deepEqual(n.views.path.node_ids, ["A", "B"]);
assert.equal(n.communities.length, 1);
});
it("fills missing views with defaults", () => {
const n = normalizeLearning({ version: 1 });
assert.equal(n.views.path.enabled, false);
assert.equal(n.views.community.enabled, false);
assert.equal(n.views.global.enabled, true);
});
it("handles missing node_ids arrays", () => {
const n = normalizeLearning({ views: { path: { enabled: true }, community: {}, global: {} } });
assert.deepEqual(n.views.path.node_ids, []);
});
});
describe("resolveInitialMode", () => {
it("returns global when learning is null", () => {
assert.equal(resolveInitialMode(null), "global");
});
it("keeps the first paint global even when path is available", () => {
const learning = { entry: { default_mode: "path" }, views: { path: { degraded: false }, community: { degraded: false } } };
assert.equal(resolveInitialMode(learning), "global");
});
it("keeps the first paint global when community is available", () => {
const learning = { entry: { default_mode: "community" }, views: { path: { degraded: true }, community: { degraded: false } } };
assert.equal(resolveInitialMode(learning), "global");
});
});
describe("getCommunityNodeIds", () => {
it("returns sorted node ids for a matching community", () => {
const nodes = [
{ id: "B", community: "c2" },
{ id: "A", community: "c1" },
{ id: "C", community: "c1" },
{ id: "D", community: null }
];
assert.deepEqual(getCommunityNodeIds(nodes, "c1"), ["A", "C"]);
});
it("returns empty array for missing community", () => {
const nodes = [{ id: "A", community: "c1" }];
assert.deepEqual(getCommunityNodeIds(nodes, "c9"), []);
});
it("returns empty array when community id is absent", () => {
const nodes = [{ id: "A", community: "c1" }];
assert.deepEqual(getCommunityNodeIds(nodes, null), []);
});
});
describe("getVisibleNodeIds", () => {
it("returns empty array when learning is null", () => {
assert.deepEqual(getVisibleNodeIds(null, "path"), []);
});
it("returns node_ids for valid mode", () => {
const learning = { views: { path: { enabled: true, node_ids: ["A", "B"] } } };
assert.deepEqual(getVisibleNodeIds(learning, "path"), ["A", "B"]);
});
it("returns empty when view is disabled", () => {
const learning = { views: { path: { enabled: false, node_ids: ["A"] } } };
assert.deepEqual(getVisibleNodeIds(learning, "path"), []);
});
it("returns empty for global mode when view node_ids is empty", () => {
const learning = { views: { global: { enabled: true, node_ids: [] } } };
assert.deepEqual(getVisibleNodeIds(learning, "global"), []);
});
});
describe("getVisibleLinks", () => {
it("returns all links when visibleIds is empty", () => {
const links = [{ source: "A", target: "B" }];
assert.deepEqual(getVisibleLinks(links, []), links);
});
it("filters links to only those with both endpoints visible", () => {
const links = [
{ source: { id: "A" }, target: { id: "B" } },
{ source: { id: "A" }, target: { id: "C" } },
{ source: { id: "B" }, target: { id: "C" } }
];
const result = getVisibleLinks(links, ["A", "B"]);
assert.equal(result.length, 1);
assert.equal(result[0].source.id, "A");
assert.equal(result[0].target.id, "B");
});
});
describe("search helpers", () => {
it("builds searchable haystacks and finds matches", () => {
const nodes = [
{ id: "A", label: "Transformer", content: "attention and language" },
{ id: "B", label: "CNN", content: "vision only" }
];
const index = buildSearchIndex(nodes);
assert.equal(index.length, 2);
assert.deepEqual(applySearchToNodeIds(index, "attention"), ["A"]);
assert.deepEqual(applySearchToNodeIds(index, ""), ["A", "B"]);
});
});
describe("filterLinksByTypes", () => {
it("keeps only enabled edge types", () => {
const links = [
{ id: "e1", type: "EXTRACTED" },
{ id: "e2", type: "INFERRED" },
{ id: "e3", type: "AMBIGUOUS" }
];
const result = filterLinksByTypes(links, { EXTRACTED: true, INFERRED: false, AMBIGUOUS: false });
assert.deepEqual(result.map((link) => link.id), ["e1"]);
});
});
describe("applyFocusMode", () => {
const nodes = [
{ id: "A", degree: 3 },
{ id: "B", degree: 2 },
{ id: "C", degree: 2 },
{ id: "D", degree: 1 }
];
const links = [
{ source: "A", target: "B", weight: 0.95 },
{ source: "A", target: "C", weight: 0.82 },
{ source: "C", target: "D", weight: 0.45 }
];
const nodeIds = ["A", "B", "C", "D"];
it("returns scoped nodes unchanged for all mode", () => {
const result = applyFocusMode({ mode: "all", nodes, links, nodeIds });
assert.deepEqual(result.node_ids, nodeIds);
assert.equal(result.links.length, 3);
});
it("keeps anchor and one-hop neighbors for one_hop mode", () => {
const result = applyFocusMode({ mode: "one_hop", nodes, links, nodeIds, anchorNodeId: "A" });
assert.deepEqual(result.node_ids, ["A", "B", "C"]);
assert.equal(result.links.length, 2);
});
it("keeps only strong links for high_confidence mode", () => {
const result = applyFocusMode({ mode: "high_confidence", nodes, links, nodeIds, anchorNodeId: "A", highConfidenceThreshold: 0.8 });
assert.deepEqual(result.node_ids, ["A", "B", "C"]);
assert.equal(result.links.length, 2);
});
it("prefers high-score nodes for core mode", () => {
const result = applyFocusMode({ mode: "core", nodes, links, nodeIds, coreLimit: 2 });
assert.deepEqual(result.node_ids, ["A", "C"]);
assert.equal(result.links.length, 1);
});
});
describe("resolveVisibleSnapshot", () => {
it("applies search after focus mode", () => {
const nodes = [
{ id: "A", label: "Transformer", content: "attention", degree: 3 },
{ id: "B", label: "Attention", content: "weights", degree: 2 },
{ id: "C", label: "CNN", content: "vision", degree: 1 }
];
const links = [
{ source: "A", target: "B", type: "EXTRACTED", weight: 0.95 },
{ source: "A", target: "C", type: "INFERRED", weight: 0.4 }
];
const result = resolveVisibleSnapshot({
nodes,
links,
baseNodeIds: ["A", "B", "C"],
filters: { EXTRACTED: true, INFERRED: false, AMBIGUOUS: false },
focusMode: "high_confidence",
searchQuery: "attention",
anchorNodeId: "A",
highConfidenceThreshold: 0.8
});
assert.deepEqual(result.node_ids, ["A", "B"]);
assert.equal(result.links.length, 1);
});
});
describe("shouldAutoOpenDrawer", () => {
it("returns true for path mode after explicit path entry", () => {
assert.equal(shouldAutoOpenDrawer("path"), true);
});
it("returns false for community mode", () => {
assert.equal(shouldAutoOpenDrawer("community"), false);
});
it("returns false for global mode", () => {
assert.equal(shouldAutoOpenDrawer("global"), false);
});
});
+115
View File
@@ -0,0 +1,115 @@
const { describe, it } = require("node:test");
// DEPRECATED: Covers the legacy wash helpers; remove in the same retirement cycle as templates/graph-styles/wash/.
const assert = require("node:assert/strict");
const {
getWikiStorageNamespace,
defaultQueue,
normalizeQueue,
toggleQueueFavorite,
appendQueueNote,
summarizeQueue
} = require("../../templates/graph-styles/wash/graph-wash-helpers");
describe("getWikiStorageNamespace", () => {
it("returns a stable namespace for the same wiki", () => {
const a = getWikiStorageNamespace({ wiki_title: "AI知识图谱Demo" }, "/wiki/graph.html");
const b = getWikiStorageNamespace({ wiki_title: "AI知识图谱Demo" }, "/wiki/graph.html");
assert.equal(a, b);
assert.match(a, /^llm-wiki:/);
});
it("changes namespace when pathname changes", () => {
const a = getWikiStorageNamespace({ wiki_title: "AI知识图谱Demo" }, "/wiki-a/graph.html");
const b = getWikiStorageNamespace({ wiki_title: "AI知识图谱Demo" }, "/wiki-b/graph.html");
assert.notEqual(a, b);
});
});
describe("defaultQueue and normalizeQueue", () => {
it("returns an empty queue by default", () => {
assert.deepEqual(defaultQueue(), {
version: 1,
favorites: [],
notes: [],
recentNoteIds: []
});
});
it("normalizes malformed queue data", () => {
const queue = normalizeQueue({
favorites: ["n1", "n1", 2, null],
notes: [
{ id: "a", node_id: "n1", label: "节点 A", text: "摘录", created_at: "2026-04-24T00:00:00Z" },
{ bad: true }
],
recentNoteIds: ["a", "missing", "a"]
});
assert.deepEqual(queue.favorites, ["n1", "2"]);
assert.equal(queue.notes.length, 1);
assert.deepEqual(queue.recentNoteIds, ["a"]);
});
});
describe("toggleQueueFavorite", () => {
it("adds and removes favorites", () => {
let queue = defaultQueue();
queue = toggleQueueFavorite(queue, "n1");
assert.deepEqual(queue.favorites, ["n1"]);
queue = toggleQueueFavorite(queue, "n2");
assert.deepEqual(queue.favorites, ["n2", "n1"]);
queue = toggleQueueFavorite(queue, "n1");
assert.deepEqual(queue.favorites, ["n2"]);
});
});
describe("appendQueueNote", () => {
it("prepends note and updates recent ids", () => {
let queue = defaultQueue();
queue = appendQueueNote(queue, {
id: "note-1",
node_id: "n1",
label: "节点一",
text: "第一条",
created_at: "2026-04-24T00:00:00Z"
});
queue = appendQueueNote(queue, {
id: "note-2",
node_id: "n2",
label: "节点二",
text: "第二条",
created_at: "2026-04-24T00:01:00Z"
});
assert.deepEqual(queue.notes.map((note) => note.id), ["note-2", "note-1"]);
assert.deepEqual(queue.recentNoteIds, ["note-2", "note-1"]);
});
});
describe("summarizeQueue", () => {
it("summarizes counts and recent items", () => {
let queue = defaultQueue();
queue = toggleQueueFavorite(queue, "n3");
queue = appendQueueNote(queue, {
id: "note-1",
node_id: "n1",
label: "节点一",
text: "摘录一",
created_at: "2026-04-24T00:00:00Z"
});
const summary = summarizeQueue(queue, {
n1: { id: "n1", label: "节点一" },
n3: { id: "n3", label: "节点三" }
}, 4);
assert.equal(summary.favorite_count, 1);
assert.equal(summary.note_count, 1);
assert.deepEqual(summary.recent_items, [
{ kind: "note", node_id: "n1", label: "节点一", text: "摘录一" },
{ kind: "favorite", node_id: "n3", label: "节点三", text: "" }
]);
});
});
+282
View File
@@ -0,0 +1,282 @@
const { describe, it } = require("node:test");
// DEPRECATED: Covers the legacy wash helpers; remove in the same retirement cycle as templates/graph-styles/wash/.
const assert = require("node:assert/strict");
const {
resolveVisibleSnapshot,
buildAtlasModel,
deriveAtlasLayout,
resolveAtlasVisibleSnapshot,
resolveAtlasSelectedNodeId,
getAtlasDensityMode,
atlasNodePoint,
getAtlasModelBounds,
fitAtlasViewport,
centerAtlasViewportOnPoint,
zoomAtlasViewport,
atlasViewportRect,
atlasPointToMinimap,
minimapPointToAtlasPoint,
atlasViewportToMinimapRect
} = require("../../templates/graph-styles/wash/graph-wash-helpers");
describe("resolveVisibleSnapshot", () => {
const nodes = [
{ id: "n1", label: "机器学习基础", content: "监督学习与数据预处理", degree: 2 },
{ id: "n2", label: "深度学习", content: "神经网络与 Transformer", degree: 3 },
{ id: "n3", label: "Transformer", content: "语言模型核心架构", degree: 2 },
{ id: "n4", label: "数据清洗", content: "数据预处理的一部分", degree: 1 }
];
const links = [
{ id: "e1", source: "n1", target: "n2", type: "EXTRACTED", weight: 0.95 },
{ id: "e2", source: "n2", target: "n3", type: "EXTRACTED", weight: 0.91 },
{ id: "e3", source: "n1", target: "n4", type: "INFERRED", weight: 0.55 }
];
it("combines edge filtering, focus mode, and search query", () => {
const snapshot = resolveVisibleSnapshot({
nodes,
links,
baseNodeIds: ["n1", "n2", "n3", "n4"],
filters: { EXTRACTED: true, INFERRED: false, AMBIGUOUS: false },
focusMode: "high_confidence",
searchQuery: "transformer",
anchorNodeId: "n2",
highConfidenceThreshold: 0.9
});
assert.deepEqual(snapshot.node_ids, ["n2", "n3"]);
assert.deepEqual(snapshot.nodes.map((node) => node.id), ["n2", "n3"]);
assert.deepEqual(snapshot.links.map((link) => link.id), ["e2"]);
assert.deepEqual(snapshot.searchIndex.map((entry) => entry.node.id), ["n1", "n2", "n3"]);
});
it("keeps one-hop scope around the selected anchor", () => {
const snapshot = resolveVisibleSnapshot({
nodes,
links,
baseNodeIds: ["n1", "n2", "n3"],
filters: { EXTRACTED: true, INFERRED: true, AMBIGUOUS: false },
focusMode: "one_hop",
searchQuery: "",
anchorNodeId: "n2"
});
assert.deepEqual(snapshot.node_ids, ["n1", "n2", "n3"]);
assert.deepEqual(snapshot.links.map((link) => link.id), ["e1", "e2"]);
});
it("returns empty visible nodes when search has no matches", () => {
const snapshot = resolveVisibleSnapshot({
nodes,
links,
baseNodeIds: ["n1", "n2", "n3"],
filters: { EXTRACTED: true, INFERRED: true, AMBIGUOUS: false },
focusMode: "all",
searchQuery: "不存在",
anchorNodeId: "n2"
});
assert.deepEqual(snapshot.node_ids, []);
assert.deepEqual(snapshot.nodes, []);
assert.deepEqual(snapshot.links, []);
assert.equal(snapshot.searchIndex.length, 3);
});
it("keeps an explicitly empty current range empty", () => {
const snapshot = resolveVisibleSnapshot({
nodes,
links,
baseNodeIds: [],
filters: { EXTRACTED: true, INFERRED: true, AMBIGUOUS: false },
focusMode: "all",
searchQuery: "机器"
});
assert.deepEqual(snapshot.node_ids, []);
assert.deepEqual(snapshot.nodes, []);
assert.deepEqual(snapshot.links, []);
assert.deepEqual(snapshot.searchIndex, []);
});
});
describe("atlas state contract", () => {
const rawGraph = {
meta: { wiki_title: "测试知识库", build_date: "2026-04-27" },
nodes: [
{ id: "a", label: "知识编译", type: "topic", community: "method", confidence: "EXTRACTED", content: "# 知识编译\n\n整理一次,持续维护。" },
{ id: "b", label: "素材消化", type: "topic", community: "method", confidence: "INFERRED", source_path: "wiki/topics/b.md" },
{ id: "c", label: "网页文章", type: "source", community: "source", confidence: "AMBIGUOUS" }
],
edges: [
{ id: "ab", from: "a", to: "b", type: "EXTRACTED", weight: 0.9 },
{ id: "ac", from: "a", to: "c", type: "INFERRED", weight: 0.6 }
],
learning: {
entry: { recommended_start_node_id: "a" },
communities: [
{ id: "method", label: "方法论", node_count: 2, is_primary: true, recommended_start_node_id: "a" },
{ id: "source", label: "素材来源", node_count: 1, recommended_start_node_id: "c" }
]
}
};
it("normalizes raw graph into one atlas model", () => {
const model = buildAtlasModel(rawGraph);
assert.equal(model.meta.wiki_title, "测试知识库");
assert.equal(model.nodes.length, 3);
assert.equal(model.edges.length, 2);
assert.equal(model.byId.a.degree, 2);
assert.equal(model.byId.a.summary, "整理一次,持续维护。");
assert.deepEqual(model.communities.map((community) => community.label), ["方法论", "素材来源"]);
assert.equal(model.starts[0].node.id, "a");
});
it("treats null atlas coordinates as missing layout input", () => {
const model = buildAtlasModel({
nodes: [
{ id: "nullish", label: "Nullish", x: null, y: null },
{ id: "origin", label: "Origin", x: 0, y: 0 }
],
edges: []
});
deriveAtlasLayout(model);
assert.notDeepEqual(
{ x: model.byId.nullish.x, y: model.byId.nullish.y },
{ x: 5, y: 8 }
);
assert.deepEqual(
{ x: model.byId.origin.x, y: model.byId.origin.y },
{ x: 5, y: 8 }
);
});
it("uses one visible snapshot for filters, search, density, and starts", () => {
const model = buildAtlasModel(rawGraph);
const layout = deriveAtlasLayout(model);
const snapshot = resolveAtlasVisibleSnapshot(model, layout, {
activeCommunityId: "method",
focusMode: "all",
query: "素材",
selectedNodeId: "a",
filters: { EXTRACTED: true, INFERRED: true }
});
assert.deepEqual(snapshot.node_ids, ["b"]);
assert.deepEqual(snapshot.nodes.map((node) => node.id), ["b"]);
assert.deepEqual(snapshot.edges, []);
assert.equal(snapshot.densityMode, "card");
assert.equal(snapshot.starts[0].node.id, "b");
assert.equal(snapshot.importantNodeIds.b, true);
assert.equal(snapshot.counts.total_nodes, 3);
});
it("keeps recommended starts and high-priority nodes readable as atlas index slips", () => {
const model = buildAtlasModel(rawGraph);
const layout = deriveAtlasLayout(model);
const snapshot = resolveAtlasVisibleSnapshot(model, layout, {
activeCommunityId: "all",
focusMode: "all",
query: "",
selectedNodeId: null,
filters: { EXTRACTED: true, INFERRED: true, AMBIGUOUS: true, UNVERIFIED: true }
});
assert.equal(snapshot.starts[0].node.id, "a");
assert.equal(snapshot.startNodeIds.a, true);
assert.equal(snapshot.importantNodeIds.a, true);
assert.equal(snapshot.labelNodeIds.a, true);
});
it("preserves only explicit selections inside the current visible atlas range", () => {
const model = buildAtlasModel(rawGraph);
const layout = deriveAtlasLayout(model);
const methodSnapshot = resolveAtlasVisibleSnapshot(model, layout, {
activeCommunityId: "source",
focusMode: "all",
query: "",
selectedNodeId: "a",
filters: { EXTRACTED: true, INFERRED: true, AMBIGUOUS: true, UNVERIFIED: true }
});
const emptySnapshot = resolveAtlasVisibleSnapshot(model, layout, {
activeCommunityId: "source",
focusMode: "all",
query: "没有结果",
selectedNodeId: "c",
filters: { EXTRACTED: true, INFERRED: true, AMBIGUOUS: true, UNVERIFIED: true }
});
assert.equal(resolveAtlasSelectedNodeId(model, methodSnapshot, "a"), null);
assert.equal(resolveAtlasSelectedNodeId(model, methodSnapshot, "c"), "c");
assert.equal(resolveAtlasSelectedNodeId(model, emptySnapshot, "c"), null);
});
it("does not auto-select a recommended start on first open", () => {
const model = buildAtlasModel(rawGraph);
const layout = deriveAtlasLayout(model);
const snapshot = resolveAtlasVisibleSnapshot(model, layout, {
activeCommunityId: "all",
focusMode: "all",
query: "",
selectedNodeId: null,
filters: { EXTRACTED: true, INFERRED: true, AMBIGUOUS: true, UNVERIFIED: true }
});
assert.equal(snapshot.starts[0].node.id, "a");
assert.equal(resolveAtlasSelectedNodeId(model, snapshot, null), null);
});
it("selects density mode by visible node budget", () => {
assert.equal(getAtlasDensityMode(50), "card");
assert.equal(getAtlasDensityMode(120), "compact-card");
assert.equal(getAtlasDensityMode(300), "point-plus-focus");
assert.equal(getAtlasDensityMode(800), "overview");
});
it("derives one model coordinate space for nodes and bounds", () => {
const model = buildAtlasModel(rawGraph);
deriveAtlasLayout(model);
const point = atlasNodePoint(model.byId.a);
const bounds = getAtlasModelBounds(model.nodes, 0);
assert.equal(point.x, model.byId.a.x * 10);
assert.equal(point.y, model.byId.a.y * 6.8);
assert.ok(bounds.width > 0);
assert.ok(bounds.height > 0);
assert.ok(bounds.minX <= point.x && point.x <= bounds.maxX);
assert.ok(bounds.minY <= point.y && point.y <= bounds.maxY);
});
it("fits, zooms, and reports the current viewport rectangle", () => {
const viewportSize = { width: 1000, height: 680 };
const bounds = { minX: 250, minY: 180, maxX: 750, maxY: 500, width: 500, height: 320 };
const fitted = fitAtlasViewport(bounds, viewportSize, { padding: 0.8 });
const zoomed = zoomAtlasViewport(fitted, 1.5, { x: 500, y: 340 }, viewportSize);
const rect = atlasViewportRect(zoomed, viewportSize);
assert.ok(fitted.scale > 1);
assert.ok(zoomed.scale > fitted.scale);
assert.ok(rect.width < 1000);
assert.ok(rect.height < 680);
assert.ok(rect.minX >= 0 && rect.maxX <= 1000);
assert.ok(rect.minY >= 0 && rect.maxY <= 680);
});
it("centers viewport on model points and maps minimap clicks back to atlas coordinates", () => {
const viewportSize = { width: 800, height: 500 };
const point = { x: 250, y: 170 };
const centered = centerAtlasViewportOnPoint(point, viewportSize, 1.4);
const rect = atlasViewportRect(centered, viewportSize);
const miniPoint = atlasPointToMinimap(point);
const restored = minimapPointToAtlasPoint(miniPoint);
const miniRect = atlasViewportToMinimapRect(centered, viewportSize);
assert.ok(rect.minX <= point.x && point.x <= rect.maxX);
assert.ok(rect.minY <= point.y && point.y <= rect.maxY);
assert.deepEqual(restored, point);
assert.ok(miniRect.width > 0);
assert.ok(miniRect.height > 0);
});
});
+48
View File
@@ -0,0 +1,48 @@
const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const { scanWiki } = require("../../scripts/source-signal-coverage");
const path = require("path");
const FIXTURE_ROOT = path.join(__dirname, "..", "fixtures", "coverage-sample-wiki");
describe("scanWiki", () => {
it("returns correct summary counts", () => {
const { summary } = scanWiki(FIXTURE_ROOT);
assert.equal(summary.ok, 2);
assert.equal(summary.missing_sources, 1);
assert.equal(summary.empty_sources, 1);
assert.equal(summary.invalid_sources, 1);
assert.equal(summary.not_applicable, 2);
assert.equal(summary.applicable_total, 5);
});
it("returns pages for all scanned files", () => {
const { pages } = scanWiki(FIXTURE_ROOT);
assert.equal(pages.length, 7);
});
it("marks synthesis as not_applicable", () => {
const { pages } = scanWiki(FIXTURE_ROOT);
const crystal = pages.find((p) => p.id === "Crystal");
assert.equal(crystal.reason, "not_applicable");
});
it("marks query as not_applicable", () => {
const { pages } = scanWiki(FIXTURE_ROOT);
const query = pages.find((p) => p.pageType === "query");
assert.equal(query.reason, "not_applicable");
});
it("detects ok with correct sourceCount", () => {
const { pages } = scanWiki(FIXTURE_ROOT);
const alpha = pages.find((p) => p.id === "Alpha");
assert.equal(alpha.reason, "ok");
assert.equal(alpha.sourceCount, 2);
});
it("detects invalid_sources", () => {
const { pages } = scanWiki(FIXTURE_ROOT);
const delta = pages.find((p) => p.id === "Delta");
assert.equal(delta.reason, "invalid_sources");
});
});
+202
View File
@@ -0,0 +1,202 @@
const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const {
SCAN_KINDS,
extractFrontmatter,
evaluateSourceSignalEligibility,
parseSourcesFrontmatter
} = require("../../scripts/lib/source-signal-eligibility");
describe("extractFrontmatter", () => {
it("returns hasFrontmatter:false when no frontmatter", () => {
const result = extractFrontmatter("just body text");
assert.equal(result.hasFrontmatter, false);
assert.equal(result.frontmatter, "");
assert.equal(result.body, "just body text");
});
it("extracts frontmatter from valid document", () => {
const result = extractFrontmatter("---\ntitle: Test\n---\nbody");
assert.equal(result.hasFrontmatter, true);
assert.equal(result.frontmatter, "title: Test");
assert.equal(result.body, "body");
});
it("returns hasFrontmatter:false for broken frontmatter", () => {
const result = extractFrontmatter("---\ntitle: Test\nno closing");
assert.equal(result.hasFrontmatter, false);
});
});
describe("parseSourcesFrontmatter", () => {
it("returns hasField:false for empty input", () => {
const result = parseSourcesFrontmatter("");
assert.equal(result.hasField, false);
assert.equal(result.parsed, false);
assert.deepEqual(result.sources, []);
});
it("returns hasField:false when no sources key", () => {
const result = parseSourcesFrontmatter("title: Test\nauthor: me");
assert.equal(result.hasField, false);
});
it("parses single string source", () => {
const result = parseSourcesFrontmatter('sources: "paper.md"');
assert.equal(result.hasField, true);
assert.equal(result.parsed, true);
assert.deepEqual(result.sources, ["paper.md"]);
assert.equal(result.signalAvailable, true);
});
it("parses inline array", () => {
const result = parseSourcesFrontmatter('sources: ["a.md", "b.md"]');
assert.equal(result.hasField, true);
assert.deepEqual(result.sources, ["a.md", "b.md"]);
});
it("parses multiline list", () => {
const result = parseSourcesFrontmatter("sources:\n - paper.md\n - note.md");
assert.equal(result.hasField, true);
assert.deepEqual(result.sources, ["note.md", "paper.md"]);
});
it("returns empty_sources for empty array", () => {
const result = parseSourcesFrontmatter("sources: []");
assert.equal(result.hasField, true);
assert.equal(result.parsed, true);
assert.deepEqual(result.sources, []);
assert.equal(result.signalAvailable, false);
});
it("returns empty_sources for whitespace-only token", () => {
const result = parseSourcesFrontmatter('sources:\n - ""');
assert.equal(result.hasField, true);
assert.deepEqual(result.sources, []);
});
it("returns parsed:false for broken inline array", () => {
const result = parseSourcesFrontmatter("sources: [");
assert.equal(result.hasField, true);
assert.equal(result.parsed, false);
assert.deepEqual(result.sources, []);
});
it("returns parsed:false for invalid multiline content", () => {
const result = parseSourcesFrontmatter("sources:\n foo: bar");
assert.equal(result.hasField, true);
assert.equal(result.parsed, false);
});
it("handles numeric sources by stringifying", () => {
const result = parseSourcesFrontmatter("sources: [1, 2]");
assert.equal(result.hasField, true);
assert.equal(result.parsed, true);
assert.deepEqual(result.sources, ["1", "2"]);
});
});
describe("evaluateSourceSignalEligibility", () => {
it("returns not_applicable for synthesis", () => {
const result = evaluateSourceSignalEligibility({
pageType: "synthesis",
frontmatter: "title: Test"
});
assert.equal(result.eligible, false);
assert.equal(result.reason, "not_applicable");
});
it("returns not_applicable for query", () => {
const result = evaluateSourceSignalEligibility({
pageType: "query",
frontmatter: "sources: [a.md]"
});
assert.equal(result.eligible, false);
assert.equal(result.reason, "not_applicable");
});
it("returns not_applicable for unknown page type", () => {
const result = evaluateSourceSignalEligibility({
pageType: "unknown",
frontmatter: "sources: [a.md]"
});
assert.equal(result.eligible, false);
assert.equal(result.reason, "not_applicable");
});
it("returns ok for entity with valid sources", () => {
const result = evaluateSourceSignalEligibility({
pageType: "entity",
frontmatter: "sources:\n - paper.md\n - note.md"
});
assert.equal(result.eligible, true);
assert.equal(result.reason, "ok");
assert.deepEqual(result.sources, ["note.md", "paper.md"]);
});
it("returns ok for topic with single source", () => {
const result = evaluateSourceSignalEligibility({
pageType: "topic",
frontmatter: 'sources: "paper.md"'
});
assert.equal(result.eligible, true);
assert.equal(result.reason, "ok");
});
it("returns missing_sources when no frontmatter", () => {
const result = evaluateSourceSignalEligibility({
pageType: "entity",
frontmatter: ""
});
assert.equal(result.eligible, false);
assert.equal(result.reason, "missing_sources");
});
it("returns missing_sources when no sources field", () => {
const result = evaluateSourceSignalEligibility({
pageType: "entity",
frontmatter: "title: Test"
});
assert.equal(result.eligible, false);
assert.equal(result.reason, "missing_sources");
});
it("returns empty_sources for empty array", () => {
const result = evaluateSourceSignalEligibility({
pageType: "entity",
frontmatter: "sources: []"
});
assert.equal(result.eligible, false);
assert.equal(result.reason, "empty_sources");
});
it("returns invalid_sources for broken syntax", () => {
const result = evaluateSourceSignalEligibility({
pageType: "entity",
frontmatter: "sources: ["
});
assert.equal(result.eligible, false);
assert.equal(result.reason, "invalid_sources");
});
it("treats comparison as applicable", () => {
const result = evaluateSourceSignalEligibility({
pageType: "comparison",
frontmatter: 'sources: "a.md"'
});
assert.equal(result.eligible, true);
assert.equal(result.reason, "ok");
});
});
describe("SCAN_KINDS", () => {
it("includes all 6 page types", () => {
const types = SCAN_KINDS.map((k) => k.pageType).sort();
assert.deepEqual(types, ["comparison", "entity", "query", "source", "synthesis", "topic"]);
});
it("marks entity/topic/source/comparison as applicable", () => {
const applicable = SCAN_KINDS.filter((k) => k.applicable).map((k) => k.pageType).sort();
assert.deepEqual(applicable, ["comparison", "entity", "source", "topic"]);
});
});