fix(confidence-engine): keep episode reasoning server-side
This commit is contained in:
@@ -1,4 +1,7 @@
|
|||||||
import { updateCase } from "@/lib/graph/orchestrator.js";
|
import { updateCase, reconsiderCompletedEpisode } from "@/lib/graph/orchestrator.js";
|
||||||
|
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
|
||||||
|
import { prepareCompletedEpisode } from "@/lib/graph/episode-preparation.js";
|
||||||
|
import { updateCaseEpisodeRequestSchema } from "@/lib/graph/schema.js";
|
||||||
|
|
||||||
function mapFailureStatus(result) {
|
function mapFailureStatus(result) {
|
||||||
switch (result?.stage) {
|
switch (result?.stage) {
|
||||||
@@ -35,6 +38,25 @@ function buildFailureResponse(result) {
|
|||||||
export async function POST(request) {
|
export async function POST(request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
const isEpisodeMode = body?.episodeMode === true;
|
||||||
|
|
||||||
|
if (isEpisodeMode) {
|
||||||
|
const parsed = updateCaseEpisodeRequestSchema.safeParse(body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return Response.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
stage: "request_validation",
|
||||||
|
error: "Invalid episode request",
|
||||||
|
validationErrors: parsed.error.issues,
|
||||||
|
},
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await handleEpisodeMode(body.situationGraph, body);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await updateCase(body, { applyProposal: true });
|
const result = await updateCase(body, { applyProposal: true });
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -66,3 +88,50 @@ export async function POST(request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Server-side completed-episode reconsideration flow. */
|
||||||
|
async function handleEpisodeMode(situationGraph, body) {
|
||||||
|
const prepared = prepareCompletedEpisode({
|
||||||
|
situationGraph,
|
||||||
|
targetNodeId: body.targetNodeId,
|
||||||
|
contributions: body.contributions ?? [],
|
||||||
|
findings: body.findings,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!prepared?.turns?.length && !prepared?.eligibleCanonicalFindings?.length) {
|
||||||
|
return Response.json(
|
||||||
|
{ success: false, stage: "preparation", error: "no_episodic_content" },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reasoning = await reconsiderCompletedEpisode(prepared);
|
||||||
|
if (!reasoning.success) {
|
||||||
|
return Response.json(
|
||||||
|
buildFailureResponse(reasoning),
|
||||||
|
{ status: mapFailureStatus(reasoning) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const application = await applyValidatedProposal({
|
||||||
|
situationGraph,
|
||||||
|
proposal: reasoning.proposal,
|
||||||
|
evidenceContext: {
|
||||||
|
isCompletedEpisode: true,
|
||||||
|
episodeEvidence: prepared,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!application.success) {
|
||||||
|
return Response.json(
|
||||||
|
buildFailureResponse(application),
|
||||||
|
{ status: mapFailureStatus(application) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
updatedSituationGraph: application.updatedSituationGraph,
|
||||||
|
proposal: reasoning.proposal,
|
||||||
|
}, { status: 200 });
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import ReasoningWorkspace, { LoadingOverlay, ContinueLaterBanner } from "@/compo
|
|||||||
import { mockFetch, AVAILABLE_SCENARIOS } from "@/lib/mocks/confidence-engine/mock-client";
|
import { mockFetch, AVAILABLE_SCENARIOS } from "@/lib/mocks/confidence-engine/mock-client";
|
||||||
import { deriveFindingsFromContributions, normalizeFindings } from "@/lib/graph/finding-helpers";
|
import { deriveFindingsFromContributions, normalizeFindings } from "@/lib/graph/finding-helpers";
|
||||||
import { prepareCompletedEpisode } from "@/lib/graph/episode-preparation.js";
|
import { prepareCompletedEpisode } from "@/lib/graph/episode-preparation.js";
|
||||||
import { reconsiderCompletedEpisode } from "@/lib/graph/orchestrator.js";
|
|
||||||
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
|
|
||||||
import { loadInvestigation, saveInvestigation, clearInvestigation } from "@/lib/storage/investigation-storage";
|
import { loadInvestigation, saveInvestigation, clearInvestigation } from "@/lib/storage/investigation-storage";
|
||||||
|
|
||||||
/* Compile-time env resolution — NEXT_PUBLIC_ vars are injected by Next.js at build */
|
/* Compile-time env resolution — NEXT_PUBLIC_ vars are injected by Next.js at build */
|
||||||
@@ -244,7 +242,7 @@ export function derivePrimarySurface(result, status, _showExperimentView, scenar
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Orchestrate the authoritative episode reconsideration flow.
|
* Orchestrate the authoritative episode reconsideration flow.
|
||||||
* Exported for deterministic testing — all domain functions accepted as parameters.
|
* Exported for deterministic testing — domain functions and server endpoint accepted as parameters.
|
||||||
*/
|
*/
|
||||||
export async function executeEpisodeDone({
|
export async function executeEpisodeDone({
|
||||||
resultSituationGraph,
|
resultSituationGraph,
|
||||||
@@ -252,8 +250,7 @@ export async function executeEpisodeDone({
|
|||||||
focusedContributions,
|
focusedContributions,
|
||||||
findings,
|
findings,
|
||||||
prepareCompletedEpisode: prepFn = prepareCompletedEpisode,
|
prepareCompletedEpisode: prepFn = prepareCompletedEpisode,
|
||||||
reconsiderCompletedEpisode: reconsiderFn = reconsiderCompletedEpisode,
|
episodeDoneServer,
|
||||||
applyValidatedProposal: applyFn = applyValidatedProposal,
|
|
||||||
synthesizeFn,
|
synthesizeFn,
|
||||||
setResult: setAppState,
|
setResult: setAppState,
|
||||||
}) {
|
}) {
|
||||||
@@ -268,25 +265,18 @@ export async function executeEpisodeDone({
|
|||||||
return { success: false, stage: "preparation", reason: "no_episodic_content" };
|
return { success: false, stage: "preparation", reason: "no_episodic_content" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const reasoning = await reconsiderFn(prepared);
|
const serverResult = await episodeDoneServer({
|
||||||
if (!reasoning.success) {
|
|
||||||
return { success: false, stage: "reconsideration", error: reasoning.error };
|
|
||||||
}
|
|
||||||
|
|
||||||
const application = await applyFn({
|
|
||||||
situationGraph: resultSituationGraph,
|
situationGraph: resultSituationGraph,
|
||||||
proposal: reasoning.proposal,
|
targetNodeId,
|
||||||
evidenceContext: {
|
contributions: focusedContributions ?? [],
|
||||||
isCompletedEpisode: true,
|
findings,
|
||||||
episodeEvidence: prepared,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!application.success) {
|
if (!serverResult.success) {
|
||||||
return { success: false, stage: "application", error: application.error };
|
return { success: false, stage: "episode_done", error: serverResult.error };
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextGraph = application.updatedSituationGraph;
|
const nextGraph = serverResult.updatedSituationGraph;
|
||||||
setAppState(prev => ({ ...(prev ?? {}), situationGraph: nextGraph }));
|
setAppState(prev => ({ ...(prev ?? {}), situationGraph: nextGraph }));
|
||||||
|
|
||||||
const synthesisResult = await synthesizeFn(nextGraph, findings);
|
const synthesisResult = await synthesizeFn(nextGraph, findings);
|
||||||
@@ -393,6 +383,12 @@ export default function ScenarioForm() {
|
|||||||
targetNodeId,
|
targetNodeId,
|
||||||
focusedContributions: focusedContributions ?? [],
|
focusedContributions: focusedContributions ?? [],
|
||||||
findings,
|
findings,
|
||||||
|
episodeDoneServer: (payload) =>
|
||||||
|
fetch("/api/cases/update", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ ...payload, episodeMode: true }),
|
||||||
|
}).then((res) => res.json()),
|
||||||
synthesizeFn: (graph, fn) => synthesizeFromFindings(fetch, { situationGraph: graph, findings: fn }),
|
synthesizeFn: (graph, fn) => synthesizeFromFindings(fetch, { situationGraph: graph, findings: fn }),
|
||||||
setResult,
|
setResult,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export const startCaseRequestSchema = z.object({
|
|||||||
promptVersion: z.string().optional(),
|
promptVersion: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Legacy schema — unchanged contract for existing consumers. */
|
||||||
export const updateCaseRequestSchema = z.object({
|
export const updateCaseRequestSchema = z.object({
|
||||||
situationGraph: situationGraphSchema,
|
situationGraph: situationGraphSchema,
|
||||||
previousQuestion: z.string().min(1),
|
previousQuestion: z.string().min(1),
|
||||||
@@ -222,6 +223,19 @@ export const updateCaseRequestSchema = z.object({
|
|||||||
).optional(),
|
).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Extended schema with optional fields accepted by the route for all requests. */
|
||||||
|
export const updateCaseRequestSchemaExtended = updateCaseRequestSchema.extend({
|
||||||
|
targetNodeId: z.string().optional(),
|
||||||
|
contributions: z.any().array().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Minimal schema for episode-mode requests (legacy fields not required). */
|
||||||
|
export const updateCaseEpisodeRequestSchema = z.object({
|
||||||
|
situationGraph: situationGraphSchema,
|
||||||
|
targetNodeId: z.string().min(1),
|
||||||
|
contributions: z.any().array().optional(),
|
||||||
|
}).passthrough();
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────
|
||||||
|
|
||||||
/** Generate a short deterministic ID from a label */
|
/** Generate a short deterministic ID from a label */
|
||||||
|
|||||||
@@ -33,123 +33,111 @@ function makeProposal() {
|
|||||||
import { executeEpisodeDone } from "@/components/scenario-form.jsx";
|
import { executeEpisodeDone } from "@/components/scenario-form.jsx";
|
||||||
|
|
||||||
describe("episode-done orchestration", () => {
|
describe("episode-done orchestration", () => {
|
||||||
function makeMocks() {
|
function makeServerMock() {
|
||||||
|
const calls = [];
|
||||||
return {
|
return {
|
||||||
prepareCompletedEpisode: vi.fn().mockReturnValue(makePreparedEpisode()),
|
fn: vi.fn().mockImplementation(async (payload) => {
|
||||||
reconsiderCompletedEpisode: vi.fn().mockResolvedValue({ success: true, proposal: makeProposal() }),
|
calls.push(payload);
|
||||||
applyValidatedProposal: vi.fn().mockResolvedValue({ success: true, updatedSituationGraph: DISTINCTIVE_GRAPH }),
|
return { success: true, updatedSituationGraph: DISTINCTIVE_GRAPH, proposal: makeProposal() };
|
||||||
|
}),
|
||||||
|
calls,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
it("1. Successful path order", async () => {
|
function makePrepMock(returnValue) {
|
||||||
const m = makeMocks();
|
return vi.fn().mockReturnValue(returnValue ?? makePreparedEpisode());
|
||||||
|
}
|
||||||
|
|
||||||
|
it("1. Successful path order (prepare → synthesize)", async () => {
|
||||||
const order = [];
|
const order = [];
|
||||||
|
const serverMock = makeServerMock();
|
||||||
const trackReconsider = vi.fn().mockImplementation(async (...args) => {
|
const prepFn = makePrepMock().mockImplementation(() => {
|
||||||
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(() => {
|
|
||||||
order.push("prepare");
|
order.push("prepare");
|
||||||
return makePreparedEpisode();
|
return makePreparedEpisode();
|
||||||
});
|
});
|
||||||
|
serverMock.fn.mockImplementation(async (payload) => {
|
||||||
|
order.push("server");
|
||||||
|
return { success: true, updatedSituationGraph: DISTINCTIVE_GRAPH, proposal: makeProposal() };
|
||||||
|
});
|
||||||
|
|
||||||
await executeEpisodeDone({
|
await executeEpisodeDone({
|
||||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||||
targetNodeId: "n-q1",
|
targetNodeId: "n-q1",
|
||||||
focusedContributions: [{ id: "c-1" }],
|
focusedContributions: [{ id: "c-1" }],
|
||||||
findings: [],
|
findings: [],
|
||||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
prepareCompletedEpisode: prepFn,
|
||||||
reconsiderCompletedEpisode: trackReconsider,
|
episodeDoneServer: serverMock.fn,
|
||||||
applyValidatedProposal: trackApply,
|
synthesizeFn: vi.fn().mockImplementation(async () => {
|
||||||
synthesizeFn: trackSynth,
|
order.push("synthesize");
|
||||||
|
return { ok: true, data: {} };
|
||||||
|
}),
|
||||||
setResult: vi.fn(),
|
setResult: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify exact order
|
|
||||||
expect(order[0]).toBe("prepare");
|
expect(order[0]).toBe("prepare");
|
||||||
expect(order[1]).toBe("reconsider");
|
expect(order[1]).toBe("server");
|
||||||
expect(order[2]).toBe("apply");
|
expect(order[2]).toBe("synthesize");
|
||||||
expect(order[3]).toBe("synthesis");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("2. Correct prepared episode input", async () => {
|
it("2. Client sends prepared episode source state to server", async () => {
|
||||||
const m = makeMocks();
|
|
||||||
const findings = [{ id: "f-1", proposition: "P1" }];
|
const findings = [{ id: "f-1", proposition: "P1" }];
|
||||||
const contributions = [{ id: "c-1", question: "Q?", answer: "A?" }];
|
const contributions = [{ id: "c-1", question: "Q?", answer: "A?" }];
|
||||||
|
const serverMock = makeServerMock();
|
||||||
|
|
||||||
await executeEpisodeDone({
|
await executeEpisodeDone({
|
||||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||||
targetNodeId: "n-q1",
|
targetNodeId: "n-q1",
|
||||||
focusedContributions: contributions,
|
focusedContributions: contributions,
|
||||||
findings,
|
findings,
|
||||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
prepareCompletedEpisode: makePrepMock(),
|
||||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
episodeDoneServer: serverMock.fn,
|
||||||
applyValidatedProposal: m.applyValidatedProposal,
|
|
||||||
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
||||||
setResult: vi.fn(),
|
setResult: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(m.prepareCompletedEpisode).toHaveBeenCalledWith({
|
const [serverPayload] = serverMock.calls;
|
||||||
situationGraph: MOCK_SITUATION_GRAPH,
|
expect(serverPayload).toHaveProperty("situationGraph", MOCK_SITUATION_GRAPH);
|
||||||
targetNodeId: "n-q1",
|
expect(serverPayload.targetNodeId).toBe("n-q1");
|
||||||
contributions,
|
expect(serverPayload.contributions).toEqual(contributions);
|
||||||
findings,
|
expect(serverPayload.findings).toEqual(findings);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("3. Structured application evidence — no answer fields", async () => {
|
it("3. No synthetic answer fields in server payload", async () => {
|
||||||
const m = makeMocks();
|
const serverMock = makeServerMock();
|
||||||
let capturedEvidenceContext = null;
|
|
||||||
m.applyValidatedProposal.mockImplementation(async (args) => {
|
|
||||||
capturedEvidenceContext = args?.evidenceContext;
|
|
||||||
return { success: true, updatedSituationGraph: DISTINCTIVE_GRAPH };
|
|
||||||
});
|
|
||||||
|
|
||||||
await executeEpisodeDone({
|
await executeEpisodeDone({
|
||||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||||
targetNodeId: "n-q1",
|
targetNodeId: "n-q1",
|
||||||
focusedContributions: [],
|
focusedContributions: [],
|
||||||
findings: [],
|
findings: [],
|
||||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
prepareCompletedEpisode: makePrepMock(),
|
||||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
episodeDoneServer: serverMock.fn,
|
||||||
applyValidatedProposal: m.applyValidatedProposal,
|
|
||||||
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
||||||
setResult: vi.fn(),
|
setResult: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(capturedEvidenceContext?.isCompletedEpisode).toBe(true);
|
const [serverPayload] = serverMock.calls;
|
||||||
expect(capturedEvidenceContext?.episodeEvidence).toBeDefined();
|
const keys = Object.keys(serverPayload);
|
||||||
const keys = Object.keys(capturedEvidenceContext || {});
|
|
||||||
expect(keys).not.toContain("answer");
|
expect(keys).not.toContain("answer");
|
||||||
expect(keys).not.toContain("syntheticAnswer");
|
expect(keys).not.toContain("syntheticAnswer");
|
||||||
expect(keys).not.toContain("combinedAnswer");
|
expect(keys).not.toContain("combinedAnswer");
|
||||||
expect(keys).not.toContain("lastAnswer");
|
expect(keys).not.toContain("lastAnswer");
|
||||||
|
expect(keys).not.toContain("previousQuestion");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("4. nextGraph used for synthesis (not stale state)", async () => {
|
it("4. nextGraph used for synthesis (not stale state)", async () => {
|
||||||
const m = makeMocks();
|
const serverMock = makeServerMock();
|
||||||
let synthesizedGraph = null;
|
let synthesizedGraph = null;
|
||||||
m.applyValidatedProposal.mockResolvedValue({ success: true, updatedSituationGraph: DISTINCTIVE_GRAPH });
|
|
||||||
|
serverMock.fn.mockResolvedValue({ success: true, updatedSituationGraph: DISTINCTIVE_GRAPH, proposal: makeProposal() });
|
||||||
|
|
||||||
await executeEpisodeDone({
|
await executeEpisodeDone({
|
||||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||||
targetNodeId: "n-q1",
|
targetNodeId: "n-q1",
|
||||||
focusedContributions: [],
|
focusedContributions: [],
|
||||||
findings: [{ id: "f-1" }],
|
findings: [{ id: "f-1" }],
|
||||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
prepareCompletedEpisode: makePrepMock(),
|
||||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
episodeDoneServer: serverMock.fn,
|
||||||
applyValidatedProposal: m.applyValidatedProposal,
|
|
||||||
synthesizeFn: (graph) => {
|
synthesizeFn: (graph) => {
|
||||||
synthesizedGraph = graph;
|
synthesizedGraph = graph;
|
||||||
return { ok: true, data: {} };
|
return { ok: true, data: {} };
|
||||||
@@ -160,49 +148,28 @@ describe("episode-done orchestration", () => {
|
|||||||
expect(synthesizedGraph).toBe(DISTINCTIVE_GRAPH);
|
expect(synthesizedGraph).toBe(DISTINCTIVE_GRAPH);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("5. Reasoning failure — apply not called, CU synthesis not called", async () => {
|
it("5. Reasoning/application failure — CU synthesis not called", async () => {
|
||||||
const m = makeMocks();
|
const serverMock = makeServerMock();
|
||||||
m.reconsiderCompletedEpisode.mockResolvedValue({ success: false, stage: "provider" });
|
serverMock.fn.mockResolvedValue({ success: false, stage: "provider", error: "provider failed" });
|
||||||
|
|
||||||
const result = await executeEpisodeDone({
|
const result = await executeEpisodeDone({
|
||||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||||
targetNodeId: "n-q1",
|
targetNodeId: "n-q1",
|
||||||
focusedContributions: [],
|
focusedContributions: [],
|
||||||
findings: [],
|
findings: [],
|
||||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
prepareCompletedEpisode: makePrepMock(),
|
||||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
episodeDoneServer: serverMock.fn,
|
||||||
applyValidatedProposal: m.applyValidatedProposal,
|
|
||||||
synthesizeFn: vi.fn(),
|
synthesizeFn: vi.fn(),
|
||||||
setResult: vi.fn(),
|
setResult: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
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 () => {
|
it("6. Application failure — CU synthesis not called, graph not replaced", async () => {
|
||||||
const m = makeMocks();
|
const serverMock = makeServerMock();
|
||||||
m.applyValidatedProposal.mockResolvedValue({ success: false, stage: "proposal_compatibility" });
|
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();
|
const setResult = vi.fn();
|
||||||
|
|
||||||
await executeEpisodeDone({
|
await executeEpisodeDone({
|
||||||
@@ -210,9 +177,26 @@ describe("episode-done orchestration", () => {
|
|||||||
targetNodeId: "n-q1",
|
targetNodeId: "n-q1",
|
||||||
focusedContributions: [],
|
focusedContributions: [],
|
||||||
findings: [],
|
findings: [],
|
||||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
prepareCompletedEpisode: makePrepMock(),
|
||||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
episodeDoneServer: serverMock.fn,
|
||||||
applyValidatedProposal: m.applyValidatedProposal,
|
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 }),
|
synthesizeFn: vi.fn().mockReturnValue({ ok: false }),
|
||||||
setResult,
|
setResult,
|
||||||
});
|
});
|
||||||
@@ -224,37 +208,34 @@ describe("episode-done orchestration", () => {
|
|||||||
expect(updatedState.situationGraph).toBe(DISTINCTIVE_GRAPH);
|
expect(updatedState.situationGraph).toBe(DISTINCTIVE_GRAPH);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("8. Exactly-once — each step invoked once per call", async () => {
|
it("8. Exactly-once — server called once per done action", async () => {
|
||||||
const m = makeMocks();
|
const serverMock = makeServerMock();
|
||||||
|
|
||||||
await executeEpisodeDone({
|
await executeEpisodeDone({
|
||||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||||
targetNodeId: "n-q1",
|
targetNodeId: "n-q1",
|
||||||
focusedContributions: [],
|
focusedContributions: [],
|
||||||
findings: [],
|
findings: [],
|
||||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
prepareCompletedEpisode: makePrepMock(),
|
||||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
episodeDoneServer: serverMock.fn,
|
||||||
applyValidatedProposal: m.applyValidatedProposal,
|
|
||||||
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
||||||
setResult: vi.fn(),
|
setResult: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(m.prepareCompletedEpisode).toHaveBeenCalledTimes(1);
|
expect(serverMock.fn).toHaveBeenCalledTimes(1);
|
||||||
expect(m.reconsiderCompletedEpisode).toHaveBeenCalledTimes(1);
|
expect(serverMock.calls.length).toBe(1);
|
||||||
expect(m.applyValidatedProposal).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("9. Legacy Done CU writer retired — pipeline does not use deterministic summary", async () => {
|
it("9. Success returns authoritative nextGraph", async () => {
|
||||||
const m = makeMocks();
|
const serverMock = makeServerMock();
|
||||||
|
|
||||||
const result = await executeEpisodeDone({
|
const result = await executeEpisodeDone({
|
||||||
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
resultSituationGraph: MOCK_SITUATION_GRAPH,
|
||||||
targetNodeId: "n-q1",
|
targetNodeId: "n-q1",
|
||||||
focusedContributions: [],
|
focusedContributions: [],
|
||||||
findings: [],
|
findings: [],
|
||||||
prepareCompletedEpisode: m.prepareCompletedEpisode,
|
prepareCompletedEpisode: makePrepMock(),
|
||||||
reconsiderCompletedEpisode: m.reconsiderCompletedEpisode,
|
episodeDoneServer: serverMock.fn,
|
||||||
applyValidatedProposal: m.applyValidatedProposal,
|
|
||||||
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
synthesizeFn: vi.fn().mockReturnValue({ ok: true, data: {} }),
|
||||||
setResult: vi.fn(),
|
setResult: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user