Files
confidence-engine/tests/graph/open-question-presentation.test.js
T

204 lines
8.3 KiB
JavaScript

import { describe, expect, it } from "vitest";
import { buildInitialGraph } from "@/lib/graph/builder.js";
import { formulateQuestionForTarget } from "@/lib/graph/focused-investigation.js";
import { makeGraph, makeNodeId } from "@/lib/graph/schema.js";
import { buildFacilitatorViewModel } from "@/lib/presentation/facilitator-view-adapter.js";
import { buildPrompt } from "@/lib/reconstruction/prompt.js";
import { validateReconstructionOpenQuestions } from "@/lib/reconstruction/open-question-presentation.js";
import { reconstructionV2Schema } from "@/lib/reconstruction/schema.js";
function reconstructionWithUnknowns(importantUnknowns) {
return {
inputClassification: {
primaryType: "other",
secondaryTypes: [],
reasoningModes: ["identify_missing_information"],
classificationReason: "The scenario contains unresolved factors.",
confidence: "medium",
},
reconstruction: {
summary: "A situation with unresolved factors.",
actors: [],
systemsOrObjects: [],
expectedStates: [],
observedStates: [],
differences: [],
knownTransitions: [],
unexplainedTransitions: [],
contradictions: [],
importantUnknowns,
plausibleInterpretations: [],
relationships: [],
},
evidence: [],
nextQuestion: {
id: "next-question",
question: "What information would clarify the situation?",
targets: [],
reason: "The situation has unresolved factors.",
expectedInformationValue: "medium",
reasoningMode: "identify_missing_information",
},
};
}
const CASES = [
{
name: "recruitment uncertainty",
description: "Whether skilled production staff can be recruited within the required timeframe",
question: "Can skilled production staff be recruited within the required timeframe?",
terms: ["skilled production staff", "required timeframe"],
},
{
name: "demand uncertainty",
description: "The level of demand in the North West market needed to justify the expansion cost",
question: "What level of demand in the North West market is needed to justify the expansion cost?",
terms: ["North West market", "justify the expansion cost"],
},
{
name: "historical uncertainty",
description: "Whether the prior staffing level was sufficient before the expansion",
question: "Was the prior staffing level sufficient before the expansion?",
terms: ["prior staffing level", "before the expansion"],
},
{
name: "non-business uncertainty",
description: "The time at which the missing hiker last used the eastern trail",
question: "When did the missing hiker last use the eastern trail?",
terms: ["missing hiker", "eastern trail"],
},
];
describe("Open Question presentation", () => {
it.each(["v0.2", "v0.3", "v0.4", "v0.5"])("instructs reconstruction prompt %s to produce a direct Open Question", async (version) => {
const { prompt } = await buildPrompt("A scenario with an unresolved condition.", version);
expect(prompt).toContain("importantUnknown");
expect(prompt).toContain("natural, direct interrogative");
expect(prompt).toContain('"question"');
expect(prompt).toContain('"Whether …"');
});
it.each(CASES)("preserves $name meaning while displaying a direct interrogative", ({ description, question, terms }) => {
const parsed = reconstructionV2Schema.parse(
reconstructionWithUnknowns([{ id: "unknown", description, question, confidence: "medium" }]),
);
const graph = buildInitialGraph({ reconstruction: parsed.reconstruction, evidence: [] });
const unknown = graph.nodes.find((node) => node.kind === "unknown");
const viewModel = buildFacilitatorViewModel({ nodes: graph.nodes, resolvedIds: new Set() });
expect(unknown.label).toBe(description);
expect(unknown.description).toBe(description);
expect(unknown.openQuestion).toBe(question);
expect(viewModel.investigating.items).toContain(question);
expect(question).toMatch(/\?$/);
expect(question).not.toMatch(/^whether\b/i);
expect(question).not.toMatch(/^before whether\b/i);
for (const term of terms) expect(question.toLowerCase()).toContain(term.toLowerCase());
});
it("leaves canonical identity, explicit target selection, and focused formulation unchanged", () => {
const description = CASES[0].description;
const question = CASES[0].question;
const graphData = buildInitialGraph({
reconstruction: reconstructionWithUnknowns([
{ id: "recruitment", description, question, confidence: "medium" },
]).reconstruction,
evidence: [],
});
const target = graphData.nodes.find((node) => node.kind === "unknown");
const graph = makeGraph({
centralStatement: "A manufacturer is considering expansion.",
nodes: graphData.nodes,
edges: graphData.edges,
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "Expansion depends on recruitment capacity.",
});
const before = JSON.stringify(graph);
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: target.id });
expect(target.id).toBe(makeNodeId(description));
expect(target.id).not.toBe(makeNodeId(question));
expect(result.success).toBe(true);
expect(result.targetNodeId).toBe(target.id);
expect(result.question).toMatch(/\?$/);
expect(result.question.toLowerCase()).toContain("staff");
expect(JSON.stringify(graph)).toBe(before);
});
it("keeps persisted legacy unknowns valid and falls back to canonical text when no display question exists", () => {
const legacyDescription = "Whether the figures are comparable";
const legacyNode = makeGraph({
centralStatement: "Compare two figures.",
nodes: [
{
id: makeNodeId(legacyDescription),
label: legacyDescription,
description: legacyDescription,
kind: "unknown",
status: "unknown",
confidence: "medium",
evidenceIds: [],
dependsOn: [],
affects: [],
parentId: null,
childIds: [],
},
],
edges: [],
activeUnknownNodeId: null,
resolvedNodeIds: [],
currentSummary: "The comparison remains unresolved.",
});
const viewModel = buildFacilitatorViewModel({ nodes: legacyNode.nodes, resolvedIds: new Set() });
expect(viewModel.investigating.items).toContain(legacyDescription);
expect(legacyNode.nodes[0]).not.toHaveProperty("openQuestion");
});
it.each([
["missing", undefined, false],
["empty", " ", true],
["whether fragment", "Whether the facility can recruit workers in time", true],
["whether fragment with punctuation", "Whether the facility can recruit workers in time?", true],
["statement", "The facility can recruit workers in time.", true],
])(
"keeps an invalid %s presentation canonically selectable and investigable",
(_name, question, includeQuestion) => {
const description =
"Whether the facility can recruit skilled workers within the launch timeframe.";
const unknown = {
id: "unknown",
description,
confidence: "medium",
};
if (includeQuestion) unknown.question = question;
const reconstruction = reconstructionWithUnknowns([unknown]).reconstruction;
const validated = validateReconstructionOpenQuestions(reconstruction);
const graphParts = buildInitialGraph({
reconstruction: validated.reconstruction,
evidence: [],
});
const graph = makeGraph({
centralStatement: "The facility launch depends on workforce availability.",
nodes: graphParts.nodes,
edges: graphParts.edges,
currentSummary: "Workforce availability remains unresolved.",
});
const graphUnknown = graph.nodes.find((node) => node.kind === "unknown");
const focused = formulateQuestionForTarget({
situationGraph: graph,
targetNodeId: graphUnknown.id,
});
expect(validated.issues).toHaveLength(1);
expect(graphUnknown.label).toBe(description);
expect(graphUnknown.description).toBe(description);
expect(graphUnknown).not.toHaveProperty("openQuestion");
expect(graphUnknown.id).toBe(makeNodeId(description));
expect(focused.success).toBe(true);
expect(focused.targetNodeId).toBe(graphUnknown.id);
},
);
});