import { describe, expect, it, vi } from "vitest"; // ── Domain fixtures ─────────────────────────────────────────── const MOCK_SITUATION_GRAPH = { nodes: [{ id: "n-q1", label: "Query Q1", kind: "question", status: "unknown" }], edges: [], }; const DISTINCTIVE_GRAPH = { __distinctive_graph__: true }; function makeProposal() { return { addedNodes: [], updatedNodes: [{ nodeId: "n-q1", newStatus: "resolved" }], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [], affectedNodeIds: [], selectedQuestion: null, }; } // ── Import the testable pipeline ─────────────────────────────── import { executeEpisodeDone } from "@/components/scenario-form.jsx"; describe("episode-done orchestration", () => { function makeServerMock() { const calls = []; return { fn: vi.fn().mockImplementation(async (payload) => { calls.push(payload); return { success: true, updatedSituationGraph: DISTINCTIVE_GRAPH, proposal: makeProposal() }; }), calls, }; } it("1. Successful path order (server → synthesize)", async () => { const order = []; const serverMock = makeServerMock(); serverMock.fn.mockImplementation(async (payload) => { order.push("server"); return { success: true, updatedSituationGraph: DISTINCTIVE_GRAPH, proposal: makeProposal() }; }); await executeEpisodeDone({ resultSituationGraph: MOCK_SITUATION_GRAPH, targetNodeId: "n-q1", focusedContributions: [{ id: "c-1" }], findings: [], episodeDoneServer: serverMock.fn, synthesizeFn: vi.fn().mockImplementation(async () => { order.push("synthesize"); return { ok: true, data: {} }; }), setResult: vi.fn(), }); expect(order[0]).toBe("server"); expect(order[1]).toBe("synthesize"); }); it("2. Client sends prepared episode source state to server", async () => { const findings = [{ id: "f-1", proposition: "P1" }]; const contributions = [{ id: "c-1", question: "Q?", answer: "A?" }]; const serverMock = makeServerMock(); await executeEpisodeDone({ resultSituationGraph: MOCK_SITUATION_GRAPH, targetNodeId: "n-q1", focusedContributions: contributions, findings, episodeDoneServer: serverMock.fn, synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }), setResult: vi.fn(), }); const [serverPayload] = serverMock.calls; expect(serverPayload).toHaveProperty("situationGraph", MOCK_SITUATION_GRAPH); expect(serverPayload.targetNodeId).toBe("n-q1"); expect(serverPayload.contributions).toEqual(contributions); expect(serverPayload.findings).toEqual(findings); }); it("3. No synthetic answer fields in server payload", async () => { const serverMock = makeServerMock(); await executeEpisodeDone({ resultSituationGraph: MOCK_SITUATION_GRAPH, targetNodeId: "n-q1", focusedContributions: [], findings: [], episodeDoneServer: serverMock.fn, synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }), setResult: vi.fn(), }); const [serverPayload] = serverMock.calls; const keys = Object.keys(serverPayload); expect(keys).not.toContain("answer"); expect(keys).not.toContain("syntheticAnswer"); expect(keys).not.toContain("combinedAnswer"); expect(keys).not.toContain("lastAnswer"); expect(keys).not.toContain("previousQuestion"); }); it("4. nextGraph used for synthesis (not stale state)", async () => { const serverMock = makeServerMock(); let synthesizedGraph = null; serverMock.fn.mockResolvedValue({ success: true, updatedSituationGraph: DISTINCTIVE_GRAPH, proposal: makeProposal() }); await executeEpisodeDone({ resultSituationGraph: MOCK_SITUATION_GRAPH, targetNodeId: "n-q1", focusedContributions: [], findings: [{ id: "f-1" }], episodeDoneServer: serverMock.fn, synthesizeFn: (graph) => { synthesizedGraph = graph; return { ok: true, data: {} }; }, setResult: vi.fn(), }); expect(synthesizedGraph).toBe(DISTINCTIVE_GRAPH); }); it("5. Reasoning/application failure — CU synthesis not called", async () => { const serverMock = makeServerMock(); serverMock.fn.mockResolvedValue({ success: false, stage: "provider", error: "provider failed" }); const result = await executeEpisodeDone({ resultSituationGraph: MOCK_SITUATION_GRAPH, targetNodeId: "n-q1", focusedContributions: [], findings: [], episodeDoneServer: serverMock.fn, synthesizeFn: vi.fn(), setResult: vi.fn(), }); expect(result.success).toBe(false); }); it("6. Application failure — CU synthesis not called, graph not replaced", async () => { const serverMock = makeServerMock(); serverMock.fn.mockResolvedValue({ success: false, stage: "proposal_compatibility", error: "incompatible" }); const setResult = vi.fn(); await executeEpisodeDone({ resultSituationGraph: MOCK_SITUATION_GRAPH, targetNodeId: "n-q1", focusedContributions: [], findings: [], episodeDoneServer: serverMock.fn, synthesizeFn: vi.fn(), setResult, }); expect(setResult).not.toHaveBeenCalled(); }); it("7. Synthesis failure — nextGraph remains installed", async () => { const serverMock = makeServerMock(); const setResult = vi.fn(); await executeEpisodeDone({ resultSituationGraph: MOCK_SITUATION_GRAPH, targetNodeId: "n-q1", focusedContributions: [], findings: [], episodeDoneServer: serverMock.fn, synthesizeFn: vi.fn().mockReturnValue({ ok: false }), setResult, }); // Graph was installed despite synthesis failure expect(setResult).toHaveBeenCalledTimes(1); const updater = setResult.mock.calls[0][0]; const updatedState = updater(null); expect(updatedState.situationGraph).toBe(DISTINCTIVE_GRAPH); }); it("8. Exactly-once — server called once per done action", async () => { const serverMock = makeServerMock(); await executeEpisodeDone({ resultSituationGraph: MOCK_SITUATION_GRAPH, targetNodeId: "n-q1", focusedContributions: [], findings: [], episodeDoneServer: serverMock.fn, synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }), setResult: vi.fn(), }); expect(serverMock.fn).toHaveBeenCalledTimes(1); expect(serverMock.calls.length).toBe(1); }); it("9. Success returns authoritative nextGraph", async () => { const serverMock = makeServerMock(); const result = await executeEpisodeDone({ resultSituationGraph: MOCK_SITUATION_GRAPH, targetNodeId: "n-q1", focusedContributions: [], findings: [], episodeDoneServer: serverMock.fn, synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }), setResult: vi.fn(), }); expect(result.success).toBe(true); expect(result.nextGraph).toBe(DISTINCTIVE_GRAPH); }); });