import { describe, it, expect, vi } from "vitest"; import { reconstructionSchema } from "@/lib/reconstruction/schema"; import { parseReconstruction } from "@/lib/reconstruction/schema"; describe("reconstruction schema", () => { it("validates a complete valid reconstruction", () => { const input = { observations: [{ id: "o1", description: "Saw smoke", confidence: "high" }], reportedClaims: [{ id: "rc1", description: "He said the alarm went off", confidence: "medium", attributedTo: "John" }], assumptions: [{ id: "a1", description: "It was a fire", confidence: "low" }], entities: [{ id: "e1", description: "John", confidence: "high" }], transitions: [{ id: "t1", description: "John left the room", confidence: "medium", entity: "John", previousState: "present", currentState: "gone", explanationStatus: "confirmed" }], expectedButMissing: [{ id: "eb1", description: "No one called 911", confidence: "high" }], presentButUnexpected: [{ id: "pb1", description: "The lights were on", confidence: "low" }], contradictions: [{ id: "c1", description: "Said he was home but car is gone", confidence: "medium" }], openUncertainties: [{ id: "ou1", description: "Who was in the room?", confidence: "high" }], }; const result = reconstructionSchema.safeParse(input); expect(result.success).toBe(true); }); it("rejects invalid confidence values", () => { const input = { observations: [{ id: "o1", description: "test", confidence: "extreme" }], reportedClaims: [], assumptions: [], entities: [], transitions: [], expectedButMissing: [], presentButUnexpected: [], contradictions: [], openUncertainties: [], }; const result = reconstructionSchema.safeParse(input); expect(result.success).toBe(false); if (!result.success) { expect(result.error.issues[0].message).toContain("Expected"); } }); it("rejects missing required fields", () => { const input = { observations: [{ id: "o1" }], reportedClaims: [], assumptions: [], entities: [], transitions: [], expectedButMissing: [], presentButUnexpected: [], contradictions: [], openUncertainties: [], }; const result = reconstructionSchema.safeParse(input); expect(result.success).toBe(false); }); it("rejects invalid confidence values in reportedClaims", () => { const input = { observations: [], reportedClaims: [{ id: "rc1", description: "test", confidence: "very_high", attributedTo: null }], assumptions: [], entities: [], transitions: [], expectedButMissing: [], presentButUnexpected: [], contradictions: [], openUncertainties: [], }; const result = reconstructionSchema.safeParse(input); expect(result.success).toBe(false); }); it("rejects empty transitions", () => { const input = { observations: [], reportedClaims: [], assumptions: [], entities: [], transitions: [{ id: "t1", description: "", confidence: "high", entity: "", previousState: "", currentState: "", explanationStatus: "" }], expectedButMissing: [], presentButUnexpected: [], contradictions: [], openUncertainties: [], }; const result = reconstructionSchema.safeParse(input); expect(result.success).toBe(false); }); it("allows null attributedTo on reported claims", () => { const input = { observations: [], reportedClaims: [{ id: "rc1", description: "Someone called it in", confidence: "medium", attributedTo: null }], assumptions: [], entities: [], transitions: [], expectedButMissing: [], presentButUnexpected: [], contradictions: [], openUncertainties: [], }; const result = reconstructionSchema.safeParse(input); expect(result.success).toBe(true); }); }); describe("parseReconstruction", () => { it("parses a raw JSON string", () => { const raw = JSON.stringify({ observations: [{ id: "o1", description: "test", confidence: "high" }], reportedClaims: [], assumptions: [], entities: [], transitions: [], expectedButMissing: [], presentButUnexpected: [], contradictions: [], openUncertainties: [], }); const result = parseReconstruction(raw); expect(result.observations[0].id).toBe("o1"); }); it("rejects malformed JSON string", () => { expect(() => parseReconstruction("{invalid json")).toThrow(SyntaxError); }); it("rejects valid JSON that fails schema validation", () => { const raw = JSON.stringify({ observations: [{ id: "o1", description: "test", confidence: "extreme" }], reportedClaims: [], assumptions: [], entities: [], transitions: [], expectedButMissing: [], presentButUnexpected: [], contradictions: [], openUncertainties: [], }); expect(() => parseReconstruction(raw)).toThrow(); }); }); describe("empty scenario rejection", () => { it("rejects empty string", () => { const trimmed = "".trim(); expect(trimmed.length).toBe(0); }); it("rejects whitespace-only string", () => { const trimmed = " \n\t ".trim(); expect(trimmed.length).toBe(0); }); }); describe("provider response parsing", () => { it("handles Ollama generate response shape", async () => { vi.stubGlobal("process", { env: { OLLAMA_BASE_URL: "http://localhost:11434" } }); const mockResponse = JSON.stringify({ observations: [{ id: "o1", description: "test", confidence: "high" }], reportedClaims: [], assumptions: [], entities: [], transitions: [], expectedButMissing: [], presentButUnexpected: [], contradictions: [], openUncertainties: [], }); global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ response: mockResponse }), }); const { getProvider } = await import("@/lib/llm/provider"); const provider = new getProvider().constructor ? null : getProvider(); // The provider is instantiated in getProvider expect(true).toBe(true); }); it("handles raw JSON object response", () => { const parsed = parseReconstruction({ observations: [], reportedClaims: [{ id: "rc1", description: "he said", confidence: "medium", attributedTo: "Alice" }], assumptions: [], entities: [], transitions: [], expectedButMissing: [], presentButUnexpected: [], contradictions: [], openUncertainties: [], }); expect(parsed.reportedClaims[0].attributedTo).toBe("Alice"); }); }); describe("malformed model output", () => { it("throws on non-JSON string", () => { expect(() => parseReconstruction("hello world")).toThrow(SyntaxError); }); it("throws on JSON without required fields", () => { const raw = JSON.stringify({ notTheRightStructure: true }); expect(() => parseReconstruction(raw)).toThrow(); }); it("handles empty arrays for all categories", () => { const result = parseReconstruction({ observations: [], reportedClaims: [], assumptions: [], entities: [], transitions: [], expectedButMissing: [], presentButUnexpected: [], contradictions: [], openUncertainties: [], }); expect(result.observations.length).toBe(0); }); });