Files
confidence-engine/tests/app/api/cases-start-route.test.js

228 lines
7.0 KiB
JavaScript

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),
}));
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 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),
}),
);
});
});