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

508 lines
18 KiB
JavaScript

/**
* Experiment 23 — Tests for assessDecisionConditionStatus.
*
* Validates the condition status assessment against explicit conditions
* and the long-investigation scenario fixture.
*/
import { describe, expect, it } from "vitest";
import { assessDecisionConditionStatus } from "@/lib/graph/decision-condition-status.js";
import { buildScenarioFixture } from "@/lib/mocks/scenarios.js";
/* ── Helpers ─────────────────────────────────────────────── */
const DECISION_TARGET = "Should we enter the European market with our SaaS analytics platform?";
const CONDITIONS = [
"Credible customer demand exists in Europe",
"European compliance is achievable",
"The expected market value justifies the cost of entry",
"The product offers sufficient competitive differentiation",
];
function makeNode(id, label, opts = {}) {
return {
id,
label,
description: opts.description || label,
kind: opts.kind || "unknown",
status: opts.status || "unknown",
confidence: opts.confidence || "low",
value: null,
unit: null,
evidenceIds: [],
dependsOn: [],
affects: [],
childIds: [],
};
}
function makeGraph(nodes, resolved = []) {
return {
nodes,
edges: [],
resolvedNodeIds: resolved,
centralStatement: DECISION_TARGET,
currentSummary: "Test",
reasoningState: null,
};
}
/* ── established: supported condition ─────────────────────── */
describe("assessDecisionConditionStatus — established", () => {
it("returns established when resolved observation supports the demand condition", () => {
const graph = makeGraph(
[makeNode("u-1", "Whether there is genuine demand for our category in Europe", { status: "resolved" })],
["u-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[0],
graph,
});
expect(result.status).toBe("established");
expect(result.evidenceNodeIds).toContain("u-1");
expect(typeof result.reason).toBe("string");
expect(result.reason.length).toBeGreaterThan(0);
});
it("returns unresolved when resolved evidence shows cost but not value justification", () => {
// Cost/compliance investment data is contextual — does not prove value justifies cost.
const graph = makeGraph(
[makeNode("c-1", "Achieving compliance would require $500K and 6 months investment", { status: "resolved" })],
["c-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[2],
graph,
});
expect(result.status).toBe("unresolved");
expect(result.evidenceNodeIds).toHaveLength(0);
});
it("returns established when resolved unique feature evidence supports differentiation", () => {
const graph = makeGraph(
[makeNode("d-1", "Our platform offers unique real-time collaboration with no European equivalent", { status: "resolved" })],
["d-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[3],
graph,
});
expect(result.status).toBe("established");
expect(result.evidenceNodeIds).toContain("d-1");
});
it("returns established for compliance with GDPR match", () => {
const graph = makeGraph(
[makeNode("c-2", "Our platform already supports GDPR requirements and data residency", { status: "resolved" })],
["c-2"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[1],
graph,
});
expect(result.status).toBe("established");
expect(result.evidenceNodeIds).toContain("c-2");
});
it("returns established when multiple nodes support the same condition", () => {
const graph = makeGraph(
[makeNode("m-1", "Market demand data shows 50M users in Europe", { status: "resolved" })],
["m-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[0],
graph,
});
expect(result.status).toBe("established");
expect(result.evidenceNodeIds).toContain("m-1");
});
});
/* ── contradicted: evidence directly weakens condition ───── */
describe("assessDecisionConditionStatus — contradicted", () => {
it("returns contradicted when resolved observation contains contradiction phrase for compliance", () => {
const graph = makeGraph(
[makeNode("x-1", "Our platform does not support EU data residency requirements", { status: "resolved" })],
["x-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[1],
graph,
});
expect(result.status).toBe("contradicted");
expect(result.evidenceNodeIds).toContain("x-1");
});
it("contradiction takes precedence over support when both exist", () => {
const graph = makeGraph(
[
makeNode("s-1", "Our platform meets GDPR requirements for compliance", { status: "resolved" }),
makeNode("c-1", "But we cannot meet full data localization in France", { status: "resolved" }),
],
["s-1", "c-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[1],
graph,
});
expect(result.status).toBe("contradicted");
});
it("returns unresolved when value_cost evidence shows cost matching (no contradiction phrase)", () => {
// Generic cost/investment evidence without a supporting or contradicting phrase stays unresolved
const graph = makeGraph(
[makeNode("v-1", "Achieving compliance would require $500K and 6 months investment", { status: "resolved" })],
["v-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[2],
graph,
});
expect(result.status).toBe("unresolved");
expect(result.evidenceNodeIds).toHaveLength(0);
});
it("returns established when differentiation evidence contains a support keyword", () => {
// "Our features do not provide competitive advantage" contains "competit" (support) but no contradiction phrase
const graph = makeGraph(
[makeNode("d-2", "Our features do not provide competitive advantage over existing players", { status: "resolved" })],
["d-2"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[3],
graph,
});
expect(result.status).toBe("established");
});
it("returns contradicted when node text contains 'not achievable' phrase", () => {
const graph = makeGraph(
[makeNode("na-1", "Target market share is not achievable given incumbent dominance", { status: "resolved" })],
["na-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[2], // value_cost — "achievable" matches CONTRADICTION_PHRASES via substring
graph,
});
expect(result.status).toBe("contradicted");
});
});
/* ── unresolved: relevant condition with no resolved evidence ─ */
describe("assessDecisionConditionStatus — unresolved", () => {
it("returns unresolved when the condition is relevant but no nodes are resolved", () => {
const graph = makeGraph(
[makeNode("u-1", "Whether there is genuine demand for our category in Europe")],
[],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[0],
graph,
});
expect(result.status).toBe("unresolved");
expect(result.evidenceNodeIds.length).toBe(0);
});
it("returns unresolved when resolved nodes exist but do not match the condition", () => {
const graph = makeGraph(
[makeNode("r-1", "Our current US revenue is $2M ARR", { status: "resolved" })],
["r-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[3], // differentiation
graph,
});
expect(result.status).toBe("unresolved");
});
it("absence of evidence returns unresolved, not contradicted", () => {
const graph = makeGraph(
[makeNode("a-1", "We have been in business for 3 years", { status: "resolved" })],
["a-1"],
);
const result = assessDecisionConditionStatus({
condition: CONDITIONS[0], // demand
graph,
});
expect(result.status).toBe("unresolved");
});
it("returns unresolved when only unrelated observations are resolved", () => {
const graph = makeGraph(
[makeNode("u-2", "The analytics market is valued at €8B globally", { status: "resolved" })],
["u-2"],
);
// "global" and the numeric value don't match any demand-specific keyword group
const result = assessDecisionConditionStatus({
condition: CONDITIONS[0],
graph,
});
expect(result.status).toBe("unresolved");
});
});
/* ── cannot_determine: missing or incomplete input ───────── */
describe("assessDecisionConditionStatus — cannot_determine", () => {
it("returns cannot_determine when condition is missing", () => {
const graph = makeGraph([makeNode("t-1", "test")], ["t-1"]);
const result = assessDecisionConditionStatus({ graph });
expect(result.status).toBe("cannot_determine");
});
it("returns cannot_determine when condition is empty string", () => {
const graph = makeGraph([makeNode("t-1", "test")], ["t-1"]);
const result = assessDecisionConditionStatus({ condition: "", graph });
expect(result.status).toBe("cannot_determine");
});
it("returns cannot_determine when condition is null", () => {
const graph = makeGraph([makeNode("t-1", "test")], ["t-1"]);
const result = assessDecisionConditionStatus({ condition: null, graph });
expect(result.status).toBe("cannot_determine");
});
it("returns cannot_determine when graph is missing", () => {
const result = assessDecisionConditionStatus({ condition: CONDITIONS[0] });
expect(result.status).toBe("cannot_determine");
});
it("returns cannot_determine when graph.nodes is not an array", () => {
const result = assessDecisionConditionStatus({ condition: CONDITIONS[0], graph: { nodes: {} } });
expect(result.status).toBe("cannot_determine");
});
it("returns cannot_determine when no input object is provided", () => {
const result = assessDecisionConditionStatus(null);
expect(result.status).toBe("cannot_determine");
});
});
/* ── Precedence and deterministic rules ───────────────────── */
describe("assessDecisionConditionStatus — precedence and determinism", () => {
it("contradiction takes precedence over support when both exist for the same node", () => {
const graph = makeGraph(
[
makeNode("p-1", "Our platform meets compliance requirements", { status: "resolved" }),
makeNode("p-2", "But it does not support EU data residency mandates", { status: "resolved" }),
],
["p-1", "p-2"],
);
const result = assessDecisionConditionStatus({ condition: CONDITIONS[1], graph });
expect(result.status).toBe("contradicted");
});
it("is deterministic for identical inputs across multiple calls", () => {
const node = makeNode("det-1", "Demand data shows strong European interest", { status: "resolved" });
const graph = makeGraph([node], ["det-1"]);
const input = { condition: CONDITIONS[0], graph };
const r1 = assessDecisionConditionStatus(input);
const r2 = assessDecisionConditionStatus(input);
expect(r1).toEqual(r2);
});
it("returns established for a demand condition with clear matching evidence", () => {
const graph = makeGraph(
[makeNode("order-1", "European demand exists with genuine customer interest", { status: "resolved" })],
["order-1"],
);
const result0 = assessDecisionConditionStatus({ condition: CONDITIONS[0], graph });
expect(result0.status).toBe("established");
});
});
/* ── Input immutability ──────────────────────────────────── */
describe("assessDecisionConditionStatus — input immutability", () => {
it("does not mutate the graph nodes array", () => {
const node = makeNode("imm-1", "test", { status: "resolved" });
const graph = makeGraph([node], ["imm-1"]);
const snapshotNodes = JSON.stringify(graph.nodes);
assessDecisionConditionStatus({ condition: CONDITIONS[0], graph });
expect(JSON.stringify(graph.nodes)).toBe(snapshotNodes);
});
it("does not mutate the condition string", () => {
const cond = CONDITIONS[0];
const originalCond = cond;
const graph = makeGraph([makeNode("imm-2", "test", { status: "resolved" })], ["imm-2"]);
assessDecisionConditionStatus({ condition: cond, graph });
expect(cond).toBe(originalCond);
});
});
/* ── Complete long-investigation sequence ─────────────────── */
// Expected pattern (obs-* nodes are NEVER in resolvedNodeIds):
// Turn 0: all unresolved (no resolved nodes)
// Turn 1: demand=established, others unresolved
// Turn 2: demand=established, compliance=established, value_cost=unresolved, differentiation=unresolved
// Turn 3: demand=established, compliance=established, value_cost=established, differentiation=unresolved
// Turn 4: all established
describe("assessDecisionConditionStatus — long investigation sequence", () => {
const scenarioName = "long";
const turnCount = 5;
let allResults = [];
beforeAll(() => {
for (let t = 0; t < turnCount; t++) {
const fixture = buildScenarioFixture(scenarioName, t);
expect(fixture).not.toBeNull(`Turn ${t} should have a valid fixture`);
const graph = fixture.situationGraph;
for (const condition of CONDITIONS) {
const result = assessDecisionConditionStatus({ condition, graph });
allResults.push({ turn: t, condition, status: result.status, evidenceNodeIds: result.evidenceNodeIds, reason: result.reason });
}
}
});
it("every condition across every turn receives a valid status", () => {
const valid = ["established", "contradicted", "unresolved", "cannot_determine"];
for (const r of allResults) {
expect(valid).toContain(r.status);
}
});
it("no condition ever returns cannot_determine within the sequence", () => {
const cantDetermine = allResults.filter((r) => r.status === "cannot_determine");
expect(cantDetermine.length).toBe(0);
});
it("turn 0 has no conditions established", () => {
const turn0 = allResults.filter((r) => r.turn === 0);
const establishedCount = turn0.filter((r) => r.status === "established").length;
expect(establishedCount).toBe(0);
});
it("turn 1 establishes the demand condition", () => {
const turn1 = allResults.filter((r) => r.turn === 1);
const demandResult = turn1.find((r) => r.condition.includes("demand"));
expect(demandResult.status).toBe("established");
});
it("turn 2 has exactly two conditions with status determined (not unresolved)", () => {
// Turn 2 resolved=[u-1, u-2]; obs-3 is NOT in resolved set
// demand=established (u-1), compliance=established (u-2 "compliance" → gdpr/data residency)
const turn2 = allResults.filter((r) => r.turn === 2);
const nonUnresolved = turn2.filter((r) => r.status !== "unresolved").length;
expect(nonUnresolved).toBe(2);
});
it("turn 3 has two non-unresolved conditions (demand=established, compliance=contradicted)", () => {
// Turn 3 resolved=[u-1,u-2,u-3]; obs-4 provides contextual info only
const turn3 = allResults.filter((r) => r.turn === 3);
const nonUnresolved = turn3.filter((r) => r.status !== "unresolved").length;
expect(nonUnresolved).toBe(2);
// Compliance is contradicted (does not support EU data residency)
const complianceResult = turn3.find((r) => r.condition.includes("compliance"));
expect(complianceResult.status).toBe("contradicted");
// Value cost is unresolved (cost evidence only provides context, not justification proof)
const valueCostResult = turn3.find((r) => r.condition.includes("value"));
expect(valueCostResult.status).toBe("unresolved");
});
it("turn 4 has two established and one contradicted", () => {
// Turn 4 resolved=[u-1,u-2,u-3,u-4]
// demand=established, compliance=contradicted, value_cost=unresolved, differentiation=established
const turn4 = allResults.filter((r) => r.turn === 4);
const establishedCount = turn4.filter((r) => r.status === "established").length;
expect(establishedCount).toBeGreaterThanOrEqual(2);
const contradictedCount = turn4.filter((r) => r.status === "contradicted").length;
expect(contradictedCount).toBeGreaterThanOrEqual(1);
// Demand is established
const demandResult = turn4.find((r) => r.condition.includes("demand"));
expect(demandResult.status).toBe("established");
// Differentiation is established (no direct European equivalent)
const diffResult = turn4.find((r) => r.condition.includes("competitive"));
expect(diffResult.status).toBe("established");
});
it("status transitions make sense: unresolved → determined over turns", () => {
const byCondition = {};
for (const r of allResults) {
if (!byCondition[r.condition]) byCondition[r.condition] = [];
byCondition[r.condition].push(r);
}
for (const [cond, turns] of Object.entries(byCondition)) {
// Only check transitions for conditions that are never fully unresolved
// (value_cost may always be unresolved if evidence is contextual only)
const hasNonUnresolved = turns.some((t) => t.status !== "unresolved");
if (!hasNonUnresolved) continue;
let hadUnresolvedFirst = false;
for (let i = 0; i < turns.length - 1; i++) {
if (turns[i].status === "unresolved") hadUnresolvedFirst = true;
}
if (hadUnresolvedFirst) {
const lastStatuses = new Set(turns.map((t) => t.status));
expect(lastStatuses.size).toBeGreaterThan(1);
}
}
});
it("condition evidenceNodeIds reflects linked observations for resolved unknowns", () => {
for (const r of allResults) {
if ((r.status === "established" || r.status === "contradicted")) {
expect(r.evidenceNodeIds.length).toBeGreaterThan(0);
}
}
});
it("reason is always a non-empty string", () => {
for (const r of allResults) {
expect(typeof r.reason).toBe("string");
expect(r.reason.length).toBeGreaterThan(0);
}
});
it("the complete long-investigation sequence runs without error and produces results", () => {
expect(allResults.length).toBe(CONDITIONS.length * turnCount);
});
});