import { beforeEach, describe, expect, it, vi } from "vitest"; const mockUpdateCase = vi.fn(); vi.mock("@/lib/graph/orchestrator.js", () => ({ updateCase: (...args) => mockUpdateCase(...args), })); function makeSuccessResult() { return { success: true, stage: "update_applied", updatedSituationGraph: { centralStatement: "Scenario", nodes: [{ id: "n1" }], edges: [], activeUnknownNodeId: null, resolvedNodeIds: ["n1"], currentSummary: "Updated summary", }, proposal: { addedNodes: [], updatedNodes: [], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: ["n1"], affectedNodeIds: ["n1"], selectedQuestion: null, }, selectedQuestion: null, affectedNodeIds: ["n1"], resolvedUnknownNodeIds: ["n1"], previousActiveUnknownNodeId: "n0", newActiveUnknownNodeId: null, changesApplied: { updatedNodeCount: 1 }, diagnostics: { promptVersion: "v0.4" }, }; } describe("app/api/cases/update route", () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); }); it("valid update returns HTTP 200", async () => { mockUpdateCase.mockResolvedValue(makeSuccessResult()); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({ answer: "A" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(200); }); it("route calls updateCase with applyProposal: true", async () => { mockUpdateCase.mockResolvedValue(makeSuccessResult()); const { POST } = await import("@/app/api/cases/update/route.js"); const body = { situationGraph: {}, previousQuestion: "Q", answer: "A" }; await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify(body), headers: { "content-type": "application/json" }, }), ); expect(mockUpdateCase).toHaveBeenCalledWith(body, { applyProposal: true }); }); it("invalid JSON returns 400", async () => { const { POST } = await import("@/app/api/cases/update/route.js"); const request = { json: vi.fn().mockRejectedValue(new SyntaxError("Unexpected token")), }; const response = await POST(request); expect(response.status).toBe(400); await expect(response.json()).resolves.toMatchObject({ success: false, stage: "request_validation", error: "Invalid JSON request body", }); }); it("request validation failure returns 400", async () => { mockUpdateCase.mockResolvedValue({ success: false, stage: "request_validation", error: "Invalid update-case request", validationErrors: [{ message: "Required" }], }); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({}), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(400); }); it("graph validation failure returns 400", async () => { mockUpdateCase.mockResolvedValue({ success: false, stage: "graph_validation", error: "Invalid situation graph", graphValidationErrors: ["bad graph"], }); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({ answer: "A" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(400); }); it("provider failure returns 502", async () => { mockUpdateCase.mockResolvedValue({ success: false, stage: "provider", error: "Graph update proposal generation failed", providerErrors: ["provider offline"], }); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({ answer: "A" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(502); }); it("proposal validation failure returns 422", async () => { mockUpdateCase.mockResolvedValue({ success: false, stage: "proposal_validation", error: "Invalid graph update proposal", proposalErrors: [{ message: "bad proposal" }], }); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({ answer: "A" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(422); }); it("proposal compatibility failure returns 422", async () => { mockUpdateCase.mockResolvedValue({ success: false, stage: "proposal_compatibility", error: "Update case failed", errors: ["incompatible proposal"], }); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({ answer: "A" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(422); }); it("application failure returns 422", async () => { mockUpdateCase.mockResolvedValue({ success: false, stage: "application", error: "Update case failed", errors: ["could not apply"], }); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({ answer: "A" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(422); }); it("result validation failure returns 500", async () => { mockUpdateCase.mockResolvedValue({ success: false, stage: "result_validation", error: "Update case failed", errors: ["invalid result"], }); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({ answer: "A" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(500); }); it("unknown failure returns 500", async () => { mockUpdateCase.mockRejectedValue(new Error("boom")); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({ answer: "A" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(500); await expect(response.json()).resolves.toMatchObject({ success: false, stage: "internal", error: "Internal server error", }); }); it("success response preserves updated graph fields", async () => { const success = makeSuccessResult(); mockUpdateCase.mockResolvedValue(success); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({ answer: "A" }), headers: { "content-type": "application/json" }, }), ); await expect(response.json()).resolves.toMatchObject({ updatedSituationGraph: success.updatedSituationGraph, proposal: success.proposal, affectedNodeIds: success.affectedNodeIds, resolvedUnknownNodeIds: success.resolvedUnknownNodeIds, previousActiveUnknownNodeId: success.previousActiveUnknownNodeId, newActiveUnknownNodeId: success.newActiveUnknownNodeId, selectedQuestion: success.selectedQuestion, changesApplied: success.changesApplied, diagnostics: success.diagnostics, }); }); it("stack traces and raw provider output are not exposed", async () => { mockUpdateCase.mockResolvedValue({ success: false, stage: "provider", error: "Graph update proposal generation failed", providerErrors: ["provider offline"], rawResponse: "secret", stack: "trace", diagnostics: {}, }); const { POST } = await import("@/app/api/cases/update/route.js"); const response = await POST( new Request("http://localhost/api/cases/update", { method: "POST", body: JSON.stringify({ answer: "A" }), headers: { "content-type": "application/json" }, }), ); const payload = await response.json(); expect(payload).not.toHaveProperty("stack"); expect(payload).not.toHaveProperty("rawResponse"); }); });