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); }, ); });