feat(confidence-engine): add completed episode reasoning seam
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// ── Fixtures ────────────────────────────────────────────────
|
||||
|
||||
function makePreparedEpisode(overrides = {}) {
|
||||
return {
|
||||
situationGraph: {
|
||||
centralStatement: "Complaint counts increased.",
|
||||
nodes: [
|
||||
{ id: "n-unknown", label: "Rate denominator", kind: "unknown", status: "unknown" },
|
||||
],
|
||||
edges: [],
|
||||
activeUnknownNodeId: "n-unknown",
|
||||
resolvedNodeIds: [],
|
||||
},
|
||||
targetNodeId: "n-unknown",
|
||||
turns: [
|
||||
{ contributionId: "contrib-a", sequence: 1, question: "What denominator?", answer: "100 units" },
|
||||
{ contributionId: "contrib-b", sequence: 2, question: "Is it consistent?", answer: "Yes" },
|
||||
],
|
||||
eligibleCanonicalFindings: [
|
||||
{ findingId: "f-1", contributionId: "contrib-a", proposition: "Denominator is fixed at 100.", endorsement: null },
|
||||
],
|
||||
excludedFindingProvenance: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeProposalResponse(overrides = {}) {
|
||||
return {
|
||||
addedNodes: [],
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: "n-unknown",
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue: "100 units",
|
||||
reason: "Denominator confirmed.",
|
||||
},
|
||||
],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: ["n-unknown"],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: null,
|
||||
answerMeaning: null,
|
||||
structuralActionRequired: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const MOCK_CONFIG = { OLLAMA_MODEL: "configured" };
|
||||
const TEST_EPISODE = makePreparedEpisode();
|
||||
|
||||
// ── Tests ───────────────────────────────────────────────────
|
||||
|
||||
describe("episode reasoning seam (reconsiderCompletedEpisode)", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("uses the episode-aware prompt builder, not legacy buildGraphUpdatePrompt", async () => {
|
||||
const buildEpisodeAwareGraphPrompt = vi.fn().mockReturnValue("EPISODE_PROMPT");
|
||||
const buildGraphUpdatePrompt = vi.fn();
|
||||
const provider = {
|
||||
generateReconstruction: vi.fn().mockResolvedValue(makeProposalResponse()),
|
||||
};
|
||||
|
||||
// epiParams carries provider via its own key (as in the API contract)
|
||||
const result = await (await import("@/lib/graph/orchestrator.js")).reconsiderCompletedEpisode(
|
||||
{ ...TEST_EPISODE, provider },
|
||||
{ getModelName: () => "configured", buildEpisodePrompt: buildEpisodeAwareGraphPrompt },
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(buildEpisodeAwareGraphPrompt).toHaveBeenCalledWith({ episode: expect.any(Object) });
|
||||
expect(provider.generateReconstruction).toHaveBeenCalledWith("EPISODE_PROMPT", "configured");
|
||||
expect(buildGraphUpdatePrompt).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("calls provider.generateReconstruction exactly once with the built episode prompt", async () => {
|
||||
const buildEpisodeAwareGraphPrompt = vi.fn().mockReturnValue("EPISODE_PROMPT_V2");
|
||||
const provider = {
|
||||
generateReconstruction: vi.fn().mockResolvedValue(makeProposalResponse()),
|
||||
};
|
||||
|
||||
const { reconsiderCompletedEpisode } = await import("@/lib/graph/orchestrator.js");
|
||||
const result = await reconsiderCompletedEpisode(
|
||||
{ ...TEST_EPISODE, provider },
|
||||
{ getModelName: () => "configured", buildEpisodePrompt: buildEpisodeAwareGraphPrompt },
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(provider.generateReconstruction.mock.calls.length).toBe(1);
|
||||
expect(provider.generateReconstruction).toHaveBeenCalledWith("EPISODE_PROMPT_V2", "configured");
|
||||
});
|
||||
|
||||
it("reuses existing parser — returns parsed GraphUpdateProposal shape for valid JSON", async () => {
|
||||
const provider = {
|
||||
generateReconstruction: vi.fn().mockResolvedValue(JSON.stringify(makeProposalResponse())),
|
||||
};
|
||||
|
||||
const { reconsiderCompletedEpisode } = await import("@/lib/graph/orchestrator.js");
|
||||
const result = await reconsiderCompletedEpisode(
|
||||
TEST_EPISODE,
|
||||
{ getModelName: () => "configured", provider },
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.stage).toBe("proposal_ready");
|
||||
expect(result.proposal).toMatchObject({
|
||||
updatedNodes: [expect.objectContaining({ nodeId: "n-unknown" })],
|
||||
resolvedUnknownNodeIds: ["n-unknown"],
|
||||
answerMeaning: null,
|
||||
structuralActionRequired: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves existing parser error behaviour for invalid provider result", async () => {
|
||||
const provider = {
|
||||
generateReconstruction: vi.fn().mockResolvedValue("{ not valid json }"),
|
||||
};
|
||||
|
||||
const { reconsiderCompletedEpisode } = await import("@/lib/graph/orchestrator.js");
|
||||
const result = await reconsiderCompletedEpisode(
|
||||
TEST_EPISODE,
|
||||
{ getModelName: () => "configured", provider },
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.stage).toBe("proposal_validation");
|
||||
expect(result.error).toBe("Invalid graph update proposal");
|
||||
expect(result.statusCode).toBe(502);
|
||||
expect(result.proposalErrors).toBeInstanceOf(Array);
|
||||
});
|
||||
|
||||
it("does not perform any graph mutation or application", async () => {
|
||||
const applyValidatedProposal = vi.fn();
|
||||
const provider = {
|
||||
generateReconstruction: vi.fn().mockResolvedValue(makeProposalResponse()),
|
||||
};
|
||||
|
||||
const { reconsiderCompletedEpisode } = await import("@/lib/graph/orchestrator.js");
|
||||
const result = await reconsiderCompletedEpisode(
|
||||
TEST_EPISODE,
|
||||
{ getModelName: () => "configured", provider, applyValidatedProposal },
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(applyValidatedProposal).not.toHaveBeenCalled();
|
||||
|
||||
// No mutation of the input episode's graph
|
||||
expect(TEST_EPISODE.situationGraph.nodes.length).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user