import { describe, expect, it } from "vitest"; import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js"; import { formulateQuestion } from "@/lib/graph/question-formulator.js"; import { selectActiveUnknownCandidate } from "@/lib/graph/utils.js"; import { questionPriorityGeneralisationFixtures } from "@/tests/fixtures/question-priority-generalisation.js"; function clone(value) { return JSON.parse(JSON.stringify(value)); } function buildResolutionProposal(graph) { const activeNode = graph.nodes.find( (node) => node.id === graph.activeUnknownNodeId, ); const placeholderCandidate = graph.nodes.find( (node) => node.kind === "unknown" && node.id !== activeNode.id, ); return { addedNodes: [], updatedNodes: [ { nodeId: activeNode.id, previousStatus: activeNode.status, newStatus: "resolved", previousValue: activeNode.value ?? null, newValue: activeNode.value ?? "Resolved context answer", reason: "The resolved context unknown is treated as answered for fixture progression.", }, ], addedEdges: [], removedEdgeIds: [], resolvedUnknownNodeIds: [activeNode.id], affectedNodeIds: [], selectedQuestion: { nodeId: placeholderCandidate?.id, question: "Placeholder candidate question?", reason: "Candidate only; deterministic selector should override it.", }, }; } function assertQuestionStructure(question) { expect(question.match(/\?/g) || []).toHaveLength(1); expect(question).not.toMatch(/\?\s*(and|or)\b/i); expect(question).not.toMatch(/^What is\s+/i); expect(question).toMatch(/^(What|Who|When)\b/); } describe("question priority generalisation", () => { for (const fixture of questionPriorityGeneralisationFixtures) { it(`${fixture.scenario} selects a foundational unknown and singular answerable strategy`, () => { const originalGraph = clone(fixture.graph); const deterministicSelection = selectActiveUnknownCandidate( fixture.graph, [fixture.graph.activeUnknownNodeId], ); const result = applyValidatedProposal({ situationGraph: fixture.graph, proposal: buildResolutionProposal(fixture.graph), }); expect(result.success).toBe(true); expect(fixture.graph).toEqual(originalGraph); expect(result.graphUpdate.selectedQuestion?.question).toBe( "Placeholder candidate question?", ); expect(deterministicSelection.nodeId).toBe( result.selectedQuestion.nodeId, ); expect(fixture.acceptableFoundationalUnknownNodeIds).toContain( result.selectedQuestion.nodeId, ); expect(result.selectedQuestion.nodeId).not.toBe( fixture.graph.nodes[fixture.graph.nodes.length - 1].id, ); expect(fixture.acceptableQuestionStrategies).toContain( result.selectedQuestion.strategy, ); assertQuestionStructure(result.selectedQuestion.question); const lowerQuestion = result.selectedQuestion.question.toLowerCase(); for (const topic of fixture.prohibitedFirstTopics) { expect(lowerQuestion).not.toContain(topic.toLowerCase()); } const selectedNode = result.updatedSituationGraph.nodes.find( (node) => node.id === result.selectedQuestion.nodeId, ); const reformulated = formulateQuestion({ node: selectedNode, graph: result.updatedSituationGraph, context: { resolvedValues: ["Resolved context answer"], }, }); expect(reformulated.question).toBe(result.selectedQuestion.question); expect(clone(result.updatedSituationGraph)).toEqual( result.updatedSituationGraph, ); }); } it("reports all five selected unknowns and strategies", () => { const summary = questionPriorityGeneralisationFixtures.map((fixture) => { const result = applyValidatedProposal({ situationGraph: fixture.graph, proposal: buildResolutionProposal(fixture.graph), }); expect(result.success).toBe(true); return { scenario: fixture.scenario, nodeId: result.selectedQuestion.nodeId, strategy: result.selectedQuestion.strategy, }; }); expect(summary).toMatchInlineSnapshot(` [ { "nodeId": "hire-success-criteria", "scenario": "Should we hire another engineer?", "strategy": "decision_threshold", }, { "nodeId": "van-reliability-threshold", "scenario": "Should we replace the delivery vans?", "strategy": "decision_threshold", }, { "nodeId": "country-value-threshold", "scenario": "Should we launch in another country?", "strategy": "decision_threshold", }, { "nodeId": "project-benefit-threshold", "scenario": "Should we continue a project that is over budget?", "strategy": "decision_threshold", }, { "nodeId": "support-value-threshold", "scenario": "Should we introduce a paid support tier?", "strategy": "baseline_reconstruction", }, ] `); }); });