Files
fusion/packages/dashboard/app/components/__tests__/KnowledgeGraphPanel.test.tsx
gsxdsm dad726c777 FN-9038: add Knowledge Graph tab to Memory view
Expose knowledge graph data in the Memory view through a localized, resilient dashboard panel.

- Add protected knowledge graph API routes and access helpers.
- Add a Memory view tab, graph panel, loading/error states, and localized labels.
- Document graph navigation and cover access, route, hook, and component behavior.

Files changed:
 .changeset/fn-9038-knowledge-graph-tab.md          |   7 +
 docs/dashboard-guide.md                            |   2 +
 docs/knowledge-graph.md                            |  10 ++
 packages/dashboard/app/api/legacy.ts               |  17 ++-
 packages/dashboard/app/api/system/index.ts         |   1 +
 .../dashboard/app/api/system/knowledge-graph.ts    |  23 +++
 .../app/components/KnowledgeGraphPanel.css         |  35 +++++
 .../app/components/KnowledgeGraphPanel.tsx         |  68 +++++++++
 packages/dashboard/app/components/MemoryView.tsx   |  15 +-
 .../__tests__/KnowledgeGraphPanel.test.tsx         |  49 +++++++
 .../app/components/__tests__/MemoryView.test.tsx   |  13 ++
 .../app/hooks/__tests__/useKnowledgeGraph.test.ts  |  66 +++++++++
 packages/dashboard/app/hooks/useKnowledgeGraph.ts  |  94 ++++++++++++
 .../src/__tests__/knowledge-graph-access.test.ts   |  88 ++++++++++++
 packages/dashboard/src/knowledge-graph-access.ts   | 160 +++++++++++++++++++++
 .../register-knowledge-graph-routes.test.ts        |  75 ++++++++++
 .../src/routes/register-knowledge-routes.ts        | 120 ++++++++++++++++
 packages/i18n/locales/en/app.json                  |  15 +-
 packages/i18n/locales/es/app.json                  |  15 +-
 packages/i18n/locales/fr/app.json                  |  15 +-
 packages/i18n/locales/ko/app.json                  |  15 +-
 packages/i18n/locales/pt-BR/app.json               |  15 +-
 packages/i18n/locales/zh-CN/app.json               |  15 +-
 packages/i18n/locales/zh-TW/app.json               |  15 +-
 24 files changed, 939 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-9038

Fusion-Task-Lineage: 4200e214-ad22-4f92-b980-c9bcb8ffd3b9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-13 16:48:01 -07:00

