import { describe, expect, it } from "vitest"; import { hasRemainingMaterialFactors, shouldCloseDecision, isUserConfirmationOfNoRemainingUncertainty, } from "@/lib/graph/decision-sufficiency.js"; import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js"; // ── Confirmation detection tests ────────────────────────────── describe("isUserConfirmationOfNoRemainingUncertainty", () => { it("returns true for exact confirmation phrase: no other material uncertainty remains", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "There are no other material uncertainties between launching this year and waiting twelve months.", )).toBe(true); }); it("returns true for bounded paraphrase: no remaining material uncertainty", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "no remaining material uncertainty exists between the options.", )).toBe(true); }); it("returns true for plural variant: no other material uncertainties remain", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "There are no other material uncertainties between the options.", )).toBe(true); }); it("returns true for 'no further material uncertainty remains' (exact phrase via includes)", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "I confirm: no further material uncertainty remains.", )).toBe(true); }); it("rejects contradictory wording: 'still ... material'", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "There is still another material uncertainty.", )).toBe(false); }); it("rejects contradictory wording: 'am not saying'", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "I am not saying there are no other material uncertainties.", )).toBe(false); }); it("rejects negated phrase: 'not claiming'", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "I'm not claiming that all uncertainties are resolved.", )).toBe(false); }); it("rejects vague completion phrases like 'That's it'", () => { expect(isUserConfirmationOfNoRemainingUncertainty("That's it.")).toBe(false); }); it("returns false for non-confirmation answer", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "The customer has confirmed they will sign.", )).toBe(false); }); it("returns false for null input", () => { expect(isUserConfirmationOfNoRemainingUncertainty(null)).toBe(false); }); it("returns false for empty string", () => { expect(isUserConfirmationOfNoRemainingUncertainty("")).toBe(false); }); it("returns false for non-string input", () => { expect(isUserConfirmationOfNoRemainingUncertainty(42)).toBe(false); }); it("accepts 'no remaining material differences'", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "There are no remaining material differences between the options.", )).toBe(true); }); it("accepts 'nothing else material is uncertain'", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "Nothing else material is uncertain about this decision.", )).toBe(true); }); it("accepts bounded regex pattern: 'no other/further material ... between'", () => { expect(isUserConfirmationOfNoRemainingUncertainty( "No further material difference between the choices.", )).toBe(true); }); }); // ── Remaining-factor detection tests ────────────────────────── describe("hasRemainingMaterialFactors", () => { // Test 1 — containment-only factor (Route D) it("returns true when unresolved unknown is contained_in an option of the decision", () => { const decision = makeNode({ id: "n_decision", label: "Decision", kind: "unknown", status: "unknown", }); const optA = makeNode({ id: "opt_a", label: "Option A", kind: "option", status: "known", }); const factor = makeNode({ id: "n_factor", label: "Material factor", kind: "unknown", status: "unknown", }); const graph = makeGraph({ centralStatement: "test", currentSummary: "Decision context.", nodes: [decision, optA, factor], edges: [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), makeEdge({ fromNodeId: factor.id, toNodeId: optA.id, relationship: "contained_in" }), ], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true); }); // Test 2 — same factor resolved it("returns false when the containment-only factor is resolved", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const factor = makeNode({ id: "n_factor", label: "R", kind: "unknown", status: "resolved" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "Context.", nodes: [decision, optA, factor], edges: [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), makeEdge({ fromNodeId: factor.id, toNodeId: optA.id, relationship: "contained_in" }), ], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); }); // Test 3 — consequence-linked factor (Route C) it("returns true when unresolved unknown has affects/may_cause/causes to an option", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const factor = makeNode({ id: "n_factor", label: "F", kind: "unknown", status: "unknown" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "Context.", nodes: [decision, optA, factor], edges: [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), makeEdge({ fromNodeId: factor.id, toNodeId: optA.id, relationship: "may_cause" }), ], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true); }); // Test 4 — direct dependency (Route B) it("returns true when unresolved unknown directly depends_on the decision", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const depUnknown = makeNode({ id: "n_dep", label: "Dep", kind: "unknown", status: "unknown" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "Context.", nodes: [decision, depUnknown], edges: [ makeEdge({ fromNodeId: depUnknown.id, toNodeId: decision.id, relationship: "depends_on" }), ], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true); }); // Test 5 — hierarchy (Route A) childIds it("returns true when the decision has an unresolved child unknown via childIds", () => { const childUnknown = makeNode({ id: "n_child", label: "Child", kind: "unknown", status: "unknown" }); const decision = makeNode({ id: "n_decision", label: "D", kind: "unknown", status: "unknown", childIds: [childUnknown.id], }); childUnknown.parentId = decision.id; const graph = makeGraph({ centralStatement: "test", currentSummary: "Context.", nodes: [decision, childUnknown], edges: [], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(true); }); // Test 6 — weak relationship excluded it("returns false when only supports/measures connects unresolved node", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const weakUnknown = makeNode({ id: "n_weak", label: "W", kind: "unknown", status: "unknown" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "Context.", nodes: [decision, optA, weakUnknown], edges: [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), makeEdge({ fromNodeId: weakUnknown.id, toNodeId: optA.id, relationship: "supports" }), ], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); }); // Test 7 — decision node itself doesn't self-count it("returns false when the decision node itself is unknown with no factors", () => { const decision = makeNode({ id: "n_dec_self", label: "D", kind: "unknown", status: "unknown" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "Context.", nodes: [decision], edges: [], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); }); // Test 8 — status=known excluded it("returns false when unknown has status=known", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const knownUnknown = makeNode({ id: "n_known", label: "K", kind: "unknown", status: "known" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "Context.", nodes: [decision, optA, knownUnknown], edges: [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), makeEdge({ fromNodeId: knownUnknown.id, toNodeId: optA.id, relationship: "contained_in" }), ], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); }); // Test 9 — contradicted excluded it("returns false when unknown has status=contradicted", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const contra = makeNode({ id: "n_contra", label: "C", kind: "unknown", status: "contradicted" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "Context.", nodes: [decision, optA, contra], edges: [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), makeEdge({ fromNodeId: contra.id, toNodeId: optA.id, relationship: "contained_in" }), ], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); }); // Test 10 — non-unknown nodes excluded it("returns false when only observation-level unknowns are present", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const obsNode = makeNode({ id: "n_obs", label: "Obs", kind: "observation", status: "supported" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "Context.", nodes: [decision, optA, obsNode], edges: [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), makeEdge({ fromNodeId: obsNode.id, toNodeId: optA.id, relationship: "measures" }), ], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); }); // Test — arbitrary connectivity excluded it("returns false when unknown connected only via non-approved edges", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const farUnknown = makeNode({ id: "n_far", label: "F", kind: "unknown", status: "unknown" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "Context.", nodes: [decision, optA, farUnknown], edges: [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), makeEdge({ fromNodeId: farUnknown.id, toNodeId: optA.id, relationship: "other" }), ], }); expect(hasRemainingMaterialFactors(decision.id, graph)).toBe(false); }); }); // ── shouldCloseDecision predicate tests ─────────────────────── describe("shouldCloseDecision", () => { function makeClosureGraph({ extraFactor = null } = {}) { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const nodes = [decision, optA]; const edges = [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), ]; if (extraFactor) { nodes.push(extraFactor); } return { graph: makeGraph({ centralStatement: "test", currentSummary: "C.", nodes, edges }) }; } it("returns true when no remaining factors AND explicit confirmation present", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "C.", nodes: [decision, optA], edges: [makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" })], }); expect(shouldCloseDecision({ decisionNodeId: decision.id, graph, answer: "no other material uncertainty remains", })).toBe(true); }); it("returns false when no remaining factors BUT no confirmation", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "C.", nodes: [decision, optA], edges: [makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" })], }); expect(shouldCloseDecision({ decisionNodeId: decision.id, graph, answer: "The customer signed.", })).toBe(false); }); it("returns false when remaining factors present", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const remainingFactor = makeNode({ id: "n_rem", label: "R", kind: "unknown", status: "unknown" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "C.", nodes: [decision, optA, remainingFactor], edges: [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), makeEdge({ fromNodeId: remainingFactor.id, toNodeId: optA.id, relationship: "may_cause" }), ], }); expect(shouldCloseDecision({ decisionNodeId: decision.id, graph, answer: "no other material uncertainty remains", })).toBe(false); }); it("returns false when confirmation is contradictory", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "C.", nodes: [decision, optA], edges: [makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" })], }); expect(shouldCloseDecision({ decisionNodeId: decision.id, graph, answer: "There is still another material uncertainty.", })).toBe(false); }); it("uses pendingResolvedIds to treat a node as resolved", () => { const decision = makeNode({ id: "n_dec", label: "D", kind: "unknown", status: "unknown" }); const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); // factor still shows as unknown (not yet synced) const unresolvedFactor = makeNode({ id: "n_factor", label: "F", kind: "unknown", status: "unknown" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "C.", nodes: [decision, optA, unresolvedFactor], edges: [ makeEdge({ fromNodeId: optA.id, toNodeId: decision.id, relationship: "contained_in" }), makeEdge({ fromNodeId: unresolvedFactor.id, toNodeId: optA.id, relationship: "contained_in" }), ], }); // Without pendingResolvedIds → remaining > 0 → false expect(shouldCloseDecision({ decisionNodeId: decision.id, graph, answer: "no other material uncertainty remains", })).toBe(false); // With pendingResolvedIds including the factor → remaining = 0 + confirmed → true const pendingResolvedIds = new Set(["n_factor"]); expect(shouldCloseDecision({ decisionNodeId: decision.id, graph, answer: "no other material uncertainty remains", pendingResolvedIds, })).toBe(true); }); it("non-existent decision node with confirmation returns true (remaining = 0)", () => { const optA = makeNode({ id: "opt_a", label: "O", kind: "option", status: "known" }); const graph = makeGraph({ centralStatement: "test", currentSummary: "C.", nodes: [optA], edges: [], }); // countRemainingMaterialFactors returns 0 for non-existent node // so combined with confirmation, shouldCloseDecision returns true // (this matches original checkRemainingFactorsVirtual semantics which also returns 0) expect(shouldCloseDecision({ decisionNodeId: "n_nonexistent", graph, answer: "no other material uncertainty remains", })).toBe(true); // Without confirmation — still false expect(shouldCloseDecision({ decisionNodeId: "n_nonexistent", graph, answer: "Just confirming the customer signed.", })).toBe(false); }); });