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>
50 lines
4.6 KiB
TypeScript
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");
|
|
});
|
|
});
|