50 lines
4.6 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { KnowledgeGraphPanel } from "../KnowledgeGraphPanel";
const useKnowledgeGraph = vi.fn();
vi.mock("../../hooks/useKnowledgeGraph.js", () => ({ useKnowledgeGraph: (...args: unknown[]) => useKnowledgeGraph(...args) }));
const selectNode = vi.fn();
const setFilters = vi.fn();
const setNeighborOptions = vi.fn();
const setPathInput = vi.fn();
const node = { id: "symbol:src/a.ts#run", kind: "symbol" as const, name: "run", owner: "file" as const, ownerPath: "src/a.ts", source: { path: "src/a.ts", line: 4, column: 1 }, attributes: {} };
const edge = { id: "e-1", kind: "contains" as const, from: "file:src/a.ts", to: node.id, provenance: "extracted" as const, owner: "file" as const, ownerPath: "src/a.ts", source: { path: "src/a.ts", line: 1, column: 1 }, attributes: {} };
function graph(overrides: Record<string, unknown> = {}) { return { status: { available: true, nodeCount: 2, edgeCount: 1, graphDir: ".fusion-knowledge/graph", nodeKindCounts: { symbol: 1 }, edgeKindCounts: { contains: 1 }, provenanceCounts: { extracted: 1, inferred: 0 }, fnxcAreas: ["Memory"], pathLimits: { defaultMaxHops: 6, maxHops: 10, maxExpansions: 20000 } }, filters: { limit: 50, offset: 0 }, setFilters, nodes: { nodes: [node], total: 1, limit: 50, offset: 0 }, loading: false, selectedId: node.id, detail: { node, outgoing: [edge], incoming: [edge], outgoingTotal: 1, incomingTotal: 1 }, selectNode, neighborOptions: { direction: "both", depth: 1, edgeKinds: [] }, setNeighborOptions, neighborResults: { neighbors: [{ node, distance: 1, edges: [edge] }], total: 1 }, refreshNeighbors: vi.fn(), pathInput: { fromId: "file:src/a.ts", toId: node.id, maxHops: 2 }, setPathInput, pathResult: null, findPath: vi.fn(), rebuild: vi.fn(), rebuilding: false, refreshStatus: vi.fn(), error: null, clearFilters: vi.fn(), ...overrides }; }
describe("KnowledgeGraphPanel", () => {
it("renders status, complete filters, detail provenance, and drills through endpoints", async () => {
useKnowledgeGraph.mockReturnValue(graph());
render(<KnowledgeGraphPanel addToast={vi.fn()} />);
expect(screen.getByText("2 nodes · 1 edges")).toBeInTheDocument();
const symbolKind = screen.getByLabelText("Symbol kind");
expect(symbolKind).toBeInTheDocument();
expect(Array.from((symbolKind as HTMLSelectElement).options).map((option) => option.value)).toEqual([
"", "function", "class", "interface", "type-alias", "enum", "variable", "namespace", "alias",
]);
expect(screen.getByLabelText("Owner")).toBeInTheDocument();
expect(screen.getByLabelText("Result limit")).toBeInTheDocument();
expect(screen.getAllByText("extracted").length).toBeGreaterThan(0);
await userEvent.click(screen.getAllByRole("button", { name: node.id })[0]);
expect(selectNode).toHaveBeenCalledWith(node.id);
});
it("distinguishes unavailable graph, empty search, and all path outcomes", () => {
useKnowledgeGraph.mockReturnValue(graph({ status: { ...graph().status, available: false, recoveryReason: "missing-artifact" } }));
const { rerender } = render(<KnowledgeGraphPanel addToast={vi.fn()} />);
expect(screen.getByTestId("knowledge-graph-no-graph")).toHaveTextContent("missing-artifact");
useKnowledgeGraph.mockReturnValue(graph({ status: null, error: "Request failed" }));
rerender(<KnowledgeGraphPanel addToast={vi.fn()} />);
expect(screen.getByTestId("knowledge-graph-error")).toHaveTextContent("Request failed");
useKnowledgeGraph.mockReturnValue(graph({ nodes: { nodes: [], total: 0, limit: 50, offset: 0 } }));
rerender(<KnowledgeGraphPanel addToast={vi.fn()} />);
expect(screen.getByTestId("knowledge-graph-no-results")).toBeInTheDocument();
for (const [result, id] of [[{ outcome: "found", path: { nodes: [node], edges: [] }, hops: 0, maxHops: 2, expansions: 1, truncated: false }, "knowledge-graph-path-found"], [{ outcome: "not-found", path: null, maxHops: 2, expansions: 2, truncated: false }, "knowledge-graph-path-not-found"], [{ outcome: "limit-reached", path: null, maxHops: 2, expansions: 2, truncated: true, limit: "max-hops" }, "knowledge-graph-path-limit-reached"]] as const) {
useKnowledgeGraph.mockReturnValue(graph({ pathResult: result })); rerender(<KnowledgeGraphPanel addToast={vi.fn()} />); expect(screen.getByTestId(id)).toBeInTheDocument();
}
useKnowledgeGraph.mockReturnValue(graph({ error: "Unknown graph node" })); rerender(<KnowledgeGraphPanel addToast={vi.fn()} />); expect(screen.getByTestId("knowledge-graph-path-error")).toHaveTextContent("Unknown graph node");
});
});