Files
confidence-engine/tests/app/api/current-understanding-synthesis-route.test.js
T

187 lines
7.1 KiB
JavaScript

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: () => ({}),
}));
// ── 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);
});
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);
});
});