import { describe, expect, it } from "vitest"; import { formulateQuestion, selectInvestigationStrategy, } from "@/lib/graph/question-formulator.js"; import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js"; function makeGraphFor(node, extra = {}) { return makeGraph({ centralStatement: extra.centralStatement || "Decision context", nodes: [node, ...(extra.nodes || [])], edges: extra.edges || [], activeUnknownNodeId: node.id, resolvedNodeIds: extra.resolvedNodeIds || [], currentSummary: "Test summary", }); } describe("formulateQuestion", () => { it("commercial viability plus build decision produces a decision-threshold question", () => { const unknown = makeNode({ id: "n-commercial", label: "Uncertainty regarding the commercial value of the product", description: "Commercial justification remains unclear because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "high", parentId: "n-decision", }); const decision = makeNode({ id: "n-decision", label: "Build decision", description: "Decision introduced by the answer.", kind: "state", status: "known", confidence: "medium", childIds: [unknown.id], value: "Deciding whether to build the product", }); const graph = makeGraphFor(unknown, { nodes: [decision], resolvedNodeIds: [decision.id], }); const result = formulateQuestion({ node: unknown, graph }); expect(result.strategy).toBe("decision_threshold"); expect(result.question).toContain("What outcome"); expect(result.question.toLowerCase()).toContain("justify"); }); it("commercial viability does not produce a pricing-first question", () => { const unknown = makeNode({ id: "n-commercial", label: "Commercial viability", description: "Commercial viability remains unresolved because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "high", }); const graph = makeGraphFor(unknown); const result = formulateQuestion({ node: unknown, graph }); expect(result.question.toLowerCase()).not.toContain("price"); expect(result.question.toLowerCase()).not.toContain("pricing"); }); it("undefined term produces a definition question", () => { const unknown = makeNode({ id: "n-term", label: "Success criteria definition", description: "Need a definition of the term because the team uses it inconsistently.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.strategy).toBe("definition"); expect(result.question).toMatch(/^What does /); }); it("unsupported claim produces an evidence question", () => { const unknown = makeNode({ id: "n-claim", label: "Demand claim", description: "Need evidence because the claim has not been validated.", kind: "reported_claim", status: "provisional", confidence: "low", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.strategy).toBe("evidence_gathering"); expect(result.question).toContain("What evidence"); }); it("missing previous state produces a baseline question", () => { const unknown = makeNode({ id: "n-baseline", label: "Baseline conversion rate", description: "Need the previous baseline because the change cannot be assessed without it.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.strategy).toBe("baseline_reconstruction"); expect(result.question).toContain("What was the comparable state before"); }); it("conflicting claim produces a contradiction-resolution question", () => { const unknown = makeNode({ id: "n-conflict", label: "Conflicting churn claim", description: "Need to resolve the inconsistency because the current figures contradict each other.", kind: "unknown", status: "unknown", confidence: "medium", }); const contradiction = makeNode({ id: "n-contradiction", label: "Contradicted report", description: "Two sources disagree about churn.", kind: "conclusion", status: "contradicted", confidence: "low", childIds: [unknown.id], }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown, { nodes: [contradiction] }), }); expect(result.strategy).toBe("contradiction_resolution"); expect(result.question).toContain("resolve the contradiction"); }); it("constraint unknown uses evidence-gathering within the fixed strategy set", () => { const unknown = makeNode({ id: "n-constraint", label: "Budget constraint", description: "Need the main budget constraint because it limits the available options.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.strategy).toBe("evidence_gathering"); expect(result.question).toContain("What evidence"); }); it("the same unknown can produce different questions when paired with different strategies", () => { const unknown = makeNode({ id: "n-same-unknown", label: "Value threshold", description: "Need to resolve the value threshold.", kind: "unknown", status: "unknown", confidence: "high", }); const decisionGraph = makeGraphFor(unknown, { centralStatement: "We are deciding whether to launch this product.", nodes: [ makeNode({ id: "n-decision", label: "Launch decision", description: "Decision depends on the value threshold.", kind: "state", status: "known", confidence: "medium", childIds: [unknown.id], value: "Deciding whether to launch the product", }), ], }); const definitionGraph = makeGraphFor(unknown, { centralStatement: "The team uses the term value threshold inconsistently.", nodes: [ makeNode({ id: "n-definition", label: "Definition disagreement", description: "Need a definition of value threshold before comparing options.", kind: "state", status: "known", confidence: "medium", childIds: [unknown.id], }), ], }); const decisionResult = formulateQuestion({ node: unknown, graph: decisionGraph, }); const definitionResult = formulateQuestion({ node: unknown, graph: definitionGraph, }); expect(decisionResult.strategy).toBe("decision_threshold"); expect(definitionResult.strategy).toBe("definition"); expect(decisionResult.question).not.toBe(definitionResult.question); }); it("strategy selection is deterministic and explainable", () => { const unknown = makeNode({ id: "n-threshold", label: "Success threshold", description: "Need the success threshold because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "high", }); const graph = makeGraphFor(unknown, { centralStatement: "We need to decide whether to continue investing.", }); const first = selectInvestigationStrategy({ node: unknown, graph }); const second = selectInvestigationStrategy({ node: unknown, graph }); expect(first).toEqual(second); expect(first.key).toBe("decision_threshold"); expect(first.reason).toContain("threshold"); }); it("question is singular and answerable", () => { const unknown = makeNode({ id: "n-evidence", label: "Evidence of demand", description: "Need evidence of demand because the decision depends on it.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question.match(/\?/g) || []).toHaveLength(1); expect(result.question.toLowerCase()).not.toContain(" and "); }); it("awkward uncertainty phrasing is rejected via fallback", () => { const unknown = makeNode({ id: "n-weird", label: "Uncertainty regarding service reliability", description: "Unknown service reliability.", kind: "unknown", status: "unknown", confidence: "medium", }); const result = formulateQuestion({ node: unknown, graph: makeGraphFor(unknown), }); expect(result.question).not.toContain("How should uncertainty regarding"); expect(result.question).not.toContain( "What would resolve uncertainty regarding", ); }); });