218 lines
7.0 KiB
React
218 lines
7.0 KiB
React
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);
|
|
});
|
|
});
|