import { describe, it, expect } from "vitest"; import { SituationKind, SituationStatus, ConfidenceLevel, SituationRelationship, situationNodeSchema, situationEdgeSchema, situationGraphSchema, graphUpdateSchema, startCaseRequestSchema, updateCaseRequestSchema, makeNodeId, makeNode, makeEdge, makeGraph, } from "@/lib/graph/schema.js"; describe("situationNodeSchema", () => { const validNode = { id: "n1", label: "Test Node", description: "A test node", kind: "observation", status: "known", confidence: "high", value: null, unit: null, evidenceIds: [], dependsOn: [], affects: [], parentId: null, childIds: [], }; it("validates a complete valid node", () => { const result = situationNodeSchema.safeParse(validNode); expect(result.success).toBe(true); }); it("requires id", () => { const invalid = { ...validNode, id: "" }; const result = situationNodeSchema.safeParse(invalid); expect(result.success).toBe(false); }); it("requires label", () => { const invalid = { ...validNode, label: "" }; const result = situationNodeSchema.safeParse(invalid); expect(result.success).toBe(false); }); it("rejects invalid kind", () => { const invalid = { ...validNode, kind: "nonexistent" }; const result = situationNodeSchema.safeParse(invalid); expect(result.success).toBe(false); }); it("rejects invalid status", () => { const invalid = { ...validNode, status: "unknown_status" }; const result = situationNodeSchema.safeParse(invalid); expect(result.success).toBe(false); }); it("rejects invalid confidence", () => { const invalid = { ...validNode, confidence: "extreme" }; const result = situationNodeSchema.safeParse(invalid); expect(result.success).toBe(false); }); it("allows numeric value", () => { const node = { ...validNode, value: 42 }; const result = situationNodeSchema.safeParse(node); expect(result.success).toBe(true); }); it("allows string value", () => { const node = { ...validNode, value: "active" }; const result = situationNodeSchema.safeParse(node); expect(result.success).toBe(true); }); }); describe("situationEdgeSchema", () => { const validEdge = { id: "e1", fromNodeId: "n1", toNodeId: "n2", relationship: "supports", confidence: "medium", description: "Edge between nodes", }; it("validates a complete valid edge", () => { const result = situationEdgeSchema.safeParse(validEdge); expect(result.success).toBe(true); }); it("rejects invalid relationship type", () => { const invalid = { ...validEdge, relationship: "invalid_rel" }; const result = situationEdgeSchema.safeParse(invalid); expect(result.success).toBe(false); }); it("validates all relationship types", () => { for (const rel of Object.values(SituationRelationship)) { const edge = { ...validEdge, relationship: rel }; const result = situationEdgeSchema.safeParse(edge); expect(result.success).toBe(true); } }); it("rejects self-referencing edges", () => { // Self-refs are structurally valid but semantically questionable const edge = { ...validEdge, fromNodeId: "n1", toNodeId: "n1" }; const result = situationEdgeSchema.safeParse(edge); expect(result.success).toBe(true); // Structure is valid; semantics checked elsewhere }); }); describe("situationGraphSchema", () => { const validGraph = { centralStatement: "Test graph summary", nodes: [makeNode({ id: "n1", label: "Node 1" })], edges: [], activeUnknownNodeId: null, resolvedNodeIds: [], currentSummary: "Initial summary", }; it("validates a complete valid graph", () => { const result = situationGraphSchema.safeParse(validGraph); expect(result.success).toBe(true); }); it("requires at least one node", () => { const invalid = { ...validGraph, nodes: [] }; const result = situationGraphSchema.safeParse(invalid); expect(result.success).toBe(false); }); it("allows empty edges array", () => { const graph = { ...validGraph, edges: [] }; const result = situationGraphSchema.safeParse(graph); expect(result.success).toBe(true); }); it("rejects missing centralStatement", () => { const invalid = { ...validGraph, centralStatement: "" }; const result = situationGraphSchema.safeParse(invalid); expect(result.success).toBe(false); }); }); describe("graphUpdateSchema", () => { it("validates empty update (no-op proposal)", () => { const result = graphUpdateSchema.safeParse({}); expect(result.success).toBe(true); }); it("validates a complete update", () => { const node = makeNode({ id: "n2", label: "New Node" }); const edge = makeEdge({ fromNodeId: "n1", toNodeId: "n2" }); const result = graphUpdateSchema.safeParse({ addedNodes: [node], updatedNodes: [{ nodeId: "n1", newStatus: "resolved", previousStatus: "unknown", reason: "Question answered" }], addedEdges: [edge], removedEdgeIds: ["e-old"], resolvedUnknownNodeIds: ["n2"], affectedNodeIds: ["n3"], }); expect(result.success).toBe(true); }); it("rejects update with invalid node kind in addedNodes", () => { const invalid = graphUpdateSchema.safeParse({ addedNodes: [{ id: "x", label: "Test", kind: "invalid_kind", description: "test", status: "unknown", confidence: "medium", value: null, unit: null, evidenceIds: [], dependsOn: [], affects: [], parentId: null, childIds: [] }], }); expect(invalid.success).toBe(false); }); }); describe("API request schemas", () => { describe("startCaseRequestSchema", () => { it("validates scenario field", () => { const result = startCaseRequestSchema.safeParse({ scenario: "Test scenario" }); expect(result.success).toBe(true); }); it("rejects empty scenario", () => { const result = startCaseRequestSchema.safeParse({ scenario: "" }); expect(result.success).toBe(false); }); it("rejects scenario over 10000 chars", () => { const longScenario = "a".repeat(10001); const result = startCaseRequestSchema.safeParse({ scenario: longScenario }); expect(result.success).toBe(false); }); it("accepts optional promptVersion", () => { const result = startCaseRequestSchema.safeParse({ scenario: "Test", promptVersion: "v0.3" }); expect(result.success).toBe(true); }); }); describe("updateCaseRequestSchema", () => { it("validates complete update request", () => { const graph = makeGraph({ centralStatement: "Test scenario", nodes: [makeNode({ id: "n1", label: "N" })], currentSummary: "Current state of situation" }); const result = updateCaseRequestSchema.safeParse({ situationGraph: graph, previousQuestion: "What happened?", answer: "This is the answer", }); expect(result.success).toBe(true); }); it("rejects missing situationGraph", () => { const result = updateCaseRequestSchema.safeParse({ previousQuestion: "Q?", answer: "A", }); expect(result.success).toBe(false); }); it("rejects answer over 5000 chars", () => { const graph = makeGraph({ centralStatement: "Test", nodes: [makeNode({ id: "n1", label: "N" })], currentSummary: "Test summary" }); const result = updateCaseRequestSchema.safeParse({ situationGraph: graph, previousQuestion: "Q?", answer: "x".repeat(5001), }); expect(result.success).toBe(false); }); }); }); describe("deterministic ID generation", () => { it("generate consistent IDs for same label", () => { const id1 = makeNodeId("Same Label"); const id2 = makeNodeId("Same Label"); expect(id1).toBe(id2); }); it("generates different IDs for different labels", () => { const id1 = makeNodeId("Label A"); const id2 = makeNodeId("Label B"); expect(id1).not.toBe(id2); }); it("IDs are prefixed with 'n' and short", () => { const id = makeNodeId("A very long label that would produce a longer hash if not truncated"); expect(id.startsWith("n")).toBe(true); expect(id.length).toBeLessThan(15); }); it("same kind of nodes get deterministic IDs", () => { for (let i = 0; i < 10; i++) { expect(makeNodeId("Test Node")).toBe(makeNodeId("Test Node")); } }); }); describe("helper functions", () => { describe("makeNode", () => { it("creates a minimal node with defaults", () => { const node = makeNode({ label: "Minimal" }); const result = situationNodeSchema.safeParse(node); expect(result.success).toBe(true); expect(node.kind).toBe("observation"); expect(node.status).toBe("unknown"); expect(node.confidence).toBe("medium"); }); it("creates a node with custom kind/status", () => { const node = makeNode({ label: "Custom", kind: "metric", status: "known", confidence: "high", value: 42, unit: "count", }); expect(node.kind).toBe("metric"); expect(node.status).toBe("known"); expect(node.confidence).toBe("high"); expect(node.value).toBe(42); expect(node.unit).toBe("count"); }); it("generates ID from label if none provided", () => { const node = makeNode({ label: "Auto-ID" }); expect(node.id.startsWith("n")).toBe(true); }); }); describe("makeEdge", () => { it("creates a minimal edge with defaults", () => { const edge = makeEdge({ fromNodeId: "n1", toNodeId: "n2" }); const result = situationEdgeSchema.safeParse(edge); expect(result.success).toBe(true); }); it("generates description from node ids if not provided", () => { const edge = makeEdge({ fromNodeId: "n-alpha", toNodeId: "n-beta" }); expect(edge.description).toContain("alpha"); expect(edge.description).toContain("beta"); }); }); describe("makeGraph", () => { it("creates a minimal graph with defaults", () => { const graph = makeGraph({ centralStatement: "Test", currentSummary: "Default summary", nodes: [makeNode({ id: "n1", label: "Placeholder" })] }); const result = situationGraphSchema.safeParse(graph); expect(result.success).toBe(true); }); it("allows specifying nodes and edges", () => { const graph = makeGraph({ centralStatement: "Full Graph", currentSummary: "Full summary", nodes: [makeNode({ id: "n1", label: "N1" })], edges: [makeEdge({ fromNodeId: "n1", toNodeId: "n2" })], }); expect(graph.nodes.length).toBe(1); expect(graph.edges.length).toBe(1); }); }); }); describe("enum values completeness", () => { it("SituationKind has all expected values", () => { const expected = ["observation", "reported_claim", "metric", "state", "transition", "relationship", "assumption", "unknown", "conclusion"]; const actual = Object.values(SituationKind); expect(actual).toEqual(expect.arrayContaining(expected)); }); it("SituationStatus has all expected values", () => { const expected = ["known", "unknown", "provisional", "supported", "weakened", "contradicted", "resolved"]; const actual = Object.values(SituationStatus); expect(actual).toEqual(expect.arrayContaining(expected)); }); it("SituationRelationship has all expected values", () => { const expected = ["supports", "weakens", "contradicts", "depends_on", "causes", "may_cause", "measures", "compares_with", "updates", "other"]; const actual = Object.values(SituationRelationship); expect(actual).toEqual(expect.arrayContaining(expected)); }); it("ConfidenceLevel has all expected values", () => { const actual = Object.values(ConfidenceLevel); expect(actual).toContain("low"); expect(actual).toContain("medium"); expect(actual).toContain("high"); }); });