import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const mockStartCase = vi.fn(); let warnSpy; let errorSpy; vi.mock("@/lib/graph/orchestrator.js", () => ({ startCase: (...args) => mockStartCase(...args), })); vi.mock("@/lib/supabase/api-auth.js", () => ({ withAuthenticatedApi: (handler) => handler, })); describe("app/api/cases/start route", () => { beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); }); afterEach(() => { warnSpy.mockRestore(); errorSpy.mockRestore(); }); it("delegates request body to the orchestrator", async () => { mockStartCase.mockResolvedValue({ success: true, situationGraph: { nodes: [{ id: "n1" }], edges: [] }, selectedQuestion: null, diagnostics: {}, }); const { POST } = await import("@/app/api/cases/start/route.js"); const request = new Request("http://localhost/api/cases/start", { method: "POST", body: JSON.stringify({ scenario: "Scenario text" }), headers: { "content-type": "application/json" }, }); await POST(request); expect(mockStartCase).toHaveBeenCalledWith({ scenario: "Scenario text" }); expect(mockStartCase.mock.calls[0][0]).not.toHaveProperty("provider"); expect(mockStartCase.mock.calls[0][0]).not.toHaveProperty("modelName"); }); it("returns 200 on success", async () => { const reconstruction = { summary: "Validated reconstruction summary", relationships: [ { id: "r1", fromId: "u-intervention", toId: "u-problem", relationship: "depends_on", description: "The intervention depends on the unresolved problem.", confidence: "high", }, ], }; mockStartCase.mockResolvedValue({ success: true, reconstruction, situationGraph: { nodes: [{ id: "n1" }], edges: [ { id: "e-unrelated", fromNodeId: "n1", toNodeId: "n1", relationship: "supports", }, ], }, selectedQuestion: null, diagnostics: {}, }); const { POST } = await import("@/app/api/cases/start/route.js"); const response = await POST( new Request("http://localhost/api/cases/start", { method: "POST", body: JSON.stringify({ scenario: "Scenario text" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(200); await expect(response.json()).resolves.toMatchObject({ success: true, reconstruction, }); expect(warnSpy).not.toHaveBeenCalled(); expect(errorSpy).not.toHaveBeenCalled(); }); it("returns 400 for invalid request input", async () => { mockStartCase.mockResolvedValue({ success: false, error: "Invalid start-case request", validationErrors: [{ message: "Required" }], statusCode: 400, }); const { POST } = await import("@/app/api/cases/start/route.js"); const response = await POST( new Request("http://localhost/api/cases/start", { method: "POST", body: JSON.stringify({}), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(400); await expect(response.json()).resolves.toMatchObject({ success: false, error: "Invalid start-case request", }); expect(warnSpy).toHaveBeenCalledTimes(1); expect(warnSpy).toHaveBeenCalledWith( "[api/cases/start] error response", expect.objectContaining({ status: 400, error: "Invalid start-case request" }), ); expect(errorSpy).not.toHaveBeenCalled(); }); it("returns a sanitized 503 when the provider is unavailable", async () => { mockStartCase.mockResolvedValue({ success: false, code: "PROVIDER_UNAVAILABLE", error: "Ollama /api/generate request timed out after 5 minutes", providerApiPath: "/api/generate", providerExecution: { generateRequestAttempted: true }, }); const { POST } = await import("@/app/api/cases/start/route.js"); const response = await POST( new Request("http://localhost/api/cases/start", { method: "POST", body: JSON.stringify({ scenario: "Scenario text" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(503); const body = await response.json(); expect(body).toEqual({ success: false, error: "Reasoning service is temporarily unavailable.", }); expect(JSON.stringify(body)).not.toMatch(/ollama|generate|timed out/i); }); it("returns provider/internal failures as 5xx without stack traces", async () => { const rawResponse = `{"reconstruction":{"observedStates":[{"id":"obs-1"${"x".repeat(2500)}}]}}`; mockStartCase.mockResolvedValue({ success: false, error: "Provider unavailable", diagnostics: { modelName: "llama3" }, statusCode: 502, analysisErrors: ["reconstruction: Required"], validationIssues: [ { path: ["reconstruction", "observedStates", 2, "description"], code: "invalid_type", message: "Required", expected: "string", received: "undefined", }, ], providerApiPath: "/api/generate", providerExecution: { chatCapabilityDetected: false, chatRequestAttempted: false, chatRequestSucceeded: false, generateRequestAttempted: true, }, rawResponse, }); const { POST } = await import("@/app/api/cases/start/route.js"); const response = await POST( new Request("http://localhost/api/cases/start", { method: "POST", body: JSON.stringify({ scenario: "Scenario text" }), headers: { "content-type": "application/json" }, }), ); expect(response.status).toBe(502); const body = await response.json(); expect(body).toHaveProperty("rawResponse"); expect(body.rawResponse).toBe(rawResponse); expect(body.rawResponse.length).toBeGreaterThan(2000); expect(body.analysisErrors).toEqual(["reconstruction: Required"]); expect(body.validationIssues).toEqual([ expect.objectContaining({ path: ["reconstruction", "observedStates", 2, "description"], code: "invalid_type", message: "Required", expected: "string", received: "undefined", }), ]); expect(body.providerApiPath).toBe("/api/generate"); expect(body.providerExecution).toEqual({ chatCapabilityDetected: false, chatRequestAttempted: false, chatRequestSucceeded: false, generateRequestAttempted: true, }); expect(errorSpy).toHaveBeenCalledTimes(1); expect(errorSpy).toHaveBeenCalledWith( "[api/cases/start] error response", expect.objectContaining({ status: 502, error: "Provider unavailable", analysisErrors: ["reconstruction: Required"], validationIssues: expect.any(Array), providerApiPath: "/api/generate", providerExecution: { chatCapabilityDetected: false, chatRequestAttempted: false, chatRequestSucceeded: false, generateRequestAttempted: true, }, rawResponse, }), ); expect(warnSpy).not.toHaveBeenCalled(); }); it("returns structured 500 on malformed JSON", async () => { const { POST } = await import("@/app/api/cases/start/route.js"); const request = { json: vi.fn().mockRejectedValue(new Error("Unexpected token")), }; const response = await POST(request); expect(response.status).toBe(500); await expect(response.json()).resolves.toMatchObject({ success: false, error: "Internal server error", }); expect(errorSpy).toHaveBeenCalledWith( "[api/cases/start] unhandled exception", expect.objectContaining({ message: "Unexpected token", stack: expect.any(String), error: expect.any(Error), }), ); }); });