import { describe, expect, it } from "vitest"; import { parseGraphUpdateProposal } from "@/lib/graph/update-proposal.js"; function makeValidProposal(overrides = {}) { return { addedNodes: [], updatedNodes: [ { nodeId: "n-unknown", previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "1.9 complaints per 100 units", reason: "The answer directly provides the normalized complaint rate.", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: ["n-unknown"], affectedNodeIds: [], selectedQuestion: null, ...overrides, }; } describe("parseGraphUpdateProposal", () => { it("parses a valid proposal", () => { const result = parseGraphUpdateProposal(makeValidProposal()); expect(result.success).toBe(true); expect(result.proposal.updatedNodes).toHaveLength(1); }); it("fails on malformed JSON", () => { const result = parseGraphUpdateProposal("{not json"); expect(result.success).toBe(false); }); it("fails when required update content is invalid", () => { const result = parseGraphUpdateProposal({ updatedNodes: [{ nodeId: "n-unknown" }], }); expect(result.success).toBe(false); }); it("removes null array entries and logs them", () => { const result = parseGraphUpdateProposal( JSON.stringify({ ...makeValidProposal(), addedNodes: [null], }), ); expect(result.success).toBe(true); expect(result.proposal.addedNodes).toEqual([]); expect(result.normalisationsApplied).toEqual( expect.arrayContaining([ expect.objectContaining({ change: "Removed null array entry" }), ]), ); }); it("fills missing optional arrays with empty arrays", () => { const result = parseGraphUpdateProposal({ updatedNodes: [], }); expect(result.success).toBe(true); expect(result.proposal.addedNodes).toEqual([]); expect(result.proposal.addedEdges).toEqual([]); expect(result.normalisationsApplied.length).toBeGreaterThan(0); }); it("normalises confirmed enum alias and preserves IDs", () => { const result = parseGraphUpdateProposal({ ...makeValidProposal(), addedNodes: [ { id: "n-new", label: "Reported update", description: "A new reported claim", kind: "reported_statement", status: "supported", confidence: "medium", value: null, unit: null, evidenceIds: [], dependsOn: [], affects: [], parentId: null, childIds: [], }, ], }); expect(result.success).toBe(true); expect(result.proposal.addedNodes[0].kind).toBe("reported_claim"); expect(result.proposal.addedNodes[0].id).toBe("n-new"); }); it("unknown enum values still fail", () => { const result = parseGraphUpdateProposal({ ...makeValidProposal(), addedNodes: [ { id: "n-new", label: "Bad node", description: "Bad node", kind: "unsupported_kind", status: "supported", confidence: "medium", value: null, unit: null, evidenceIds: [], dependsOn: [], affects: [], parentId: null, childIds: [], }, ], }); expect(result.success).toBe(false); }); it("defaults missing selectedQuestion to null", () => { const result = parseGraphUpdateProposal({ addedNodes: [], updatedNodes: [], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [], affectedNodeIds: [], }); expect(result.success).toBe(true); expect(result.proposal.selectedQuestion).toBeNull(); }); it("parses a valid selectedQuestion", () => { const result = parseGraphUpdateProposal( makeValidProposal({ selectedQuestion: { nodeId: "n-follow-up", question: "How should commercial value be defined for this decision?", reason: "A consequential unknown remains unresolved.", }, }), ); expect(result.success).toBe(true); expect(result.proposal.selectedQuestion?.nodeId).toBe("n-follow-up"); }); it("does not invent a next question field outside the contract", () => { const result = parseGraphUpdateProposal(makeValidProposal()); expect(result.proposal.nextQuestion).toBeUndefined(); }); });