wip(confidence-engine): checkpoint open-question presentation work
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { normaliseAnalysisResponse } from "@/lib/reconstruction/compatibility.js";
|
||||
import { reconstructionGenerationJsonSchema } from "@/lib/reconstruction/schema.js";
|
||||
|
||||
const mockGenerateReconstruction = vi.fn();
|
||||
|
||||
@@ -17,6 +18,7 @@ vi.mock("@/lib/llm/provider.js", () => ({
|
||||
getProvider: () => ({
|
||||
generateReconstruction: (...args) => mockGenerateReconstruction(...args),
|
||||
}),
|
||||
getProviderModelName: () => "test-model",
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/reconstruction/prompt.js", () => ({
|
||||
@@ -134,13 +136,117 @@ describe("analyseScenario compatibility", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(injectedProvider.generateReconstruction).toHaveBeenCalledWith(
|
||||
"prompt",
|
||||
"experiment-model",
|
||||
);
|
||||
expect(injectedProvider.generateReconstruction).toHaveBeenCalledTimes(1);
|
||||
const [prompt, modelName, outputSchema, options] =
|
||||
injectedProvider.generateReconstruction.mock.calls[0];
|
||||
expect(prompt).toBe("prompt");
|
||||
expect(modelName).toBe("experiment-model");
|
||||
expect(outputSchema).toEqual(reconstructionGenerationJsonSchema);
|
||||
expect(options.outputZodSchema.safeParse).toBeTypeOf("function");
|
||||
expect(
|
||||
options.outputZodSchema.safeParse({
|
||||
inputClassification: {
|
||||
primaryType: "other",
|
||||
secondaryTypes: [],
|
||||
reasoningModes: [],
|
||||
classificationReason: "reason",
|
||||
confidence: "medium",
|
||||
},
|
||||
reconstruction: {
|
||||
summary: "summary",
|
||||
actors: [],
|
||||
systemsOrObjects: [],
|
||||
expectedStates: [],
|
||||
observedStates: [],
|
||||
differences: [],
|
||||
knownTransitions: [],
|
||||
unexplainedTransitions: [],
|
||||
contradictions: [],
|
||||
importantUnknowns: [],
|
||||
plausibleInterpretations: [],
|
||||
relationships: [],
|
||||
},
|
||||
evidence: [],
|
||||
nextQuestion: {
|
||||
id: "q1",
|
||||
question: "What next?",
|
||||
targets: [],
|
||||
reason: "reason",
|
||||
expectedInformationValue: "low",
|
||||
reasoningMode: "other",
|
||||
},
|
||||
}).success,
|
||||
).toBe(true);
|
||||
expect(mockGenerateReconstruction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves canonical reconstruction and reports invalid presentation nonfatally", async () => {
|
||||
const response = {
|
||||
inputClassification: {
|
||||
primaryType: "other",
|
||||
secondaryTypes: [],
|
||||
reasoningModes: [],
|
||||
classificationReason: "reason",
|
||||
confidence: "medium",
|
||||
},
|
||||
reconstruction: {
|
||||
summary: "summary",
|
||||
actors: [],
|
||||
systemsOrObjects: [],
|
||||
expectedStates: [],
|
||||
observedStates: [],
|
||||
differences: [],
|
||||
knownTransitions: [],
|
||||
unexplainedTransitions: [],
|
||||
contradictions: [],
|
||||
importantUnknowns: [
|
||||
{
|
||||
id: "unknown-1",
|
||||
description: "Whether workers can be recruited in time.",
|
||||
question: "Whether workers can be recruited in time?",
|
||||
confidence: "high",
|
||||
},
|
||||
],
|
||||
plausibleInterpretations: [],
|
||||
relationships: [],
|
||||
},
|
||||
evidence: [],
|
||||
nextQuestion: {
|
||||
id: "q1",
|
||||
question: "What recruitment evidence exists?",
|
||||
targets: ["unknown-1"],
|
||||
reason: "reason",
|
||||
expectedInformationValue: "high",
|
||||
reasoningMode: "other",
|
||||
},
|
||||
};
|
||||
const injectedProvider = {
|
||||
generateReconstruction: vi.fn().mockResolvedValue(response),
|
||||
};
|
||||
const { analyseScenario } = await import("@/lib/analysis.js");
|
||||
|
||||
const result = await analyseScenario("Scenario text", {
|
||||
reconstructionProvider: injectedProvider,
|
||||
reconstructionModelName: "experiment-model",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.validationStatus).toBe("valid");
|
||||
expect(result.presentationValidationStatus).toBe("partial");
|
||||
expect(result.reconstruction.importantUnknowns[0]).toEqual({
|
||||
id: "unknown-1",
|
||||
description: "Whether workers can be recruited in time.",
|
||||
confidence: "high",
|
||||
});
|
||||
expect(result.presentationValidationIssues).toEqual([
|
||||
expect.objectContaining({
|
||||
category: "question_indirect_proposition",
|
||||
unknownId: "unknown-1",
|
||||
severity: "nonfatal",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves an attempted provider API path on provider failure", async () => {
|
||||
const providerError = new Error("Provider failed");
|
||||
providerError.providerApiPath = "/api/chat";
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { promises as fs } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
reconstructionGenerationJsonSchema,
|
||||
reconstructionGenerationSchema,
|
||||
reconstructionV2Schema,
|
||||
} from "@/lib/reconstruction/schema.js";
|
||||
import {
|
||||
validateOpenQuestionCandidate,
|
||||
validateReconstructionOpenQuestions,
|
||||
} from "@/lib/reconstruction/open-question-presentation.js";
|
||||
|
||||
function makeAnalysisResponse(questionValue, includeQuestion = true) {
|
||||
const unknown = {
|
||||
id: "unknown-workforce",
|
||||
description:
|
||||
"Whether necessary skilled production workers can be recruited within the required project timeframe.",
|
||||
confidence: "high",
|
||||
};
|
||||
if (includeQuestion) unknown.question = questionValue;
|
||||
|
||||
return {
|
||||
inputClassification: {
|
||||
primaryType: "other",
|
||||
secondaryTypes: [],
|
||||
reasoningModes: [],
|
||||
classificationReason: "A delivery uncertainty is central to the situation.",
|
||||
confidence: "high",
|
||||
},
|
||||
reconstruction: {
|
||||
summary: "Delivery depends on recruiting skilled production workers.",
|
||||
actors: [],
|
||||
systemsOrObjects: [],
|
||||
expectedStates: [],
|
||||
observedStates: [],
|
||||
differences: [],
|
||||
knownTransitions: [],
|
||||
unexplainedTransitions: [],
|
||||
contradictions: [],
|
||||
importantUnknowns: [unknown],
|
||||
plausibleInterpretations: [],
|
||||
relationships: [],
|
||||
},
|
||||
evidence: [],
|
||||
nextQuestion: {
|
||||
id: "next-1",
|
||||
question: "What recruitment evidence is available?",
|
||||
targets: ["unknown-workforce"],
|
||||
reason: "Recruitment feasibility is unresolved.",
|
||||
expectedInformationValue: "high",
|
||||
reasoningMode: "other",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("new reconstruction generation contract", () => {
|
||||
it("requires a non-empty question for every generated important unknown", () => {
|
||||
expect(
|
||||
reconstructionGenerationSchema.safeParse(
|
||||
makeAnalysisResponse(undefined, false),
|
||||
).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
reconstructionGenerationSchema.safeParse(makeAnalysisResponse(" ")).success,
|
||||
).toBe(false);
|
||||
expect(
|
||||
reconstructionGenerationSchema.safeParse(
|
||||
makeAnalysisResponse(
|
||||
"Can the necessary skilled production workers be recruited within the required project timeframe?",
|
||||
),
|
||||
).success,
|
||||
).toBe(true);
|
||||
|
||||
const unknownSchema =
|
||||
reconstructionGenerationJsonSchema.properties.reconstruction.properties
|
||||
.importantUnknowns.items;
|
||||
expect(unknownSchema.required).toContain("description");
|
||||
expect(unknownSchema.required).toContain("question");
|
||||
expect(unknownSchema.properties.question.minLength).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps the durable compatibility schema tolerant of a missing question", () => {
|
||||
expect(
|
||||
reconstructionV2Schema.safeParse(makeAnalysisResponse(undefined, false)).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Open Question presentation validation", () => {
|
||||
it("accepts a trimmed direct interrogative", () => {
|
||||
expect(
|
||||
validateOpenQuestionCandidate(
|
||||
" Can the necessary workers be recruited in time? ",
|
||||
),
|
||||
).toEqual({
|
||||
valid: true,
|
||||
question: "Can the necessary workers be recruited in time?",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses canonical UI fallback without a technical explanation", async () => {
|
||||
const source = await fs.readFile(
|
||||
join(process.cwd(), "components/reasoning-workspace.jsx"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(source).toContain("const openQuestion = node.openQuestion || node.label;");
|
||||
expect(source).not.toContain(
|
||||
"Canonical uncertainty — direct question wording unavailable.",
|
||||
);
|
||||
expect(source).toContain("startFocused(n.id);");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["question_missing", undefined, false],
|
||||
["question_empty", " ", true],
|
||||
["question_indirect_proposition", "Whether workers can be recruited in time", true],
|
||||
["question_indirect_proposition", "Whether workers can be recruited in time?", true],
|
||||
["question_not_interrogative", "Workers can be recruited in time.", true],
|
||||
])("classifies %s nonfatally", (category, candidate, includeQuestion) => {
|
||||
const input = makeAnalysisResponse(candidate, includeQuestion).reconstruction;
|
||||
const canonicalBefore = input.importantUnknowns[0].description;
|
||||
const idBefore = input.importantUnknowns[0].id;
|
||||
|
||||
const result = validateReconstructionOpenQuestions(input);
|
||||
|
||||
expect(result.reconstruction.importantUnknowns[0]).not.toHaveProperty("question");
|
||||
expect(result.reconstruction.importantUnknowns[0].description).toBe(
|
||||
canonicalBefore,
|
||||
);
|
||||
expect(result.reconstruction.importantUnknowns[0].id).toBe(idBefore);
|
||||
expect(result.issues).toEqual([
|
||||
expect.objectContaining({
|
||||
category,
|
||||
unknownId: idBefore,
|
||||
severity: "nonfatal",
|
||||
path: ["reconstruction", "importantUnknowns", 0, "question"],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user