feat(confidence-engine): establish current understanding synthesis seam
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user