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