Files
confidence-engine/tests/graph/decision-sufficiency.test.js
T

457 lines
18 KiB
JavaScript

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