import { describe, expect, it, vi } from "vitest"; // ── Mock domain seam and provider at module level ─────────── const mockSynthesize = vi.fn(); vi.mock("@/lib/graph/current-understanding-synthesis.js", () => ({ synthesizeCurrentUnderstanding: (...args) => mockSynthesize(...args), })); vi.mock("@/lib/llm/provider.js", () => ({ getProvider: () => ({}), getProviderModelName: () => "gpt-5.6-terra", })); // ── Helpers ───────────────────────────────────────────────── function makeValidGraph() { return { centralStatement: "Test situation", nodes: [{ id: "n1", proposition: "Node prop" }], edges: [], }; } function makeRequest(body) { return new Request("http://localhost/api/cases/synthesis", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); } // ── Route tests — valid POST ──────────────────────────────── describe("POST /api/cases/synthesis — valid request", () => { beforeEach(() => mockSynthesize.mockClear()); it("invokes synthesis domain seam with situationGraph + findings", async () => { mockSynthesize.mockResolvedValue({ currentUnderstanding: "Synthesized result" }); const { POST } = await import("@/app/api/cases/synthesis/route.js"); const res = await POST(makeRequest({ situationGraph: makeValidGraph(), findings: [] })); expect(res.status).toBe(200); const data = await res.json(); expect(data.success).toBe(true); expect(data.currentUnderstanding).toBe("Synthesized result"); expect(mockSynthesize).toHaveBeenCalledTimes(1); expect(mockSynthesize.mock.calls[0][1]).toMatchObject({ modelName: "gpt-5.6-terra" }); }); it("returns narrative result on success", async () => { mockSynthesize.mockResolvedValue({ currentUnderstanding: "The revenue dropped because of X and Y." }); const { POST } = await import("@/app/api/cases/synthesis/route.js"); const res = await POST(makeRequest({ situationGraph: makeValidGraph() })); expect(res.status).toBe(200); const data = await res.json(); expect(data.success).toBe(true); expect(typeof data.currentUnderstanding).toBe("string"); expect(data.currentUnderstanding.length).toBeGreaterThan(0); }); it("passes findings to domain seam for eligibility filtering", async () => { mockSynthesize.mockResolvedValue({ currentUnderstanding: "OK" }); const findings = [ { id: "f1", proposition: "agree finding", userDisposition: "agree", evaluation: "considered" }, { id: "f2", proposition: "null finding", userDisposition: null, evaluation: "considered" }, { id: "f3", proposition: "not_quite finding", userDisposition: "not_quite", evaluation: "considered" }, ]; const { POST } = await import("@/app/api/cases/synthesis/route.js"); await POST(makeRequest({ situationGraph: makeValidGraph(), findings })); expect(mockSynthesize).toHaveBeenCalledTimes(1); expect(mockSynthesize.mock.calls[0][0].findings).toHaveLength(3); }); }); // ── Route tests — error handling ──────────────────────────── describe("POST /api/cases/synthesis — error cases", () => { it("missing situationGraph → 400", async () => { const { POST } = await import("@/app/api/cases/synthesis/route.js"); const res = await POST(makeRequest({ findings: [] })); expect(res.status).toBe(400); const data = await res.json(); expect(data.success).toBe(false); expect(data.stage).toBe("request_validation"); }); it("invalid JSON body → 400", async () => { const req = new Request("http://localhost/api/cases/synthesis", { method: "POST", headers: { "Content-Type": "application/json" }, body: "not json", }); const { POST } = await import("@/app/api/cases/synthesis/route.js"); const res = await POST(req); expect(res.status).toBe(400); }); it("null body → 400", async () => { const { POST } = await import("@/app/api/cases/synthesis/route.js"); const res = await POST(makeRequest(null)); expect(res.status).toBe(400); }); it("domain seam throws with statusCode → mapped status", async () => { mockSynthesize.mockRejectedValue(new Error("Provider failed")); // Add statusCode property to the error object after creation const err = Object.assign(new Error("Provider failed"), { statusCode: 502 }); mockSynthesize.mockRejectedValue(err); const { POST } = await import("@/app/api/cases/synthesis/route.js"); const res = await POST(makeRequest({ situationGraph: makeValidGraph() })); expect(res.status).toBe(502); const data = await res.json(); expect(data.success).toBe(false); expect(data.stage).toBe("provider"); }); it("domain seam throws without statusCode → 500", async () => { mockSynthesize.mockRejectedValue(new Error("unknown error")); const { POST } = await import("@/app/api/cases/synthesis/route.js"); const res = await POST(makeRequest({ situationGraph: makeValidGraph() })); expect(res.status).toBe(500); const data = await res.json(); expect(data.success).toBe(false); expect(data.stage).toBe("internal"); }); it("domain seam throws 400 → mapped to 400", async () => { const err = Object.assign(new Error("Invalid input"), { statusCode: 400 }); mockSynthesize.mockRejectedValue(err); const { POST } = await import("@/app/api/cases/synthesis/route.js"); const res = await POST(makeRequest({ situationGraph: makeValidGraph() })); expect(res.status).toBe(400); const data = await res.json(); expect(data.success).toBe(false); expect(data.stage).toBe("request_validation"); }); }); // ── Route thinness — no business logic in route ───────────── describe("Route thinness", () => { beforeEach(() => mockSynthesize.mockClear()); it("route does not filter eligibility itself (domain seam owns it)", async () => { mockSynthesize.mockResolvedValue({ currentUnderstanding: "OK" }); const findings = [ { id: "f1", proposition: "ineligible", userDisposition: "not_quite", evaluation: "considered" }, ]; const { POST } = await import("@/app/api/cases/synthesis/route.js"); await POST(makeRequest({ situationGraph: makeValidGraph(), findings })); // Route passes all findings through — domain seam filters expect(mockSynthesize.mock.calls[0][0].findings).toHaveLength(1); }); it("route does not construct prompts", async () => { mockSynthesize.mockResolvedValue({ currentUnderstanding: "OK" }); const { POST } = await import("@/app/api/cases/synthesis/route.js"); await POST(makeRequest({ situationGraph: makeValidGraph() })); expect(mockSynthesize).toHaveBeenCalledTimes(1); }); it("route does not contain provider logic (delegates to domain seam)", async () => { mockSynthesize.mockResolvedValue({ currentUnderstanding: "OK" }); const { POST } = await import("@/app/api/cases/synthesis/route.js"); await POST(makeRequest({ situationGraph: makeValidGraph() })); expect(mockSynthesize).toHaveBeenCalledTimes(1); }); });