import { describe, it, expect } from "vitest"; import { buildInitialGraph, buildMinimalGraph, describeGraph, } from "@/lib/graph/builder.js"; import { makeNode, situationEdgeSchema, situationGraphSchema, situationNodeSchema, } from "@/lib/graph/schema.js"; // ── Helper: create a v0.3-style reconstruction fixture ─────────── function makeReconstructionFixture() { return { summary: "Company X reports revenue growth but increasing complaints", actors: [ { id: "actor-1", description: "Customer Base", confidence: "high" }, { id: "actor-2", description: "Product Engineering Team", confidence: "high", }, ], systemsOrObjects: [ { id: "sys-1", description: "Production Line A", confidence: "high" }, { id: "sys-2", description: "Quality Control System", confidence: "medium", }, ], expectedStates: [], observedStates: [ { id: "obs-1", description: "Revenue up 15% year-over-year", confidence: "high", }, { id: "obs-2", description: "Customer complaints up 40% year-over-year", confidence: "high", }, ], differences: [ { id: "diff-1", description: "Complaint count grew faster than revenue", confidence: "medium", }, ], knownTransitions: [], unexplainedTransitions: [], contradictions: [ { id: "con-1", description: "Revenue growth vs complaint growth inconsistency", confidence: "high", }, ], importantUnknowns: [ { id: "unk-1", description: "Denominator for complaint rate (customers served)", confidence: "high", }, { id: "unk-2", description: "Root cause of complaint increase", confidence: "medium", }, ], plausibleInterpretations: [], }; } function makeEvidenceFixture() { return [ { id: "ev-1", description: "Annual report data", evidenceType: "direct_observation", confidence: "high", importance: "critical", }, { id: "ev-2", description: "Customer survey results", evidenceType: "reported_statement", confidence: "medium", importance: "important", }, ]; } describe("buildInitialGraph", () => { it("builds nodes from reconstruction data", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: makeEvidenceFixture(), }); expect(result.nodes.length).toBeGreaterThan(0); expect(result.edges.length).toBeGreaterThan(0); }); it("creates a summary node", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const summaryNode = result.nodes.find((n) => n.kind === "state"); expect(summaryNode).toBeDefined(); expect(summaryNode.label).toBe( "Company X reports revenue growth but increasing complaints", ); }); it("creates observation nodes from observedStates", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const observations = result.nodes.filter((n) => n.kind === "observation"); expect(observations.length).toBeGreaterThan(0); }); it("creates unknown nodes from importantUnknowns", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const unknowns = result.nodes.filter((n) => n.kind === "unknown"); expect(unknowns.length).toBe(2); // unk-1 and unk-2 }); it("creates metric nodes from systemsOrObjects", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const metrics = result.nodes.filter((n) => n.kind === "metric"); expect(metrics.length).toBe(2); // sys-1 and sys-2 }); it("creates actor nodes as observations", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const actors = result.nodes.filter( (n) => n.label.includes("Customer Base") || n.label.includes("Product Engineering"), ); expect(actors.length).toBe(2); }); it("creates difference nodes", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const differenceNode = result.nodes.find((n) => n.label.includes("Complaint count grew faster than revenue"), ); expect(differenceNode).toBeDefined(); expect(differenceNode.kind).toBe("relationship"); }); it("creates contradiction nodes", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const contradictionNode = result.nodes.find((n) => n.label.includes("inconsistency"), ); expect(contradictionNode).toBeDefined(); expect(contradictionNode.kind).toBe("relationship"); }); it("creates edges linking observations to summary", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const supportEdges = result.edges.filter( (e) => e.relationship === "supports", ); expect(supportEdges.length).toBeGreaterThan(0); }); it("creates edges linking unknowns to summary as depends_on", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const depEdges = result.edges.filter( (e) => e.relationship === "depends_on", ); expect(depEdges.length).toBe(2); // Two unknown nodes }); it("projects a supplied dependency between reconstruction source IDs", () => { const reconstruction = { ...makeReconstructionFixture(), importantUnknowns: [ { id: "u_problem", description: "Whether a quality problem exists", confidence: "high" }, { id: "u_intervention", description: "Whether inspection is appropriate", confidence: "high" }, ], relationships: [{ id: "r-dependency", fromId: "u_intervention", toId: "u_problem", relationship: "depends_on", description: "Inspection appropriateness depends on the quality problem.", confidence: "high", }], }; const result = buildInitialGraph({ reconstruction, evidence: [] }); const intervention = result.nodes.find((node) => node.label === "Whether inspection is appropriate"); const problem = result.nodes.find((node) => node.label === "Whether a quality problem exists"); expect(result.edges).toContainEqual(expect.objectContaining({ id: "e-rel-r-dependency", fromNodeId: intervention.id, toNodeId: problem.id, relationship: "depends_on", })); }); it("preserves a supplied compares_with relationship type", () => { const reconstruction = { ...makeReconstructionFixture(), observedStates: [ { id: "u_a", description: "Complaint rate before", confidence: "high" }, { id: "u_b", description: "Complaint rate after", confidence: "high" }, ], relationships: [{ id: "r-compare", fromId: "u_a", toId: "u_b", relationship: "compares_with", description: "Compare complaint rates before and after.", confidence: "medium", }], }; const result = buildInitialGraph({ reconstruction, evidence: [] }); expect(result.edges).toContainEqual(expect.objectContaining({ id: "e-rel-r-compare", relationship: "compares_with", })); }); it("skips relationships with invalid source IDs without creating substitute nodes", () => { const reconstruction = { ...makeReconstructionFixture(), relationships: [{ id: "r-missing", fromId: "missing-source", toId: "obs-1", relationship: "depends_on", description: "This must be skipped.", confidence: "low", }], }; expect(() => buildInitialGraph({ reconstruction, evidence: [] })).not.toThrow(); const result = buildInitialGraph({ reconstruction, evidence: [] }); expect(result.edges.find((edge) => edge.id === "e-rel-r-missing")).toBeUndefined(); expect(result.nodes.find((node) => node.label === "missing-source")).toBeUndefined(); }); it("does not infer direct dependencies between related descriptions without relationships", () => { const reconstruction = { ...makeReconstructionFixture(), importantUnknowns: [ { id: "u_problem", description: "Whether a quality problem exists", confidence: "high" }, { id: "u_intervention", description: "Whether inspection is appropriate", confidence: "high" }, ], }; const result = buildInitialGraph({ reconstruction, evidence: [] }); const intervention = result.nodes.find((node) => node.label === "Whether inspection is appropriate"); const problem = result.nodes.find((node) => node.label === "Whether a quality problem exists"); expect(result.edges).not.toContainEqual(expect.objectContaining({ fromNodeId: intervention.id, toNodeId: problem.id, relationship: "depends_on", })); }); it("handles empty observedStates gracefully", () => { const reconstruction = { ...makeReconstructionFixture(), observedStates: [], }; const result = buildInitialGraph({ reconstruction, evidence: [] }); expect(result.nodes.length).toBeGreaterThan(0); // Summary + actors + systems still created }); it("handles missing reconstruction fields gracefully", () => { const result = buildInitialGraph({ reconstruction: { summary: "Minimal" }, evidence: [], }); expect(result.nodes.length).toBeGreaterThan(0); }); it("handles null/undefined reconstruction", () => { const result = buildInitialGraph({ reconstruction: null, evidence: [] }); expect(result.nodes.length).toBe(0); expect(result.edges.length).toBe(0); }); it("handles missing evidence array", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), }); expect(result.nodes.length).toBeGreaterThan(0); expect(result.edges.length).toBeGreaterThan(0); }); it("generates deterministic node IDs for same labels", () => { const r1 = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const r2 = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: [], }); const ids1 = r1.nodes.map((n) => n.id).sort(); const ids2 = r2.nodes.map((n) => n.id).sort(); expect(ids1).toEqual(ids2); }); it("produces valid schema output (no parse errors)", () => { const result = buildInitialGraph({ reconstruction: makeReconstructionFixture(), evidence: makeEvidenceFixture(), }); for (const node of result.nodes) { const parsed = situationNodeSchema.safeParse(node); if (!parsed.success) { console.error(`Invalid node: ${node.id}`, node, parsed.error.message); } expect(parsed.success).toBe(true); } for (const edge of result.edges) { const parsed = situationEdgeSchema.safeParse(edge); if (!parsed.success) { console.error(`Invalid edge: ${edge.id}`, edge, parsed.error.message); } expect(parsed.success).toBe(true); } }); it("creates edges for knownTransitions as transition nodes", () => { const reconstruction = { ...makeReconstructionFixture(), knownTransitions: [ { id: "trans-1", description: "Product shipped v2.0", entity: "Product", previousState: "v1.x", currentState: "v2.0", explanationStatus: "confirmed", confidence: "high", }, ], }; const result = buildInitialGraph({ reconstruction, evidence: [] }); const transitions = result.nodes.filter((n) => n.kind === "transition"); expect(transitions.length).toBe(1); }); it("creates nodes for unexplainedTransitions", () => { const reconstruction = { ...makeReconstructionFixture(), unexplainedTransitions: [ { id: "ut-1", description: "Support wait time increased", entity: "Support", previousState: "2hr", currentState: "8hr", confidence: "medium", }, ], }; const result = buildInitialGraph({ reconstruction, evidence: [] }); expect(result.nodes.length).toBeGreaterThan(0); }); it("creates nodes for plausibleInterpretations as assumptions", () => { const reconstruction = { ...makeReconstructionFixture(), plausibleInterpretations: [ { id: "interp-1", description: "Quality degradation hypothesis", supportingEvidenceIds: ["ev-2"], assumptionsRequired: [], confidence: "medium", }, ], }; const result = buildInitialGraph({ reconstruction, evidence: [] }); const assumptions = result.nodes.filter((n) => n.kind === "assumption"); expect(assumptions.length).toBe(1); }); it("links evidence to observation nodes", () => { const reconstruction = makeReconstructionFixture(); const evidence = [{ id: "ev-1", description: "Test evidence" }]; // Add a mapping from observed states to evidence IDs would require modification // For now, just verify the nodes have empty evidenceIds (as per current implementation) const result = buildInitialGraph({ reconstruction, evidence }); for (const node of result.nodes) { expect(Array.isArray(node.evidenceIds)).toBe(true); } }); it("handles very large reconstruction without errors", () => { const actors = Array.from({ length: 20 }, (_, i) => ({ id: `actor-${i}`, description: `Actor ${i}`, confidence: "high", })); const result = buildInitialGraph({ reconstruction: { ...makeReconstructionFixture(), actors }, evidence: [], }); expect(result.nodes.length).toBeGreaterThan(10); }); it("handles transition with confirmed explanation", () => { const reconstruction = { ...makeReconstructionFixture(), knownTransitions: [ { id: "t-confirmed", description: "Confirmed event", entity: "E1", previousState: "s1", currentState: "s2", explanationStatus: "confirmed", confidence: "high", }, ], }; const result = buildInitialGraph({ reconstruction, evidence: [] }); const confirmedTransitions = result.nodes.filter( (n) => n.kind === "transition" && n.status === "known", ); expect(confirmedTransitions.length).toBe(1); }); }); describe("buildMinimalGraph", () => { it("creates a single node with scenario text as label", () => { const graph = buildMinimalGraph( "This is a test scenario for minimal graph creation", ); expect(graph.nodes.length).toBe(1); expect(graph.edges.length).toBe(0); }); it("truncates label to 80 chars", () => { const longScenario = "a".repeat(200); const graph = buildMinimalGraph(longScenario); expect(graph.nodes[0].label.length).toBeLessThanOrEqual(80); }); it("creates provisional state node", () => { const graph = buildMinimalGraph("Test scenario"); expect(graph.nodes[0].kind).toBe("state"); expect(graph.nodes[0].status).toBe("provisional"); expect(graph.nodes[0].confidence).toBe("low"); }); it("uses first 200 chars of scenario for description", () => { const graph = buildMinimalGraph( "This is a test scenario for minimal graph creation", ); expect(graph.nodes[0].description).toContain("Initial situation from:"); }); it("creates deterministic ID via situationNodeSchema.parse", () => { const graph = buildMinimalGraph("Test scenario"); // Node has explicit id "n0" from the builder, not makeNodeId expect(graph.nodes[0].id).toBe("n0"); }); it("creates minimal valid structure", () => { const graph = buildMinimalGraph("Test"); expect(graph.nodes).toHaveLength(1); expect(graph.edges).toHaveLength(0); expect(graph.nodes[0].evidenceIds).toEqual([]); expect(graph.nodes[0].dependsOn).toEqual([]); expect(graph.nodes[0].affects).toEqual([]); }); }); describe("describeGraph", () => { it("returns summary string with node count by kind", () => { const graph = buildMinimalGraph("Test"); const description = describeGraph(graph); expect(description).toContain("Nodes:"); expect(description).toContain("Edges:"); expect(description).toContain("Unknowns:"); }); it("shows correct edge count", () => { const graph = buildMinimalGraph("Test"); const description = describeGraph(graph); expect(description).toContain("Edges: 0 total"); }); it("counts unresolved unknowns", () => { const n1 = makeNode({ id: "n-unk", label: "Unknown", kind: "unknown", status: "unknown", }); const graph = situationGraphSchema.parse({ centralStatement: "Test", nodes: [n1], edges: [], activeUnknownNodeId: n1.id, resolvedNodeIds: [], currentSummary: "Test", }); const description = describeGraph(graph); expect(description).toContain("1"); // One unresolved unknown }); it("groups nodes by kind in output", () => { const graph = buildMinimalGraph("Test"); const description = describeGraph(graph); expect(description).toContain("1 state"); }); });