import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { drawerForExcludedGraphObject, drawerForGraphSelection, drawerForGraphSummaryNode, drawerForUnavailableGraphObject, graphOpenPagePayloadForCommand, graphObjectVisibilityReason, graphSelectionCommandForOpenDetail, graphSelectionCommandForSummaryCommand, } from "../src/lib/graph-summary-actions"; import { closedDrawer, graphCommunitySummaryDrawer } from "../src/lib/drawer-state"; import type { GraphData, GraphSummaryCommand, Selection } from "@llm-wiki/graph-engine"; describe("graph summary actions", () => { it("turns a single node selection into a lightweight node summary drawer", () => { const drawer = drawerForGraphSelection(graphFixture(), nodeSelection(), closedDrawer()); assert.equal(drawer.mode, "graph-node-summary"); assert.equal(drawer.mode === "graph-node-summary" ? drawer.payload.nodeId : null, "a"); assert.equal(drawer.mode === "graph-node-summary" ? drawer.payload.label : null, "Alpha"); assert.deepEqual( drawer.mode === "graph-node-summary" ? drawer.payload.commands.map((command) => command.kind) : [], ["open-detail-read", "select-neighbors", "set-fixed-position", "enter-community"], ); }); it("turns a single community selection into a lightweight community summary drawer", () => { const drawer = drawerForGraphSelection(graphFixture(), communitySelection(), closedDrawer()); assert.equal(drawer.mode, "graph-community-summary"); assert.equal(drawer.mode === "graph-community-summary" ? drawer.payload.communityId : null, "c1"); assert.deepEqual( drawer.mode === "graph-community-summary" ? drawer.payload.commands.map((command) => command.kind) : [], ["enter-community"], ); }); it("keeps manual same-community multi-select as an exact selection instead of widening to the full community", () => { const drawer = drawerForGraphSelection(graphFixtureWithThreeNodeCommunity(), manualSameCommunitySelection(), closedDrawer()); assert.equal(drawer.mode, "graph-selection"); assert.deepEqual(drawer.mode === "graph-selection" ? drawer.selection.nodeIds : [], ["a", "b"]); assert.equal(drawer.mode === "graph-selection" ? drawer.title : null, "选中 2 个节点"); }); it("switches a community core node list click to node summary without entering community", () => { const drawer = drawerForGraphSummaryNode(graphFixture(), "b", communitySummaryDrawer()); assert.equal(drawer.mode, "graph-node-summary"); assert.equal(drawer.mode === "graph-node-summary" ? drawer.payload.nodeId : null, "b"); }); it("keeps open detail/read as an explicit graph-reader payload", () => { const payload = graphOpenPagePayloadForCommand(graphFixture(), { kind: "open-detail-read", nodeId: "a", path: "wiki/a.md", label: "打开详情", }); assert.deepEqual(payload, { path: "wiki/a.md", node: { id: "a", title: "Alpha", type: "topic", typeLabel: "主题", sourcePath: "wiki/a.md", community: "c1", date: null, source: null, isolated: false, }, }); }); it("returns null for graph summary commands that should not open full reading", () => { const command: GraphSummaryCommand = { kind: "enter-community", communityId: "c1", label: "进入社区" }; assert.equal(graphOpenPagePayloadForCommand(graphFixture(), command), null); }); it("turns open detail/read into community focus with the node selected", () => { const command: GraphSummaryCommand = { kind: "open-detail-read", nodeId: "a", path: "wiki/a.md", label: "打开详情", }; assert.deepEqual(graphSelectionCommandForOpenDetail(graphFixture(), command), { id: "c1", nodeId: "a", type: "enter-community-node", }); }); it("turns an ungrouped community selection into a community summary drawer", () => { const drawer = drawerForGraphSelection(graphFixtureWithUngroupedNodes(), ungroupedSelection(), closedDrawer()); assert.equal(drawer.mode, "graph-community-summary"); assert.equal(drawer.mode === "graph-community-summary" ? drawer.payload.communityId : null, "_none"); assert.equal(drawer.mode === "graph-community-summary" ? drawer.payload.canEnterCommunity : null, false); }); it("preserves community free text while refreshing the same community drawer", () => { const current = drawerForGraphSelection(graphFixture(), communitySelection(), closedDrawer()); assert.equal(current.mode, "graph-community-summary"); const withText = current.mode === "graph-community-summary" ? graphCommunitySummaryDrawer(current.payload, "请重点看缺口") : current; const next = drawerForGraphSelection(graphFixture(), communitySelection(), withText); assert.equal(next.mode, "graph-community-summary"); assert.equal(next.mode === "graph-community-summary" ? next.freeText : null, "请重点看缺口"); }); it("classifies selected objects excluded by filters or search without clearing state", () => { const data = graphFixture(); const object = { kind: "node" as const, nodeId: "b" }; const filteredState = { searchQuery: "", searchResultIds: [], typeFilters: { topic: true, entity: false, source: true }, temporaryObject: null, }; const searchedState = { searchQuery: "Alpha", searchResultIds: ["a"], typeFilters: { topic: true, entity: true, source: true }, temporaryObject: null, }; assert.equal(graphObjectVisibilityReason(data, filteredState, object), "filter"); assert.equal(graphObjectVisibilityReason(data, searchedState, object), "search"); const excluded = drawerForExcludedGraphObject(data, object, "filter", closedDrawer(), { selection: { kind: "node", id: "b" }, searchResultIds: ["a"], }); assert.equal(excluded.mode, "graph-excluded-object"); assert.deepEqual( excluded.mode === "graph-excluded-object" ? excluded.payload.commands.map((command) => command.kind) : [], ["show-this-object", "clear-temporary-object-display"], ); const unavailable = drawerForUnavailableGraphObject({ ...data, nodes: data.nodes.filter((node) => node.id !== "b") }, object, "missing-node", closedDrawer()); assert.equal(unavailable.mode, "graph-unavailable-object"); }); it("maps a select-neighbors summary command to a neighbors selection command", () => { assert.deepEqual( graphSelectionCommandForSummaryCommand({ kind: "select-neighbors", nodeId: "a", label: "+邻居" }), { id: "a", type: "neighbors" }, ); assert.equal(graphSelectionCommandForSummaryCommand({ kind: "enter-community", communityId: "alpha", label: "进入社区" }), null); }); }); function nodeSelection(): Selection { return { id: "node:a", nodeIds: ["a"], communityIds: ["c1"], facts: { pageCount: 1, internalLinkCount: 0, communityCount: 1, isolatedCount: 0, }, input: { kind: "node", id: "a" }, actions: [], }; } function communitySelection(): Selection { return { id: "community:a,b", nodeIds: ["a", "b"], communityIds: ["c1"], facts: { pageCount: 2, internalLinkCount: 1, communityCount: 1, isolatedCount: 0, }, input: { kind: "community", id: "c1" }, actions: [], }; } function communitySummaryDrawer() { return drawerForGraphSelection(graphFixture(), communitySelection(), closedDrawer()); } function manualSameCommunitySelection(): Selection { return { id: "nodes:a,b", nodeIds: ["a", "b"], communityIds: ["c1"], facts: { pageCount: 2, internalLinkCount: 1, communityCount: 1, isolatedCount: 0, }, input: { kind: "nodes", ids: ["a", "b"] }, actions: [], }; } function ungroupedSelection(): Selection { return { id: "community:loose-a,loose-b", nodeIds: ["loose-a", "loose-b"], communityIds: ["_none"], facts: { pageCount: 2, internalLinkCount: 0, communityCount: 1, isolatedCount: 2 }, input: { kind: "community", id: "_none" }, actions: [], }; } function graphFixtureWithUngroupedNodes(): GraphData { const base = graphFixture(); return { ...base, nodes: [ ...base.nodes, { id: "loose-a", label: "Loose A", type: "topic", community: null, source_path: "wiki/loose/a.md" }, { id: "loose-b", label: "Loose B", type: "entity", source_path: "wiki/loose/b.md" }, ], }; } function graphFixtureWithThreeNodeCommunity(): GraphData { const base = graphFixture(); return { ...base, meta: { ...base.meta, total_nodes: 3 }, nodes: [ ...base.nodes, { id: "c", label: "Gamma", type: "source", community: "c1", source_path: "wiki/c.md" }, ], learning: base.learning ? { ...base.learning, communities: [ { id: "c1", label: "Community", node_count: 3, color_index: 0, members: ["a", "b", "c"] }, ], } : base.learning, }; } function graphFixture(): GraphData { return { meta: { build_date: "2026-06-18T00:00:00.000Z", wiki_title: "Graph summary action test", total_nodes: 2, total_edges: 1, }, nodes: [ { id: "a", label: "Alpha", type: "topic", community: "c1", source_path: "wiki/a.md" }, { id: "b", label: "Beta", type: "entity", community: "c1", source_path: "wiki/b.md" }, ], edges: [ { id: "a-b", from: "a", to: "b", type: "EXTRACTED", relation_type: "实现", weight: 1 }, ], learning: { version: 1, entry: { recommended_start_node_id: "a", recommended_start_reason: "hub", 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: ["a", "b"], degraded: false }, }, communities: [ { id: "c1", label: "Community", node_count: 2, color_index: 0, members: ["a", "b"] }, ], }, }; }