fix: normalise compatible live reconstruction responses
This commit is contained in:
@@ -46,6 +46,9 @@ function makeAnalysisResult(overrides = {}) {
|
||||
id: "q-1",
|
||||
question: "What denominator is being used for the complaint rate?",
|
||||
},
|
||||
compatibilityApplied: false,
|
||||
compatibilityChanges: [],
|
||||
compatibilityWarnings: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -171,6 +174,29 @@ describe("lib/graph/orchestrator startCase", () => {
|
||||
expect(result.selectedQuestion).toBeNull();
|
||||
});
|
||||
|
||||
it("includes compatibility diagnostics when provided by analysis", async () => {
|
||||
mockAnalyseScenario.mockResolvedValue(
|
||||
makeAnalysisResult({
|
||||
compatibilityApplied: true,
|
||||
compatibilityChanges: [
|
||||
{
|
||||
path: ["evidence", 0, "source"],
|
||||
change: "Converted null source to undefined",
|
||||
},
|
||||
],
|
||||
compatibilityWarnings: [
|
||||
"Applied deterministic reconstruction compatibility normalisation",
|
||||
],
|
||||
}),
|
||||
);
|
||||
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
||||
|
||||
const result = await startCase({ scenario: "Scenario text" });
|
||||
|
||||
expect(result.diagnostics.compatibilityApplied).toBe(true);
|
||||
expect(result.diagnostics.compatibilityChanges).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("exports placeholder updateCase", async () => {
|
||||
const { updateCase } = await import("@/lib/graph/orchestrator.js");
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { normaliseAnalysisResponse } from "@/lib/reconstruction/compatibility.js";
|
||||
|
||||
const mockGenerateReconstruction = vi.fn();
|
||||
|
||||
vi.mock("@/lib/config.js", () => ({
|
||||
getConfig: () => ({
|
||||
ok: true,
|
||||
config: {
|
||||
OLLAMA_BASE_URL: "http://example.test",
|
||||
OLLAMA_MODEL: "test-model",
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/llm/provider.js", () => ({
|
||||
getProvider: () => ({
|
||||
generateReconstruction: (...args) => mockGenerateReconstruction(...args),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/reconstruction/prompt.js", () => ({
|
||||
buildPrompt: async () => ({ prompt: "prompt", version: "v0.3" }),
|
||||
PROMPT_VERSIONS: ["v0.1", "v0.2", "v0.3"],
|
||||
DEFAULT_PROMPT_VERSION: "v0.3",
|
||||
}));
|
||||
|
||||
describe("normaliseAnalysisResponse", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("leaves already-valid responses unchanged", () => {
|
||||
const input = {
|
||||
evidence: [
|
||||
{
|
||||
id: "ev1",
|
||||
description: "x",
|
||||
evidenceType: "reported_statement",
|
||||
confidence: "medium",
|
||||
importance: "important",
|
||||
source: "report",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = normaliseAnalysisResponse(input);
|
||||
|
||||
expect(result.normalised).toEqual(input);
|
||||
expect(result.changesApplied).toEqual([]);
|
||||
});
|
||||
|
||||
it("normalises null evidence source deterministically", () => {
|
||||
const input = {
|
||||
evidence: [
|
||||
{
|
||||
id: "ev1",
|
||||
description: "x",
|
||||
evidenceType: "reported_statement",
|
||||
confidence: "medium",
|
||||
importance: "important",
|
||||
source: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = normaliseAnalysisResponse(input);
|
||||
|
||||
expect(result.normalised.evidence[0]).not.toHaveProperty("source");
|
||||
expect(result.changesApplied).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("does not invent a next question", () => {
|
||||
const input = { evidence: [] };
|
||||
const result = normaliseAnalysisResponse(input);
|
||||
expect(result.normalised.nextQuestion).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not repair missing reasoning content", () => {
|
||||
const input = { evidence: [{ source: null }] };
|
||||
const result = normaliseAnalysisResponse(input);
|
||||
expect(result.normalised.reconstruction).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("analyseScenario compatibility", () => {
|
||||
it("succeeds when the only mismatch is null evidence source", async () => {
|
||||
mockGenerateReconstruction.mockResolvedValue({
|
||||
inputClassification: {
|
||||
primaryType: "unexplained_change",
|
||||
secondaryTypes: [],
|
||||
reasoningModes: ["validate_measurement"],
|
||||
classificationReason: "reason",
|
||||
confidence: "medium",
|
||||
},
|
||||
reconstruction: {
|
||||
summary: "summary",
|
||||
actors: [],
|
||||
systemsOrObjects: [],
|
||||
expectedStates: [],
|
||||
observedStates: [],
|
||||
differences: [],
|
||||
knownTransitions: [],
|
||||
unexplainedTransitions: [],
|
||||
contradictions: [],
|
||||
importantUnknowns: [],
|
||||
plausibleInterpretations: [],
|
||||
},
|
||||
evidence: [
|
||||
{
|
||||
id: "ev1",
|
||||
description: "desc",
|
||||
evidenceType: "reported_statement",
|
||||
source: null,
|
||||
attribution: null,
|
||||
confidence: "medium",
|
||||
importance: "important",
|
||||
},
|
||||
],
|
||||
nextQuestion: {
|
||||
id: "q1",
|
||||
question: "What denominator?",
|
||||
targets: ["observedStates"],
|
||||
reason: "reason",
|
||||
expectedInformationValue: "high",
|
||||
reasoningMode: "validate_measurement",
|
||||
},
|
||||
});
|
||||
|
||||
const { analyseScenario } = await import("@/lib/analysis.js");
|
||||
const result = await analyseScenario("Scenario text", {
|
||||
promptVersion: "v0.3",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.compatibilityApplied).toBe(true);
|
||||
expect(result.compatibilityChanges).toHaveLength(1);
|
||||
expect(result.evidence[0]).not.toHaveProperty("source");
|
||||
expect(result.nextQuestion.question).toBe("What denominator?");
|
||||
});
|
||||
|
||||
it("still fails when required reasoning content is missing", async () => {
|
||||
mockGenerateReconstruction.mockResolvedValue({
|
||||
evidence: [
|
||||
{
|
||||
id: "ev1",
|
||||
description: "desc",
|
||||
evidenceType: "reported_statement",
|
||||
source: null,
|
||||
attribution: null,
|
||||
confidence: "medium",
|
||||
importance: "important",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { analyseScenario } = await import("@/lib/analysis.js");
|
||||
const result = await analyseScenario("Scenario text", {
|
||||
promptVersion: "v0.3",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.compatibilityApplied).toBe(true);
|
||||
expect(result.nextQuestion).toBeUndefined();
|
||||
});
|
||||
|
||||
it("malformed JSON still fails", async () => {
|
||||
mockGenerateReconstruction.mockResolvedValue("{not valid json");
|
||||
|
||||
const { analyseScenario } = await import("@/lib/analysis.js");
|
||||
const result = await analyseScenario("Scenario text", {
|
||||
promptVersion: "v0.3",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.compatibilityApplied).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user