import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import GraphologyGraph from "graphology"; import type { GraphRendererAdapterData } from "../src"; import { buildSigmaGlobalGraphologyGraph, canPatchSigmaGlobalGraphAttributes, patchSigmaGlobalGraphAttributes, sigmaGlobalEdgeStyle } from "../src/render/sigma-graphology-model"; describe("Sigma graphology render model", () => { it("builds graphology nodes, edges, communities, and aggregations from adapter data", () => { const adapterData = adapterDataFixture(); const graph = buildSigmaGlobalGraphologyGraph(adapterData, { GraphologyGraph }); assert.equal(graph.order, 2); assert.equal(graph.size, 1); assert.deepEqual(graph.getNodeAttributes("alpha"), { x: 10, y: 20, label: "Alpha", size: 10, color: "#ef4444", type: "circle", graphNodeType: "topic", communityId: "community-a", sourcePath: "alpha.md", selected: true, searchHit: false, pinned: false, communityDimmed: false, communitySpotlightVisible: true, aggregationIds: ["aggregation-a"], labelVisible: true, displayMode: "card", visualRole: "landmark", priority: 900, drawerTarget: { summaryKind: "node-summary", object: { kind: "node", nodeId: "alpha" } } }); assert.equal(graph.getEdgeAttribute("edge-a", "relationType"), "depends-on"); assert.equal(graph.source("edge-a"), "alpha"); assert.equal(graph.target("edge-a"), "beta"); assert.equal(graph.getAttribute("communities")[0].id, "community-a"); assert.equal(graph.getAttribute("aggregations")[0].id, "aggregation-a"); }); it("applies selected-community focus edge styling and semantic emphasis", () => { const adapterData = adapterDataFixture({ selectedCommunityIds: ["community-b"] }); const graph = buildSigmaGlobalGraphologyGraph( adapterData, { GraphologyGraph }, "shan-shui", { semanticEmphasis: true, focusHighlight: true } ); assert.deepEqual(graph.getEdgeAttributes("edge-a"), { size: 0.85, color: "rgba(49, 95, 114, 0.087)", relationType: "depends-on", confidence: "EXTRACTED", weight: 0.75, sourceCommunityId: "community-a", targetCommunityId: "community-a" }); assert.deepEqual( sigmaGlobalEdgeStyle({ relationType: "矛盾", sourceCommunityId: "community-a", targetCommunityId: "community-b", weight: 1 }, "mo-ye"), { color: "rgba(244, 114, 182, 0.66)", size: 2.25 } ); }); it("dims ordinary nodes outside the selected community while keeping priority nodes visible", () => { const graph = buildSigmaGlobalGraphologyGraph(spotlightAdapterData(), { GraphologyGraph }); assert.equal(graph.getNodeAttribute("alpha", "communityDimmed"), false); assert.equal(graph.getNodeAttribute("beta", "communityDimmed"), true); assert.equal(graph.getNodeAttribute("beta", "color"), "rgba(18, 52, 86, 0.2)"); assert.equal(graph.getNodeAttribute("beta", "size"), 3.6); assert.equal(graph.getNodeAttribute("beta-search", "communityDimmed"), false); assert.equal(graph.getNodeAttribute("beta-pinned", "communityDimmed"), false); }); it("detects patch eligibility from graph structure and theme", () => { const adapterData = adapterDataFixture(); const sameShape = adapterDataFixture({ alphaLabel: "Alpha changed" }); const nodeChanged = adapterDataFixture({ alphaId: "alpha-next" }); const edgeChanged = adapterDataFixture({ edgeId: "edge-next" }); const targetChanged = adapterDataFixture({ betaId: "beta-next" }); const nodeAdded = { ...adapterData, nodes: [ ...adapterData.nodes, nodeFixture("gamma", "community-a", { point: { x: 50, y: 60 } }) ] }; const edgeAdded = { ...adapterData, edges: [ ...adapterData.edges, { ...adapterData.edges[0], id: "edge-added" } ] }; assert.equal(canPatchSigmaGlobalGraphAttributes(adapterData, sameShape, "shan-shui", "shan-shui"), true); assert.equal(canPatchSigmaGlobalGraphAttributes(adapterData, sameShape, "shan-shui", "mo-ye"), false); assert.equal(canPatchSigmaGlobalGraphAttributes(adapterData, nodeChanged, "shan-shui", "shan-shui"), false); assert.equal(canPatchSigmaGlobalGraphAttributes(adapterData, edgeChanged, "shan-shui", "shan-shui"), false); assert.equal(canPatchSigmaGlobalGraphAttributes(adapterData, targetChanged, "shan-shui", "shan-shui"), false); assert.equal(canPatchSigmaGlobalGraphAttributes(adapterData, nodeAdded, "shan-shui", "shan-shui"), false); assert.equal(canPatchSigmaGlobalGraphAttributes(adapterData, edgeAdded, "shan-shui", "shan-shui"), false); }); it("patches graph attributes in place", () => { const graph = buildSigmaGlobalGraphologyGraph(adapterDataFixture(), { GraphologyGraph }); const sameShape = adapterDataFixture({ alphaLabel: "Alpha changed", selectedCommunityIds: [] }); patchSigmaGlobalGraphAttributes(graph, sameShape, "shan-shui"); assert.equal(graph.getNodeAttribute("alpha", "label"), "Alpha changed"); assert.equal(graph.getNodeAttribute("beta", "communityDimmed"), false); assert.deepEqual(graph.getAttribute("selection"), sameShape.selection); }); it("keeps the Sigma production model boundary on adapter data, not raw graph data", async () => { const modelSource = await readFile(new URL("../src/render/sigma-graphology-model.ts", import.meta.url), "utf8"); const rendererSource = await readFile(new URL("../src/render/sigma-global-renderer.ts", import.meta.url), "utf8"); assert.match(modelSource, /buildSigmaGlobalGraphologyGraph\(\s*adapterData: GraphRendererAdapterData/); for (const source of [modelSource, rendererSource]) { assert.doesNotMatch(source, /buildGraphRendererAdapterData/); assert.doesNotMatch(source, /GraphData/); assert.doesNotMatch(source, /\bdata\.nodes\b/); assert.doesNotMatch(source, /\bdata\.edges\b/); } }); }); function adapterDataFixture(options: { alphaId?: string; betaId?: string; edgeId?: string; alphaLabel?: string; selectedCommunityIds?: string[]; } = {}): GraphRendererAdapterData { const alphaId = options.alphaId ?? "alpha"; const betaId = options.betaId ?? "beta"; const edgeId = options.edgeId ?? "edge-a"; const selectedCommunityIds = options.selectedCommunityIds ?? ["community-a"]; return { counts: { nodes: 2, edges: 1, communities: 1, hidden: 0, renderedNodes: 2, renderedEdges: 1, aggregationContainers: 1 }, selection: { input: { kind: "community", id: selectedCommunityIds[0] ?? "community-a" }, selectionId: selectedCommunityIds[0] ? `community:${selectedCommunityIds[0]}` : null, selectedNodeIds: [alphaId], selectedCommunityIds, containsCurrentObject: selectedCommunityIds.length > 0 }, nodes: [ nodeFixture(alphaId, "community-a", { label: options.alphaLabel ?? "Alpha", point: { x: 10, y: 20 }, selected: true, priority: 900, displayMode: "card", labelVisible: true }), nodeFixture(betaId, "community-a", { label: "Beta", point: { x: 30, y: 40 }, searchHit: true, pinned: true, type: "source" }) ], edges: [ { id: edgeId, sourceNodeId: alphaId, targetNodeId: betaId, sourceCommunityId: "community-a", targetCommunityId: "community-a", relationType: "depends-on", confidence: "EXTRACTED", weight: 0.75, render: { strokeWidth: 3, opacity: 0.42 } } ], communities: [ { id: "community-a", object: { kind: "community", communityId: "community-a" }, label: "Community A", nodeIds: [alphaId, betaId], nodeCount: 2, selected: selectedCommunityIds.includes("community-a"), searchResultIds: [betaId], pinHints: [pinHint(betaId, true, { x: 30, y: 40 })], aggregationIds: ["aggregation-a"], drawerTarget: communityDrawerTarget("community-a"), commands: [{ kind: "enter-community", communityId: "community-a", label: "进入社区" }] } ], aggregations: [ { id: "aggregation-a", object: { kind: "aggregation", aggregationId: "aggregation-a", nodeIds: [alphaId, betaId], communityId: "community-a" }, label: "Aggregation A", communityId: "community-a", nodeIds: [alphaId, betaId], selectedNodeIds: [alphaId], searchResultIds: [betaId], pinnedNodeIds: [betaId], totalCount: 2, selected: true, pinHints: [pinHint(betaId, true, { x: 30, y: 40 })], drawerTarget: communityDrawerTarget("community-a"), commands: [ { kind: "show-this-object", object: { kind: "aggregation", aggregationId: "aggregation-a", nodeIds: [alphaId, betaId], communityId: "community-a" }, label: "显示这个对象" } ] } ], renderable: { nodes: [], edges: [], communities: [ renderableCommunity("community-a", "#ef4444", true), renderableCommunity("community-b", "#123456", false) ], aggregationContainers: [ { id: "aggregation-a", role: "aggregation-container", label: "Aggregation A", communityId: "community-a", nodeIds: [alphaId, betaId], nodeCount: 2, searchHitCount: 1, pinnedCount: 1, selectedCount: 1, selected: true, searchResultIds: [betaId], pinnedNodeIds: [betaId], selectedNodeIds: [alphaId], pinHints: [pinHint(betaId, true, { x: 30, y: 40 })], point: { x: 20, y: 30 }, x: 20, y: 30, radius: 12, color: "#abcdef" } ], minimap: { path: "", nodes: [] }, relationLegend: [], selectedNodeId: alphaId, selectedCommunityId: selectedCommunityIds[0] ?? null, selectedNodeIds: [alphaId], hiddenNodeIds: new Set(), searchResultIds: [betaId], worldBounds: { minX: 0, maxX: 100, minY: 0, maxY: 100 }, budgets: { limits: { maxNodes: 2, maxEdges: 1, maxLabels: 1, maxCards: 1, maxInteractionUpdates: 3, maxVisibleCommunities: 2 }, usage: { nodes: 2, edges: 1, labels: 1, cards: 1, interactionUpdate: 3, activeInteraction: 3, communities: 2, aggregationContainers: 1 } }, qualityNotice: null, communityFocus: null, communityQuality: { boundaryCertainty: "high", skeletonLabel: "stable", hiddenNodeCount: 0, hiddenEdgeCount: 0, stableCoreNodeIds: [alphaId], stableSkeletonEdgeIds: [edgeId], temporaryBoostNodeIds: [] } } }; } function spotlightAdapterData(): GraphRendererAdapterData { const data = adapterDataFixture({ selectedCommunityIds: ["community-a"] }); data.nodes = [ nodeFixture("alpha", "community-a", { point: { x: 10, y: 20 }, selected: true }), nodeFixture("beta", "community-b", { point: { x: 30, y: 40 } }), nodeFixture("beta-search", "community-b", { point: { x: 35, y: 45 }, searchHit: true }), nodeFixture("beta-pinned", "community-b", { point: { x: 40, y: 50 }, pinned: true }) ]; data.edges = []; return data; } function nodeFixture( id: string, communityId: string, options: { label?: string; point?: { x: number; y: number }; selected?: boolean; searchHit?: boolean; pinned?: boolean; type?: "topic" | "source"; priority?: number; displayMode?: string; labelVisible?: boolean; } = {} ): GraphRendererAdapterData["nodes"][number] { const point = options.point ?? { x: 0, y: 0 }; return { id, object: { kind: "node", nodeId: id }, label: options.label ?? id, type: options.type ?? "topic", communityId, sourcePath: `${id}.md`, point, selected: options.selected ?? false, searchHit: options.searchHit ?? false, pinHint: pinHint(id, options.pinned ?? false, point), aggregationIds: ["aggregation-a"], drawerTarget: { summaryKind: "node-summary", object: { kind: "node", nodeId: id } }, render: { displayMode: options.displayMode ?? "point", visualRole: options.pinned ? "map-pin" : "landmark", priority: options.priority ?? 100, labelVisible: options.labelVisible ?? false } }; } function renderableCommunity(id: string, color: string, selected: boolean) { return { id, role: "community" as const, label: id, nodeCount: 2, selected, searchHitCount: 0, pinnedCount: 0, selectedCount: selected ? 1 : 0, color, x: 0, y: 0, radius: 20, wash: { cx: 0, cy: 0, rx: 20, ry: 20 }, drawerTarget: communityDrawerTarget(id), commands: [{ kind: "enter-community" as const, communityId: id, label: "进入社区" }] }; } function communityDrawerTarget(id: string): GraphRendererAdapterData["communities"][number]["drawerTarget"] { return { summaryKind: "community-summary", object: { kind: "community", communityId: id } }; } function pinHint(nodeId: string, pinned: boolean, point: { x: number; y: number }) { return { nodeId, wikiPath: `${nodeId}.md`, pinned, position: pinned ? { ...point, coordinateSpace: "world" as const } : null }; }