Files
2026-07-12 21:26:08 +08:00

1406 lines
49 KiB
TypeScript

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
type GraphData,
type GraphTypeFilters,
type PinMap,
type SelectionInput,
type ThemeId
} from "../src";
import {
createGraphFacadeFromRenderer,
createGraphFacadeRouteManager,
createGraphOfflineCapabilities,
createGraphStandaloneCapabilities,
createGraphWorkbenchCapabilities,
selectionInputForSigmaHit,
type GraphFacadeRenderer,
type GraphFacadeRouteRendererFactoryInput,
type GraphFacadeState
} from "../src/facade";
const DATA: GraphData = {
meta: {
build_date: "2026-06-16",
wiki_title: "Facade test graph",
total_nodes: 2,
total_edges: 1
},
nodes: [
{
id: "a",
label: "Alpha",
type: "topic",
community: "c1",
source_path: "wiki/a.md",
content: "Alpha content"
},
{
id: "b",
label: "Beta",
type: "source",
community: "c1",
source_path: "wiki/b.md",
content: "Beta content"
}
],
edges: [
{
id: "a->b",
from: "a",
to: "b",
type: "EXTRACTED",
confidence: "EXTRACTED",
relation_type: "实现",
weight: 1
}
]
};
describe("GraphFacade", () => {
it("owns the public engine lifecycle around a renderer", async () => {
const container = { dataset: {} as Record<string, string | undefined> };
const renderer = createFakeRenderer();
const engine = createGraphFacadeFromRenderer(container, renderer, {
data: DATA,
theme: "shan-shui"
});
assert.equal(container.dataset.llmWikiGraphEngine, "mounted");
assert.equal(container.dataset.llmWikiGraphTheme, "shan-shui");
engine.setTheme("mo-ye");
assert.equal(container.dataset.llmWikiGraphTheme, "mo-ye");
assert.deepEqual(renderer.calls.at(-1), ["setTheme", "mo-ye"]);
engine.setEdgeStyle({ semanticEmphasis: true, focusHighlight: false });
assert.deepEqual(renderer.calls.at(-1), ["setEdgeStyle", { semanticEmphasis: true, focusHighlight: false }]);
engine.focusNode("wiki/a.md");
assert.equal(container.dataset.llmWikiGraphFocus, "wiki/a.md");
assert.deepEqual(renderer.calls.at(-1), ["focusNode", "wiki/a.md"]);
engine.clearInteraction();
assert.equal(container.dataset.llmWikiGraphFocus, undefined);
assert.deepEqual(renderer.calls.at(-1), ["clearInteraction"]);
assert.equal(engine.setNodeFixed("a", "fix"), true);
assert.deepEqual(renderer.calls.at(-1), ["setNodeFixed", "a", "fix"]);
await engine.applyDiff({ addedNodes: ["c"] });
assert.deepEqual(renderer.calls.at(-1), ["applyDiff", { addedNodes: ["c"] }, undefined]);
engine.destroy();
assert.equal(container.dataset.llmWikiGraphEngine, undefined);
assert.equal(container.dataset.llmWikiGraphTheme, undefined);
assert.equal(renderer.calls.at(-1)?.[0], "destroy");
assert.throws(() => engine.resetView(), /Graph engine has been destroyed/);
});
it("resolves selections against refreshed data", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const renderer = createFakeRenderer();
const nextData: GraphData = {
...DATA,
nodes: DATA.nodes.map((node) => node.id === "a"
? { ...node, community: "c2" }
: node)
};
const engine = createGraphFacadeFromRenderer(container, renderer, {
data: DATA,
theme: "shan-shui"
});
assert.deepEqual(engine.select({ kind: "node", id: "a" }).communityIds, ["c1"]);
engine.setData(nextData);
const selection = engine.select({ kind: "node", id: "a" });
assert.deepEqual(selection.communityIds, ["c2"]);
assert.deepEqual(renderer.calls.at(-2), ["setData", nextData, undefined]);
assert.deepEqual(renderer.calls.at(-1), ["select", { kind: "node", id: "a" }]);
});
it("keeps return global and reset layout as separate facade commands", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const renderer = createFakeRenderer();
const viewResets: number[] = [];
const selectionClears: number[] = [];
const facadeState: GraphFacadeState = { data: DATA, pins: {}, selection: null, temporaryObject: null };
const engine = createGraphFacadeFromRenderer(container, renderer, {
data: DATA,
theme: "shan-shui",
capabilities: {
onSelectionClear: () => selectionClears.push(1),
onViewReset: () => viewResets.push(1)
}
}, facadeState);
const focused = engine.focusCommunity("c1");
assert.equal(container.dataset.llmWikiGraphFocus, "community:c1");
assert.equal(facadeState.selection, null);
assert.deepEqual(focused.communityIds, ["c1"]);
assert.deepEqual(focused.nodeIds, ["a", "b"]);
engine.resetLayout();
assert.equal(container.dataset.llmWikiGraphFocus, "community:c1");
assert.deepEqual(renderer.calls.at(-1), ["resetLayout"]);
assert.deepEqual(viewResets, []);
assert.deepEqual(selectionClears, []);
engine.resetView();
assert.equal(container.dataset.llmWikiGraphFocus, undefined);
assert.equal(facadeState.selection, null);
assert.equal(facadeState.temporaryObject, null);
assert.deepEqual(renderer.calls.at(-1), ["resetView"]);
assert.deepEqual(viewResets, [1]);
assert.deepEqual(selectionClears, []);
});
it("exposes shared summary payloads from current facade data and pins", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const renderer = createFakeRenderer();
const engine = createGraphFacadeFromRenderer(container, renderer, {
data: DATA,
theme: "shan-shui",
pins: {
"wiki/a.md": { x: 10, y: 20, coordinateSpace: "world" }
}
});
const node = engine.summarizeNode("a", {
selection: { kind: "node", id: "a" },
searchResultIds: ["a"]
});
const community = engine.summarizeCommunity("c1", { selection: { kind: "community", id: "c1" } });
const global = engine.summarizeGlobal({ searchResultIds: ["b"] });
const search = engine.summarizeSearchResults("beta", ["b", "missing"]);
const excluded = engine.summarizeExcludedObject({ kind: "node", nodeId: "a" }, "filter", { searchResultIds: ["a"] });
assert.equal(node.kind, "node-summary");
assert.equal(node.nodeId, "a");
assert.equal(node.pinHint.pinned, true);
assert.equal(node.selection.containsCurrentObject, true);
assert.deepEqual(node.commands.map((command) => command.kind), ["open-detail-read", "select-neighbors", "set-fixed-position", "enter-community"]);
assert.equal(community.kind, "community-summary");
assert.equal(community.communityId, "c1");
assert.deepEqual(community.selection.selectedNodeIds, ["a", "b"]);
assert.equal(global.kind, "global-overview");
assert.deepEqual(global.searchResultIds, ["b"]);
assert.equal(search.kind, "search-results");
assert.deepEqual(search.visibleResultIds, ["b"]);
assert.deepEqual(search.unavailableResultIds, ["missing"]);
assert.equal(excluded.kind, "excluded-object");
assert.deepEqual(excluded.commands.map((command) => command.kind), ["show-this-object", "clear-temporary-object-display"]);
engine.setPins({ "wiki/b.md": { x: 1, y: 2, coordinateSpace: "world" } });
const beta = engine.summarizeNode("b");
assert.equal(beta.kind, "node-summary");
assert.equal(beta.pinHint.nodeId, "b");
assert.equal(beta.pinHint.pinned, true);
engine.setData(DATA);
const betaAfterRefresh = engine.summarizeNode("b");
assert.equal(betaAfterRefresh.kind, "node-summary");
assert.equal(betaAfterRefresh.pinHint.pinned, true);
});
it("declares separate workbench, offline, and standalone capability contracts", async () => {
const persistPins = async (_pins: PinMap) => {};
const workbench = createGraphWorkbenchCapabilities({
onOpenPage: () => {},
onSelectionChange: () => {},
onSelectionClear: () => {},
onAsk: () => {},
persistPins,
onDragStateChange: () => {}
});
const offline = createGraphOfflineCapabilities({ persistPins });
const standalone = createGraphStandaloneCapabilities();
assert.equal(workbench.mode, "workbench");
assert.deepEqual(Object.keys(workbench.capabilities || {}).sort(), [
"onAsk",
"onDragStateChange",
"onOpenPage",
"onSelectionChange",
"onSelectionClear",
"onViewReset",
"onVisibilityStateChange",
"persistPins"
]);
assert.equal(offline.mode, "offline");
assert.deepEqual(Object.keys(offline.capabilities || {}), ["persistPins"]);
assert.equal(offline.capabilities?.onOpenPage, undefined);
assert.equal(offline.capabilities?.onSelectionChange, undefined);
assert.equal(offline.capabilities?.onAsk, undefined);
assert.equal(standalone.mode, "standalone");
assert.equal(standalone.capabilities, undefined);
await offline.capabilities?.persistPins?.({});
});
it("routes global Sigma to DOM/SVG community reading and back to global Sigma with facade state", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: { "wiki/a.md": { x: 10, y: 20, coordinateSpace: "world" } },
theme: "shan-shui",
focus: null,
typeFilters: { topic: true, source: true },
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
const sigmaInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const communityInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const smallFallbackInputs: GraphFacadeRouteRendererFactoryInput[] = [];
let overLimitNoticeCount = 0;
const renderers: Array<GraphFacadeRenderer & { calls: unknown[][] }> = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: (input) => {
sigmaInputs.push(input);
return trackRenderer(renderers, "sigma");
},
createDomSvgCommunity: (input) => {
communityInputs.push(input);
return trackRenderer(renderers, "dom-community");
},
createDomSvgSmallFallback: (input) => {
smallFallbackInputs.push(input);
return trackRenderer(renderers, "small-fallback");
},
createOverLimitNotice: () => {
overLimitNoticeCount += 1;
return trackRenderer(renderers, "over-limit-notice");
}
}
});
assert.equal(manager.routeId, "sigma-global");
assert.equal(sigmaInputs.length, 1);
assert.equal(communityInputs.length, 0);
assert.equal(smallFallbackInputs.length, 0);
assert.equal(overLimitNoticeCount, 0);
manager.select({ kind: "node", id: "a" });
manager.setTypeFilters({ topic: true, source: false });
manager.setPins({ "wiki/b.md": { x: 30, y: 40, coordinateSpace: "world" } });
manager.focusCommunity("c1");
assert.equal(manager.routeId, "dom-svg-community");
assert.equal(communityInputs.length, 1);
assert.deepEqual(communityInputs[0].options.focus, { kind: "community", id: "c1" });
assert.deepEqual(communityInputs[0].options.selection, { kind: "node", id: "a" });
assert.deepEqual(communityInputs[0].options.typeFilters, { topic: true, source: false });
assert.deepEqual(Object.keys(communityInputs[0].options.pins), ["wiki/b.md"]);
communityInputs[0].options.callbacks.onVisibilityStateChange?.({
searchQuery: "Alpha",
searchResultIds: ["a"],
typeFilters: { topic: true, source: false },
temporaryObject: null
});
manager.resetView();
assert.equal(manager.routeId, "sigma-global");
assert.equal(sigmaInputs.length, 2);
assert.equal(smallFallbackInputs.length, 0);
assert.equal(overLimitNoticeCount, 0);
assert.deepEqual(sigmaInputs[1].options.focus, null);
assert.deepEqual(sigmaInputs[1].options.selection, { kind: "node", id: "a" });
assert.deepEqual(sigmaInputs[1].options.searchResultIds, ["a"]);
assert.deepEqual(sigmaInputs[1].options.typeFilters, { topic: true, source: false });
assert.deepEqual(Object.keys(sigmaInputs[1].options.pins), ["wiki/b.md"]);
assert.deepEqual(renderers.map((renderer) => renderer.calls.find((call) => call[0] === "destroy")?.[0]).filter(Boolean), [
"destroy",
"destroy"
]);
});
it("keeps edge style on the Sigma route and ignores it on DOM/SVG community routes", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: { topic: true, source: true },
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
const sigmaInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const renderers: Array<GraphFacadeRenderer & { calls: unknown[][] }> = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: (input) => {
sigmaInputs.push(input);
return trackRenderer(renderers, "sigma");
},
createDomSvgCommunity: () => trackRenderer(renderers, "dom-community"),
createDomSvgSmallFallback: () => trackRenderer(renderers, "small-fallback"),
createOverLimitNotice: () => trackRenderer(renderers, "over-limit-notice")
}
});
const baseStyle = { semanticEmphasis: true, focusHighlight: false };
const focusStyle = { semanticEmphasis: true, focusHighlight: true };
manager.setEdgeStyle(baseStyle);
assert.equal(manager.routeId, "sigma-global");
assert.deepEqual(renderers[0].calls.at(-1), ["setEdgeStyle", baseStyle]);
manager.focusCommunity("c1");
const communityRenderer = renderers.at(-1);
const communityCallCount = communityRenderer?.calls.length ?? 0;
manager.setEdgeStyle(focusStyle);
assert.equal(manager.routeId, "dom-svg-community");
assert.equal(communityRenderer?.calls.length, communityCallCount);
manager.resetView();
assert.equal(manager.routeId, "sigma-global");
assert.equal(sigmaInputs.length, 2);
assert.deepEqual(sigmaInputs[1].options.edgeStyle, focusStyle);
});
it("marks route continuity on the stable facade host and clears transition markers", async () => {
const route = createRouteMarkerHarness();
route.expect("sigma-global");
route.expectActiveRendererCount(1);
route.manager.focusCommunity("c1");
route.expect("dom-svg-community", "sigma-global->dom-svg-community");
route.expectActiveRendererCount(1);
await route.expectTransitionCleared();
route.manager.resetView();
route.expect("sigma-global", "dom-svg-community->sigma-global");
route.expectActiveRendererCount(1);
route.manager.destroy();
route.expectDestroyed();
await route.expectTransitionCleared();
route.expectDestroyed();
});
it("marks fallback and over-limit route continuity on the stable facade host", async () => {
const route = createRouteMarkerHarness();
route.expect("sigma-global");
route.sigmaInputs[0].onSigmaUnavailable?.(new Error("WebGL unavailable"));
route.expect("dom-svg-small-fallback", "sigma-global->dom-svg-small-fallback");
route.expectActiveRendererCount(1);
await route.expectTransitionCleared();
route.manager.setData(largeGraphData(2001, 1, 1));
route.expect("over-limit-notice", "dom-svg-small-fallback->over-limit-notice");
route.expectActiveRendererCount(1);
await route.expectTransitionCleared();
route.manager.destroy();
});
it("keeps public resetView active when already on the Sigma global route", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: { kind: "node", id: "a" },
searchResultIds: [],
temporaryObject: null
};
const renderers: Array<GraphFacadeRenderer & { calls: unknown[][] }> = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => trackRenderer(renderers, "sigma"),
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => createFakeRenderer()
}
});
assert.equal(manager.routeId, "sigma-global");
manager.resetView();
assert.deepEqual(renderers[0].calls.at(-1), ["resetView"]);
assert.deepEqual(state.selection, { kind: "node", id: "a" });
});
it("uses global reset to exit Sigma community spotlight without changing routes", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: { "wiki/a.md": { x: 10, y: 20, coordinateSpace: "world" } },
theme: "shan-shui",
focus: null,
typeFilters: { topic: true, source: true },
aggregationMarkers: [],
selection: { kind: "community", id: "c1" },
searchResultIds: ["a"],
temporaryObject: { kind: "community", communityId: "c1" }
};
const renderers: Array<GraphFacadeRenderer & { calls: unknown[][] }> = [];
const selectionClears: number[] = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
callbacks: {
onSelectionClearRequested: () => selectionClears.push(1)
},
factories: {
createSigmaGlobal: () => trackRenderer(renderers, "sigma"),
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => createFakeRenderer()
}
});
manager.resetView();
assert.equal(manager.routeId, "sigma-global");
assert.equal(state.focus, null);
assert.equal(state.selection, null);
assert.equal(state.temporaryObject, null);
assert.deepEqual(state.searchResultIds, ["a"]);
assert.deepEqual(Object.keys(state.pins), ["wiki/a.md"]);
assert.deepEqual(renderers[0].calls.slice(-1), [["resetView"]]);
assert.deepEqual(selectionClears, [1]);
});
it("lets the Sigma toolbar global reset clear the active renderer spotlight", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: { topic: true, source: true },
aggregationMarkers: [],
selection: { kind: "community", id: "c1" },
searchResultIds: [],
temporaryObject: { kind: "community", communityId: "c1" }
};
const sigmaInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const renderers: Array<GraphFacadeRenderer & { calls: unknown[][] }> = [];
const selectionClears: number[] = [];
const viewResets: number[] = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
callbacks: {
onSelectionClearRequested: () => selectionClears.push(1),
onViewReset: () => viewResets.push(1)
},
factories: {
createSigmaGlobal: (input) => {
sigmaInputs.push(input);
return trackRenderer(renderers, "sigma");
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => createFakeRenderer()
}
});
sigmaInputs[0].options.callbacks.onGlobalResetRequested?.();
assert.equal(manager.routeId, "sigma-global");
assert.equal(state.selection, null);
assert.equal(state.temporaryObject, null);
assert.deepEqual(selectionClears, [1]);
assert.deepEqual(viewResets, [1]);
assert.deepEqual(renderers[0].calls.slice(-1), [["resetView"]]);
});
it("returns from DOM/SVG community reading to a plain global map after community selection", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: { "wiki/a.md": { x: 10, y: 20, coordinateSpace: "world" } },
theme: "shan-shui",
focus: null,
typeFilters: { topic: true, source: true },
aggregationMarkers: [],
selection: { kind: "community", id: "c1" },
searchResultIds: ["a"],
temporaryObject: { kind: "community", communityId: "c1" }
};
const sigmaInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const communityInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const selectionClears: number[] = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
callbacks: {
onSelectionClearRequested: () => selectionClears.push(1)
},
factories: {
createSigmaGlobal: (input) => {
sigmaInputs.push(input);
return createFakeRenderer();
},
createDomSvgCommunity: (input) => {
communityInputs.push(input);
return createFakeRenderer();
},
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => createFakeRenderer()
}
});
manager.focusCommunity("c1");
manager.resetView();
assert.equal(manager.routeId, "sigma-global");
assert.equal(state.focus, null);
assert.equal(state.selection, null);
assert.equal(state.temporaryObject, null);
assert.equal(sigmaInputs.length, 2);
assert.deepEqual(sigmaInputs[1].options.selection, null);
assert.deepEqual(sigmaInputs[1].options.searchResultIds, ["a"]);
assert.deepEqual(Object.keys(sigmaInputs[1].options.pins), ["wiki/a.md"]);
assert.deepEqual(selectionClears, [1]);
});
it("lets a DOM/SVG community toolbar request the facade-level global route", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: { "wiki/a.md": { x: 10, y: 20, coordinateSpace: "world" } },
theme: "shan-shui",
focus: null,
typeFilters: { topic: true, source: true },
aggregationMarkers: [],
selection: { kind: "node", id: "a" },
searchResultIds: ["a"],
temporaryObject: null
};
const sigmaInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const communityInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const viewResets: number[] = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
callbacks: {
onViewReset: () => viewResets.push(1)
},
factories: {
createSigmaGlobal: (input) => {
sigmaInputs.push(input);
return createFakeRenderer();
},
createDomSvgCommunity: (input) => {
communityInputs.push(input);
return createFakeRenderer();
},
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => createFakeRenderer()
}
});
manager.focusCommunity("c1");
assert.equal(manager.routeId, "dom-svg-community");
assert.equal(communityInputs.length, 1);
(communityInputs[0].options.callbacks as { onGlobalResetRequested?: () => void }).onGlobalResetRequested?.();
assert.equal(manager.routeId, "sigma-global");
assert.equal(sigmaInputs.length, 2);
assert.equal(state.focus, null);
assert.deepEqual(sigmaInputs[1].options.focus, null);
assert.deepEqual(sigmaInputs[1].options.selection, { kind: "node", id: "a" });
assert.deepEqual(sigmaInputs[1].options.searchResultIds, ["a"]);
assert.deepEqual(sigmaInputs[1].options.typeFilters, { topic: true, source: true });
assert.deepEqual(Object.keys(sigmaInputs[1].options.pins), ["wiki/a.md"]);
assert.deepEqual(viewResets, [1]);
});
it("returns global to DOM/SVG small fallback without retrying a known unavailable Sigma instance", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
let sigmaCreateCount = 0;
const smallFallbackInputs: GraphFacadeRouteRendererFactoryInput[] = [];
let overLimitNoticeCount = 0;
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => {
sigmaCreateCount += 1;
throw new Error("WebGL unavailable");
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: (input) => {
smallFallbackInputs.push(input);
assert.ok(input.options.data.nodes.length <= 2000);
return createFakeRenderer();
},
createOverLimitNotice: () => {
overLimitNoticeCount += 1;
return createFakeRenderer();
}
}
});
assert.equal(manager.routeId, "dom-svg-small-fallback");
assert.equal(manager.sigmaKnownUnavailable, true);
assert.equal(manager.sigmaAttemptCount, 1);
assert.equal(sigmaCreateCount, 1);
assert.equal(smallFallbackInputs.length, 1);
assert.equal(overLimitNoticeCount, 0);
manager.focusCommunity("c1");
assert.equal(manager.routeId, "dom-svg-community");
manager.resetView();
assert.equal(manager.routeId, "dom-svg-small-fallback");
assert.equal(manager.sigmaKnownUnavailable, true);
assert.equal(manager.sigmaAttemptCount, 1);
assert.equal(sigmaCreateCount, 1);
assert.equal(smallFallbackInputs.length, 2);
assert.equal(overLimitNoticeCount, 0);
});
it("returns global to DOM/SVG small fallback from a DOM request with one view reset callback", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
const viewResets: number[] = [];
const communityInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const smallFallbackRenderers: Array<GraphFacadeRenderer & { calls: unknown[][] }> = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
callbacks: {
onViewReset: () => viewResets.push(1)
},
factories: {
createSigmaGlobal: () => {
throw new Error("WebGL unavailable");
},
createDomSvgCommunity: (input) => {
communityInputs.push(input);
return createFakeRenderer();
},
createDomSvgSmallFallback: () => {
const renderer = trackRenderer(smallFallbackRenderers, "small-fallback");
renderer.resetView = () => {
renderer.calls.push(["resetView"]);
viewResets.push(1);
};
return renderer;
},
createOverLimitNotice: () => createFakeRenderer()
}
});
manager.focusCommunity("c1");
assert.equal(manager.routeId, "dom-svg-community");
(communityInputs[0].options.callbacks as { onGlobalResetRequested?: () => void }).onGlobalResetRequested?.();
assert.equal(manager.routeId, "dom-svg-small-fallback");
assert.equal(state.focus, null);
assert.deepEqual(viewResets, [1]);
assert.deepEqual(smallFallbackRenderers.at(-1)?.calls.filter((call) => call[0] === "resetView"), [["resetView"]]);
});
it("updates the current fallback renderer when Sigma is known unavailable and the route stays the same", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
const smallFallbackRenderers: Array<GraphFacadeRenderer & { calls: unknown[][] }> = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => {
throw new Error("WebGL unavailable");
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => trackRenderer(smallFallbackRenderers, "small-fallback"),
createOverLimitNotice: () => createFakeRenderer()
}
});
const nextData = {
...DATA,
meta: { ...DATA.meta, wiki_title: "Facade test graph refreshed" }
};
assert.equal(manager.routeId, "dom-svg-small-fallback");
manager.setData(nextData);
assert.equal(manager.routeId, "dom-svg-small-fallback");
assert.equal(smallFallbackRenderers.length, 1);
assert.deepEqual(smallFallbackRenderers[0].calls.at(-1), ["setData", nextData, undefined]);
});
it("keeps a 2000-node graph eligible for Sigma global rendering", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: largeGraphData(2000, 4000, 500),
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
let sigmaCount = 0;
let overLimitCount = 0;
let domFallbackCount = 0;
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => {
sigmaCount += 1;
return createFakeRenderer();
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => {
domFallbackCount += 1;
return createFakeRenderer();
},
createOverLimitNotice: () => {
overLimitCount += 1;
return createFakeRenderer();
}
}
});
assert.equal(manager.routeId, "sigma-global");
assert.equal(sigmaCount, 1);
assert.equal(overLimitCount, 0);
assert.equal(domFallbackCount, 0);
});
it("routes a 2001-node graph directly to the over-limit notice before Sigma", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: largeGraphData(2001, 1, 1),
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
let sigmaCount = 0;
let overLimitCount = 0;
let domFallbackCount = 0;
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => {
sigmaCount += 1;
return createFakeRenderer();
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => {
domFallbackCount += 1;
return createFakeRenderer();
},
createOverLimitNotice: () => {
overLimitCount += 1;
return createFakeRenderer();
}
}
});
assert.equal(manager.routeId, "over-limit-notice");
assert.equal(sigmaCount, 0);
assert.equal(domFallbackCount, 0);
assert.equal(overLimitCount, 1);
});
it("keeps over-limit reset on the over-limit route", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: largeGraphData(2001, 1, 1),
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: { kind: "community", id: "large-community-0" },
searchResultIds: [],
temporaryObject: null
};
const overLimitRenderers: Array<GraphFacadeRenderer & { calls: unknown[][] }> = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => createFakeRenderer(),
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => trackRenderer(overLimitRenderers, "over-limit-notice")
}
});
assert.equal(manager.routeId, "over-limit-notice");
manager.resetView();
assert.equal(manager.routeId, "over-limit-notice");
assert.deepEqual(overLimitRenderers[0].calls.filter((call) => call[0] === "resetView"), [["resetView"]]);
});
it("routes stale small metadata to over-limit notice using actual node array length", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const staleLargeData = largeGraphData(2001, 1, 1);
staleLargeData.meta.total_nodes = 1;
staleLargeData.meta.total_edges = 1;
const state: GraphFacadeState = {
data: staleLargeData,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
let sigmaCount = 0;
let overLimitCount = 0;
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => {
sigmaCount += 1;
return createFakeRenderer();
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => {
overLimitCount += 1;
return createFakeRenderer();
}
}
});
assert.equal(manager.routeId, "over-limit-notice");
assert.equal(sigmaCount, 0);
assert.equal(overLimitCount, 1);
});
it("re-routes normal global data to over-limit notice when refreshed over the node cap", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
let sigmaCount = 0;
let overLimitCount = 0;
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => {
sigmaCount += 1;
return createFakeRenderer();
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => {
overLimitCount += 1;
return createFakeRenderer();
}
}
});
assert.equal(manager.routeId, "sigma-global");
manager.setData(largeGraphData(2001, 1, 1));
assert.equal(manager.routeId, "over-limit-notice");
assert.equal(container.dataset.llmWikiGraphRoute, "over-limit-notice");
assert.equal(container.dataset.llmWikiGraphRouteTransition, "sigma-global->over-limit-notice");
assert.equal(sigmaCount, 1);
assert.equal(overLimitCount, 1);
});
it("returns from over-limit notice to Sigma global when refreshed back under the node cap", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: largeGraphData(2001, 1, 1),
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
let sigmaCount = 0;
let overLimitCount = 0;
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => {
sigmaCount += 1;
return createFakeRenderer();
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => {
overLimitCount += 1;
return createFakeRenderer();
}
}
});
assert.equal(manager.routeId, "over-limit-notice");
manager.setData(largeGraphData(2000, 1, 1));
assert.equal(manager.routeId, "sigma-global");
assert.equal(overLimitCount, 1);
assert.equal(sigmaCount, 1);
});
it("re-routes known-unavailable small fallback data to over-limit notice when refreshed over the node cap", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
let smallFallbackCount = 0;
let overLimitCount = 0;
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => {
throw new Error("WebGL unavailable");
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => {
smallFallbackCount += 1;
return createFakeRenderer();
},
createOverLimitNotice: () => {
overLimitCount += 1;
return createFakeRenderer();
}
}
});
assert.equal(manager.routeId, "dom-svg-small-fallback");
manager.setData(largeGraphData(2001, 1, 1));
assert.equal(manager.routeId, "over-limit-notice");
assert.equal(smallFallbackCount, 1);
assert.equal(overLimitCount, 1);
});
it("keeps route manager selection state synchronized with renderer callbacks", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: { kind: "node", nodeId: "a" }
};
const selections: SelectionInput[] = [];
const sigmaInputs: GraphFacadeRouteRendererFactoryInput[] = [];
createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
callbacks: {
onSelectionInput: (selection) => selections.push(selection)
},
factories: {
createSigmaGlobal: (input) => {
sigmaInputs.push(input);
return createFakeRenderer();
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => createFakeRenderer()
}
});
sigmaInputs[0].options.callbacks.onSelectionInput?.({ kind: "node", id: "a" });
assert.deepEqual(state.selection, { kind: "node", id: "a" });
assert.deepEqual(selections, [{ kind: "node", id: "a" }]);
sigmaInputs[0].options.callbacks.onSelectionClearRequested?.();
assert.equal(state.selection, null);
assert.equal(state.temporaryObject, null);
});
it("routes over-limit Sigma retry back to the static notice before creating Sigma", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: largeGraphData(2001, 1, 1),
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
let sigmaCount = 0;
let overLimitCount = 0;
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: () => {
sigmaCount += 1;
return createFakeRenderer();
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => createFakeRenderer(),
createOverLimitNotice: () => {
overLimitCount += 1;
return createFakeRenderer();
}
}
});
assert.equal(manager.routeId, "over-limit-notice");
assert.equal(container.dataset.llmWikiGraphRoute, "over-limit-notice");
manager.retrySigma();
assert.equal(manager.routeId, "over-limit-notice");
assert.equal(container.dataset.llmWikiGraphRoute, "over-limit-notice");
assert.equal(container.dataset.llmWikiGraphRouteTransition, undefined);
assert.equal(sigmaCount, 0);
assert.equal(overLimitCount, 2);
});
it("routes abnormal Sigma runtime failures to DOM/SVG small fallback and retries Sigma only on request", () => {
const container = { dataset: {} as Record<string, string | undefined> };
const state: GraphFacadeState = {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
};
let sigmaCreateCount = 0;
let smallFallbackCount = 0;
let overLimitNoticeCount = 0;
const sigmaInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state,
factories: {
createSigmaGlobal: (input) => {
sigmaCreateCount += 1;
sigmaInputs.push(input);
return createFakeRenderer();
},
createDomSvgCommunity: () => createFakeRenderer(),
createDomSvgSmallFallback: () => {
smallFallbackCount += 1;
return createFakeRenderer();
},
createOverLimitNotice: () => {
overLimitNoticeCount += 1;
return createFakeRenderer();
}
}
});
assert.equal(manager.routeId, "sigma-global");
sigmaInputs[0].onSigmaUnavailable?.(new Error("canvas runtime abnormal failure"));
assert.equal(manager.routeId, "dom-svg-small-fallback");
assert.equal(manager.sigmaKnownUnavailable, true);
assert.equal(smallFallbackCount, 1);
assert.equal(overLimitNoticeCount, 0);
assert.equal(sigmaCreateCount, 1);
manager.resetView();
assert.equal(manager.routeId, "dom-svg-small-fallback");
assert.equal(sigmaCreateCount, 1);
assert.equal(smallFallbackCount, 1);
assert.equal(overLimitNoticeCount, 0);
manager.retrySigma();
assert.equal(manager.routeId, "sigma-global");
assert.equal(manager.sigmaKnownUnavailable, false);
assert.equal(sigmaCreateCount, 2);
});
});
describe("selectionInputForSigmaHit", () => {
it("converts additive Sigma node hits into manual multi-node selections", () => {
const data = sigmaHitGraph();
assert.deepEqual(selectionInputForSigmaHit(data, { kind: "node", id: "a1" }, { kind: "node", id: "a2" }, { additive: true }), {
kind: "nodes",
ids: ["a1", "a2"]
});
assert.deepEqual(selectionInputForSigmaHit(data, { kind: "nodes", ids: ["a1", "a2"] }, { kind: "node", id: "a1" }, { additive: true }), {
kind: "node",
id: "a2"
});
assert.equal(selectionInputForSigmaHit(data, { kind: "node", id: "a1" }, { kind: "node", id: "a1" }, { additive: true }), null);
assert.deepEqual(selectionInputForSigmaHit(data, { kind: "node", id: "a1" }, { kind: "node", id: "b1" }, { additive: false }), {
kind: "node",
id: "b1"
});
});
});
function sigmaHitGraph(): GraphData {
return {
meta: { build_date: "2026-06-12", wiki_title: "Sigma hit graph", total_nodes: 4, total_edges: 0 },
nodes: [
{ id: "a1", label: "Alpha 1", type: "topic", community: "alpha", source_path: "wiki/a1.md" },
{ id: "a2", label: "Alpha 2", type: "entity", community: "alpha", source_path: "wiki/a2.md" },
{ id: "a3", label: "Alpha 3", type: "entity", community: "alpha", source_path: "wiki/a3.md" },
{ id: "b1", label: "Beta 1", type: "entity", community: "beta", source_path: "wiki/b1.md" }
],
edges: []
};
}
function createFakeRenderer(): GraphFacadeRenderer & { calls: unknown[][] } {
const calls: unknown[][] = [];
return {
calls,
async applyDiff(diff, options) {
calls.push(["applyDiff", diff, options]);
},
isDragging() {
calls.push(["isDragging"]);
return false;
},
setData(data: GraphData, pins?: PinMap) {
calls.push(["setData", data, pins]);
},
setEdgeStyle(style) {
calls.push(["setEdgeStyle", style]);
},
setAggregationMarkers(markers) {
calls.push(["setAggregationMarkers", markers]);
},
focusNode(path: string) {
calls.push(["focusNode", path]);
},
focusCommunity(id: string) {
calls.push(["focusCommunity", id]);
},
previewNode(id: string | null) {
calls.push(["previewNode", id]);
},
setTypeFilters(filters: GraphTypeFilters) {
calls.push(["setTypeFilters", filters]);
},
showTemporaryObject(object) {
calls.push(["showTemporaryObject", object]);
},
clearTemporaryObjectDisplay() {
calls.push(["clearTemporaryObjectDisplay"]);
},
resetView() {
calls.push(["resetView"]);
},
select(selection: SelectionInput) {
calls.push(["select", selection]);
},
clearSelection() {
calls.push(["clearSelection"]);
},
clearInteraction() {
calls.push(["clearInteraction"]);
},
setNodeFixed(id: string, mode: "fix" | "unfix") {
calls.push(["setNodeFixed", id, mode]);
return true;
},
setTheme(theme: ThemeId) {
calls.push(["setTheme", theme]);
},
setPins(pins: PinMap) {
calls.push(["setPins", pins]);
},
resetLayout() {
calls.push(["resetLayout"]);
},
destroy() {
calls.push(["destroy"]);
}
};
}
function trackRenderer(
renderers: Array<GraphFacadeRenderer & { calls: unknown[][] }>,
route: string
): GraphFacadeRenderer & { calls: unknown[][] } {
const renderer = createFakeRenderer();
renderer.calls.push(["create", route]);
renderers.push(renderer);
return renderer;
}
function assertActiveRendererCount(renderers: Array<GraphFacadeRenderer & { calls: unknown[][] }>, expected: number): void {
const activeCount = renderers.filter((renderer) => !renderer.calls.some((call) => call[0] === "destroy")).length;
assert.equal(activeCount, expected);
}
function createRouteMarkerHarness(): {
container: { dataset: Record<string, string | undefined> };
manager: GraphFacadeRenderer & {
readonly routeId: string;
readonly sigmaKnownUnavailable: boolean;
readonly sigmaAttemptCount: number;
retrySigma(): void;
};
sigmaInputs: GraphFacadeRouteRendererFactoryInput[];
expect(routeId: string, transition?: string): void;
expectActiveRendererCount(expected: number): void;
expectDestroyed(): void;
expectTransitionCleared(): Promise<void>;
} {
const container = { dataset: {} as Record<string, string | undefined> };
const renderers: Array<GraphFacadeRenderer & { calls: unknown[][] }> = [];
const sigmaInputs: GraphFacadeRouteRendererFactoryInput[] = [];
const manager = createGraphFacadeRouteManager(container as unknown as HTMLElement, {
state: {
data: DATA,
pins: {},
theme: "shan-shui",
focus: null,
typeFilters: {},
aggregationMarkers: [],
selection: null,
searchResultIds: [],
temporaryObject: null
},
factories: {
createSigmaGlobal: (input) => {
sigmaInputs.push(input);
return trackRenderer(renderers, "sigma");
},
createDomSvgCommunity: () => trackRenderer(renderers, "community"),
createDomSvgSmallFallback: () => trackRenderer(renderers, "fallback"),
createOverLimitNotice: () => trackRenderer(renderers, "over-limit")
}
});
return {
container,
manager,
sigmaInputs,
expect(routeId, transition) {
assert.equal(container.dataset.llmWikiGraphRoute, routeId);
assert.equal(container.dataset.llmWikiGraphRouteTransition, transition);
},
expectActiveRendererCount(expected) {
assertActiveRendererCount(renderers, expected);
},
expectDestroyed() {
assert.equal(container.dataset.llmWikiGraphRoute, undefined);
assert.equal(container.dataset.llmWikiGraphRouteTransition, undefined);
},
expectTransitionCleared() {
return waitForRouteTransitionClear(container);
}
};
}
async function waitForRouteTransitionClear(container: { dataset: Record<string, string | undefined> }): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 190));
assert.equal(container.dataset.llmWikiGraphRouteTransition, undefined);
}
function largeGraphData(nodeCount: number, edgeCount: number, communitySize: number): GraphData {
const nodes = Array.from({ length: nodeCount }, (_, index) => ({
id: `large-${index}`,
label: `Large ${index}`,
type: "topic",
community: index < communitySize ? "large-community" : `community-${index}`,
source_path: `wiki/large/${index}.md`
}));
const edges = Array.from({ length: edgeCount }, (_, index) => ({
id: `large-edge-${index}`,
from: nodes[index % nodes.length].id,
to: nodes[(index + 1) % nodes.length].id,
type: "EXTRACTED"
}));
return {
meta: {
build_date: "2026-06-19",
wiki_title: "Large graph",
total_nodes: nodeCount,
total_edges: edgeCount
},
nodes,
edges
};
}