Files
confidence-engine/tests/graph/episode-reasoning-seam.test.js
T

197 lines
7.2 KiB
JavaScript

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);
});
it("resolves configured model via assertConfig fallback when no deps model is injected", async () => {
// Set minimal env vars for assertConfig to succeed in this specific test
const prevBaseUrl = process.env.OLLAMA_BASE_URL;
const prevModel = process.env.OLLAMA_MODEL;
process.env.OLLAMA_BASE_URL = "http://localhost:11434";
process.env.OLLAMA_MODEL = "test-model";
const modelCaptured = [];
const provider = {
generateReconstruction: vi.fn().mockImplementation((prompt, model) => {
modelCaptured.push(model);
return makeProposalResponse();
}),
};
// Reset modules so assertConfig reads fresh env vars
await vi.resetModules();
const { reconsiderCompletedEpisode } = await import("@/lib/graph/orchestrator.js");
// Empty deps — matches route.js call: reconsiderCompletedEpisode(prepared)
// The fix ensures assertConfig() provides a model instead of leaving it null
const result = await reconsiderCompletedEpisode(
{ ...TEST_EPISODE, provider },
{},
);
expect(result.success).toBe(true);
expect(modelCaptured.length).toBe(1);
expect(modelCaptured[0]).not.toBeNull();
expect(typeof modelCaptured[0]).toBe("string");
// Restore env
if (prevBaseUrl == null) delete process.env.OLLAMA_BASE_URL;
else process.env.OLLAMA_BASE_URL = prevBaseUrl;
if (prevModel == null) delete process.env.OLLAMA_MODEL;
else process.env.OLLAMA_MODEL = prevModel;
});
});