fix(confidence-engine): keep episode reasoning server-side
This commit is contained in:
@@ -33,123 +33,111 @@ function makeProposal() {
|
||||
import { executeEpisodeDone } from "@/components/scenario-form.jsx";
|
||||
|
||||
describe("episode-done orchestration", () => {
|
||||
function makeMocks() {
|
||||
function makeServerMock() {
|
||||
const calls = [];
|
||||
return {
|
||||
prepareCompletedEpisode: vi.fn().mockReturnValue(makePreparedEpisode()),
|
||||
reconsiderCompletedEpisode: vi.fn().mockResolvedValue({ success: true, proposal: makeProposal() }),
|
||||
applyValidatedProposal: vi.fn().mockResolvedValue({ success: true, updatedSituationGraph: DISTINCTIVE_GRAPH }),
|
||||
fn: vi.fn().mockImplementation(async (payload) => {
|
||||
calls.push(payload);
|
||||
return { success: true, updatedSituationGraph: DISTINCTIVE_GRAPH, proposal: makeProposal() };
|
||||
}),
|
||||
calls,
|
||||
};
|
||||
}
|
||||
|
||||
it("1. Successful path order", async () => {
|
||||
const m = makeMocks();
|
||||
function makePrepMock(returnValue) {
|
||||
return vi.fn().mockReturnValue(returnValue ?? makePreparedEpisode());
|
||||
}
|
||||
|
||||
it("1. Successful path order (prepare → synthesize)", async () => {
|
||||
const order = [];
|
||||
|
||||
const trackReconsider = vi.fn().mockImplementation(async (...args) => {
|
||||
order.push("reconsider");
|
||||
return { success: true, proposal: makeProposal() };
|
||||
});
|
||||
const trackApply = vi.fn().mockImplementation(async (...args) => {
|
||||
order.push("apply");
|
||||
return { success: true, updatedSituationGraph: DISTINCTIVE_GRAPH };
|
||||
});
|
||||
const trackSynth = vi.fn().mockImplementation(async (...args) => {
|
||||
order.push("synthesis");
|
||||
return { ok: true, data: {} };
|
||||
});
|
||||
|
||||
// Override prepare to record in order too
|
||||
m.prepareCompletedEpisode.mockImplementation(() => {
|
||||
const serverMock = makeServerMock();
|
||||
const prepFn = makePrepMock().mockImplementation(() => {
|
||||
order.push("prepare");
|
||||
return makePreparedEpisode();
|
||||
});
|
||||
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: [],
|
||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
||||
reconsiderCompletedEpisode: trackReconsider,
|
||||
applyValidatedProposal: trackApply,
|
||||
synthesizeFn: trackSynth,
|
||||
prepareCompletedEpisode: prepFn,
|
||||
episodeDoneServer: serverMock.fn,
|
||||
synthesizeFn: vi.fn().mockImplementation(async () => {
|
||||
order.push("synthesize");
|
||||
return { ok: true, data: {} };
|
||||
}),
|
||||
setResult: vi.fn(),
|
||||
});
|
||||
|
||||
// Verify exact order
|
||||
expect(order[0]).toBe("prepare");
|
||||
expect(order[1]).toBe("reconsider");
|
||||
expect(order[2]).toBe("apply");
|
||||
expect(order[3]).toBe("synthesis");
|
||||
expect(order[1]).toBe("server");
|
||||
expect(order[2]).toBe("synthesize");
|
||||
});
|
||||
|
||||
it("2. Correct prepared episode input", async () => {
|
||||
const m = makeMocks();
|
||||
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,
|
||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
||||
applyValidatedProposal: m.applyValidatedProposal,
|
||||
prepareCompletedEpisode: makePrepMock(),
|
||||
episodeDoneServer: serverMock.fn,
|
||||
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
||||
setResult: vi.fn(),
|
||||
});
|
||||
|
||||
expect(m.prepareCompletedEpisode).toHaveBeenCalledWith({
|
||||
situationGraph: MOCK_SITUATION_GRAPH,
|
||||
targetNodeId: "n-q1",
|
||||
contributions,
|
||||
findings,
|
||||
});
|
||||
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. Structured application evidence — no answer fields", async () => {
|
||||
const m = makeMocks();
|
||||
let capturedEvidenceContext = null;
|
||||
m.applyValidatedProposal.mockImplementation(async (args) => {
|
||||
capturedEvidenceContext = args?.evidenceContext;
|
||||
return { success: true, updatedSituationGraph: DISTINCTIVE_GRAPH };
|
||||
});
|
||||
it("3. No synthetic answer fields in server payload", async () => {
|
||||
const serverMock = makeServerMock();
|
||||
|
||||
await executeEpisodeDone({
|
||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||
targetNodeId: "n-q1",
|
||||
focusedContributions: [],
|
||||
findings: [],
|
||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
||||
applyValidatedProposal: m.applyValidatedProposal,
|
||||
prepareCompletedEpisode: makePrepMock(),
|
||||
episodeDoneServer: serverMock.fn,
|
||||
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
||||
setResult: vi.fn(),
|
||||
});
|
||||
|
||||
expect(capturedEvidenceContext?.isCompletedEpisode).toBe(true);
|
||||
expect(capturedEvidenceContext?.episodeEvidence).toBeDefined();
|
||||
const keys = Object.keys(capturedEvidenceContext || {});
|
||||
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 m = makeMocks();
|
||||
const serverMock = makeServerMock();
|
||||
let synthesizedGraph = null;
|
||||
m.applyValidatedProposal.mockResolvedValue({ success: true, updatedSituationGraph: DISTINCTIVE_GRAPH });
|
||||
|
||||
serverMock.fn.mockResolvedValue({ success: true, updatedSituationGraph: DISTINCTIVE_GRAPH, proposal: makeProposal() });
|
||||
|
||||
await executeEpisodeDone({
|
||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||
targetNodeId: "n-q1",
|
||||
focusedContributions: [],
|
||||
findings: [{ id: "f-1" }],
|
||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
||||
applyValidatedProposal: m.applyValidatedProposal,
|
||||
prepareCompletedEpisode: makePrepMock(),
|
||||
episodeDoneServer: serverMock.fn,
|
||||
synthesizeFn: (graph) => {
|
||||
synthesizedGraph = graph;
|
||||
return { ok: true, data: {} };
|
||||
@@ -160,49 +148,28 @@ describe("episode-done orchestration", () => {
|
||||
expect(synthesizedGraph).toBe(DISTINCTIVE_GRAPH);
|
||||
});
|
||||
|
||||
it("5. Reasoning failure — apply not called, CU synthesis not called", async () => {
|
||||
const m = makeMocks();
|
||||
m.reconsiderCompletedEpisode.mockResolvedValue({ success: false, stage: "provider" });
|
||||
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: [],
|
||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
||||
applyValidatedProposal: m.applyValidatedProposal,
|
||||
prepareCompletedEpisode: makePrepMock(),
|
||||
episodeDoneServer: serverMock.fn,
|
||||
synthesizeFn: vi.fn(),
|
||||
setResult: vi.fn(),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(m.applyValidatedProposal).not.toHaveBeenCalled();
|
||||
expect(m.prepareCompletedEpisode).toHaveBeenCalledTimes(1);
|
||||
expect(m.reconsiderCompletedEpisode).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("6. Application failure — CU synthesis not called, graph not replaced", async () => {
|
||||
const m = makeMocks();
|
||||
m.applyValidatedProposal.mockResolvedValue({ success: false, stage: "proposal_compatibility" });
|
||||
const serverMock = makeServerMock();
|
||||
serverMock.fn.mockResolvedValue({ success: false, stage: "proposal_compatibility", error: "incompatible" });
|
||||
|
||||
const result = await executeEpisodeDone({
|
||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||
targetNodeId: "n-q1",
|
||||
focusedContributions: [],
|
||||
findings: [],
|
||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
||||
applyValidatedProposal: m.applyValidatedProposal,
|
||||
synthesizeFn: vi.fn(),
|
||||
setResult: vi.fn(),
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("7. Synthesis failure — nextGraph remains installed", async () => {
|
||||
const m = makeMocks();
|
||||
const setResult = vi.fn();
|
||||
|
||||
await executeEpisodeDone({
|
||||
@@ -210,9 +177,26 @@ describe("episode-done orchestration", () => {
|
||||
targetNodeId: "n-q1",
|
||||
focusedContributions: [],
|
||||
findings: [],
|
||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
||||
applyValidatedProposal: m.applyValidatedProposal,
|
||||
prepareCompletedEpisode: makePrepMock(),
|
||||
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: [],
|
||||
prepareCompletedEpisode: makePrepMock(),
|
||||
episodeDoneServer: serverMock.fn,
|
||||
synthesizeFn: vi.fn().mockReturnValue({ ok: false }),
|
||||
setResult,
|
||||
});
|
||||
@@ -224,37 +208,34 @@ describe("episode-done orchestration", () => {
|
||||
expect(updatedState.situationGraph).toBe(DISTINCTIVE_GRAPH);
|
||||
});
|
||||
|
||||
it("8. Exactly-once — each step invoked once per call", async () => {
|
||||
const m = makeMocks();
|
||||
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: [],
|
||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
||||
applyValidatedProposal: m.applyValidatedProposal,
|
||||
prepareCompletedEpisode: makePrepMock(),
|
||||
episodeDoneServer: serverMock.fn,
|
||||
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
||||
setResult: vi.fn(),
|
||||
});
|
||||
|
||||
expect(m.prepareCompletedEpisode).toHaveBeenCalledTimes(1);
|
||||
expect(m.reconsiderCompletedEpisode).toHaveBeenCalledTimes(1);
|
||||
expect(m.applyValidatedProposal).toHaveBeenCalledTimes(1);
|
||||
expect(serverMock.fn).toHaveBeenCalledTimes(1);
|
||||
expect(serverMock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("9. Legacy Done CU writer retired — pipeline does not use deterministic summary", async () => {
|
||||
const m = makeMocks();
|
||||
it("9. Success returns authoritative nextGraph", async () => {
|
||||
const serverMock = makeServerMock();
|
||||
|
||||
const result = await executeEpisodeDone({
|
||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||
targetNodeId: "n-q1",
|
||||
focusedContributions: [],
|
||||
findings: [],
|
||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
||||
applyValidatedProposal: m.applyValidatedProposal,
|
||||
prepareCompletedEpisode: makePrepMock(),
|
||||
episodeDoneServer: serverMock.fn,
|
||||
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
||||
setResult: vi.fn(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user