Files
fusion/packages/cli/src/__tests__/knowledge-graph-command.test.ts
gsxdsm 27cb2d2621 FN-8921: add deterministic knowledge graph tooling
Add a committable, provenance-tagged knowledge graph layer with CLI generation and query support.

- Extract TypeScript, Markdown, and FNXC knowledge into deterministic graph nodes and edges.
- Persist recoverable graph artifacts outside ignored Fusion state and expose build/query CLI commands.
- Document configuration and add core and CLI coverage for graph structure, serialization, and recovery.

Files changed:
 .changeset/fn-8921-knowledge-graph.md              |   7 +
 .gitattributes                                     |   3 +
 AGENTS.md                                          |   1 +
 docs/README.md                                     |   2 +
 docs/cli-reference.md                              |   4 +
 docs/knowledge-graph.md                            |  37 +++++
 docs/settings-reference.md                         |   4 +
 docs/storage.md                                    |   2 +
 packages/cli/package.json                          |   3 +-
 .../__tests__/knowledge-graph-bundle-shape.test.ts |   4 +
 .../src/__tests__/knowledge-graph-command.test.ts  | 115 ++++++++++++++
 packages/cli/src/bin.ts                            |  19 +++
 packages/cli/src/commands/knowledge-graph.ts       |  79 ++++++++++
 packages/cli/tsup.config.ts                        |   2 +
 packages/core/package.json                         |   4 +-
 packages/core/src/config/settings-schema.ts        |   2 +
 packages/core/src/index.ts                         |   1 +
 .../__tests__/derive-modules.test.ts               |  11 ++
 .../__tests__/extract-file-composition.test.ts     |  20 +++
 .../knowledge-graph/__tests__/extract-fnxc.test.ts |  33 ++++
 .../__tests__/extract-markdown.test.ts             |  21 +++
 .../__tests__/extract-typescript.test.ts           |  30 ++++
 .../__tests__/file-discovery.test.ts               |  26 ++++
 .../graph-artifact-not-gitignored.test.ts          |  15 ++
 .../__tests__/graph-builder-equivalence.test.ts    |  76 ++++++++++
 .../__tests__/graph-builder-incremental.test.ts    |  52 +++++++
 .../__tests__/graph-identity.test.ts               |  11 ++
 .../knowledge-graph/__tests__/graph-query.test.ts  |  13 ++
 .../__tests__/graph-serialization.test.ts          |  29 ++++
 .../__tests__/graph-store-recovery.test.ts         | 106 +++++++++++++
 .../__tests__/resolve-imports.test.ts              |  10 ++
 .../core/src/knowledge-graph/derive-modules.ts     |   4 +
 packages/core/src/knowledge-graph/extract-file.ts  |   6 +
 packages/core/src/knowledge-graph/extract-fnxc.ts  | 168 +++++++++++++++++++++
 .../core/src/knowledge-graph/extract-markdown.ts   |   9 ++
 .../core/src/knowledge-graph/extract-typescript.ts | 107 +++++++++++++
 .../core/src/knowledge-graph/file-discovery.ts     |  85 +++++++++++
 packages/core/src/knowledge-graph/graph-builder.ts | 141 +++++++++++++++++
 .../core/src/knowledge-graph/graph-manifest.ts     |   4 +
 packages/core/src/knowledge-graph/graph-query.ts   | 126 ++++++++++++++++
 .../src/knowledge-graph/graph-serialization.ts     | 134 ++++++++++++++++
 packages/core/src/knowledge-graph/graph-store.ts   |  97 ++++++++++++
 packages/core/src/knowledge-graph/graph-types.ts   |  54 +++++++
 packages/core/src/knowledge-graph/index.ts         |  14 ++
 .../core/src/knowledge-graph/resolve-imports.ts    |   4 +
 packages/core/src/types/settings/settings-scope.ts |   2 +
 pnpm-lock.yaml                                     |  12 +-
 47 files changed, 1700 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-8921

Fusion-Task-Lineage: 7014d0f1-fc47-454b-afe5-5f0d9b229f33

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-10 06:15:17 -07:00

116 lines
3.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const build = vi.hoisted(() => vi.fn().mockResolvedValue({
graph: { nodes: [], edges: [] },
changed: false,
stats: {
parsedFiles: 1,
reusedFiles: 0,
prunedFiles: 0,
addedFiles: 1,
deletedFiles: 0,
synthesizedImportEdges: 2,
derivedModuleCount: 0,
recoveryReason: null,
},
}));
const close = vi.hoisted(() => vi.fn().mockResolvedValue(undefined));
const settings = vi.hoisted(() => vi.fn().mockResolvedValue({ knowledgeGraphDir: ".graph" }));
const resolveProject = vi.hoisted(() => vi.fn().mockResolvedValue({
projectPath: "/project",
store: { getSettings: settings },
}));
class TestLockRetryExhaustedError extends Error {}
vi.mock("@fusion/core", () => ({
buildKnowledgeGraph: build,
resolveKnowledgeGraphDir: (root: string, dir?: string) =>
`${root}/${dir ?? ".fusion-knowledge/graph"}`,
}));
vi.mock("../project-context.js", () => ({
resolveProject,
closeProjectStore: close,
createLocalStore: vi.fn(),
asLocalProjectContext: vi.fn(),
}));
vi.mock("../lock-retry.js", () => ({
retryOnLock: (run: () => unknown) => run(),
LockRetryExhaustedError: TestLockRetryExhaustedError,
}));
const { runKnowledgeGraphBuild } = await import("../commands/knowledge-graph.js");
describe("knowledge graph CLI command", () => {
beforeEach(() => {
build.mockReset().mockResolvedValue({
graph: { nodes: [], edges: [] },
changed: false,
stats: {
parsedFiles: 1,
reusedFiles: 0,
prunedFiles: 0,
addedFiles: 1,
deletedFiles: 0,
synthesizedImportEdges: 2,
derivedModuleCount: 0,
recoveryReason: null,
},
});
close.mockClear();
settings.mockReset().mockResolvedValue({ knowledgeGraphDir: ".graph" });
resolveProject.mockClear();
});
it("uses an explicit directory over settings and emits complete JSON", async () => {
const output = vi.spyOn(console, "log").mockImplementation(() => undefined);
await runKnowledgeGraphBuild({ dir: ".custom", json: true });
expect(build).toHaveBeenCalledWith(expect.objectContaining({ graphDir: "/project/.custom" }));
expect(close).toHaveBeenCalledTimes(1);
expect(JSON.parse(output.mock.calls[0]![0] as string)).toMatchObject({
recoveryReason: null,
synthesizedImportEdges: 2,
changed: false,
});
output.mockRestore();
});
it("uses the setting before the default directory", async () => {
await runKnowledgeGraphBuild();
expect(build).toHaveBeenCalledWith(expect.objectContaining({ graphDir: "/project/.graph" }));
});
it("reports all operator-facing build counters in human output", async () => {
const output = vi.spyOn(console, "log").mockImplementation(() => undefined);
build.mockResolvedValueOnce({
graph: { nodes: [{}, {}], edges: [{}] },
changed: true,
stats: { parsedFiles: 2, reusedFiles: 3, prunedFiles: 4, addedFiles: 2, deletedFiles: 4, synthesizedImportEdges: 5, derivedModuleCount: 6, recoveryReason: "forced" },
});
await runKnowledgeGraphBuild();
expect(output).toHaveBeenCalledWith(expect.stringContaining("pruned 4"));
expect(output).toHaveBeenCalledWith(expect.stringContaining("synthesized imports 5"));
expect(output).toHaveBeenCalledWith(expect.stringContaining("derived modules 6"));
expect(output).toHaveBeenCalledWith(expect.stringContaining("recovery=forced"));
output.mockRestore();
});
it("closes the store when the settings retry is exhausted", async () => {
settings.mockRejectedValueOnce(new TestLockRetryExhaustedError("locked"));
await expect(runKnowledgeGraphBuild()).rejects.toThrow("locked");
expect(close).toHaveBeenCalledTimes(1);
});
it("closes the store when building fails", async () => {
build.mockRejectedValueOnce(new Error("failed"));
await expect(runKnowledgeGraphBuild()).rejects.toThrow("failed");
expect(close).toHaveBeenCalledTimes(1);
});
});