/** * 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 evidence mentions a unique feature with european equivalent in scope", () => { // The differentiation concept family now includes "european equivalent" as a related keyword. // This evidence phrase matches, giving direct_match scope + supports → established. 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 when evidence has direct_match scope and supports differentiation", () => { // Evidence contains core keyword "differentiat" → direct_match + supports → established const graph = makeGraph( [makeNode("d-3", "Our product offers a clear competitive differentiation against existing players", { status: "resolved" })], ["d-3"], ); const result = assessDecisionConditionStatus({ condition: CONDITIONS[3], graph, }); expect(result.status).toBe("established"); expect(result.evidenceNodeIds).toContain("d-3"); }); 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"], ); // Use present-state condition (not future-feasibility) to validate GDPR keyword matching. const result = assessDecisionConditionStatus({ condition: "The platform currently supports EU data residency requirements", 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"], ); // Use present-state condition to validate contradiction detection. const result = assessDecisionConditionStatus({ condition: "The platform currently supports EU data residency requirements", graph, }); expect(result.status).toBe("contradicted"); expect(result.evidenceNodeIds).toContain("x-1"); }); it("contradiction takes precedence over support only within direct scope", () => { // s-1 gets direct_match scope (present-state evidence about present compliance) // c-1 gets unrelated scope (evidence text does not match compliance category) // With scope-aware logic: only s-1 contributes → established, contradicted status = unresolved 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"], ); // Use present-state condition (not future-feasibility) to test scope precedence. const result = assessDecisionConditionStatus({ condition: "The platform currently supports EU data residency requirements", graph, }); // Only direct-scope evidence counts; s-1 supports, c-1 is unrelated → established expect(result.status).toBe("established"); }); it("contradiction takes precedence over support within the same direct scope", () => { // Both evidence nodes have direct_match scope for compliance 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"], ); // Use present-state condition (not future-feasibility) to test precedence. const result = assessDecisionConditionStatus({ condition: "The platform currently supports EU data residency requirements", 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 unresolved when evidence contains 'not achievable' but scope is unrelated", () => { // Evidence mentions "not achievable" (a contradiction phrase) but is about market share, // not value_cost. Scope check correctly classifies as unrelated → contradiction ignored. 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 — unrelated subject → scope=unrelated graph, }); expect(result.status).toBe("unresolved"); }); it("returns unresolved when evidence scope is different_timeframe for future-feasibility condition", () => { // A condition using "can be achieved" triggers the different_timeframe scope check // because the scope module recognises this as a future-feasibility phrase. const graph = makeGraph( [makeNode("c-3", "Our platform does not currently support EU data residency requirements", { status: "resolved" })], ["c-3"], ); // This condition text triggers different_timeframe scope detection (contains "can be achieved"), // so present-state evidence cannot establish or contradict it. const result = assessDecisionConditionStatus({ condition: "European compliance can be achieved", graph, }); expect(result.status).toBe("unresolved"); }); it("present-state compliance condition with contradictory evidence returns contradicted", () => { // Focused test: a genuinely present-state compliance condition should be contradicted // when evidence directly negates the claim. Requires a separate unknown node for category // matching and an observation-type node as contradicting evidence. const cs1 = makeNode("cs-1", "Our platform does not support EU data residency requirements", { kind: "observation", status: "resolved" }); const ux = makeNode("u-x", "Whether our product meets EU compliance requirements", { kind: "unknown", status: "resolved" }); const graph = makeGraph([cs1, ux], ["cs-1", "u-x"]); const result = assessDecisionConditionStatus({ condition: "The platform currently supports EU data residency requirements", graph, }); expect(result.status).toBe("contradicted"); }); // ── Experiment 25B: actual compliance fixture wording ─────── it("compliance condition with 'is achievable' returns unresolved (present evidence does not settle future feasibility)", () => { const graph = makeGraph( [makeNode("c-4", "Our platform does not currently support EU data residency requirements", { status: "resolved" })], ["c-4"], ); // "is achievable" is a future-feasibility marker → different_timeframe scope → unresolved const result = assessDecisionConditionStatus({ condition: "European compliance is achievable", graph, }); expect(result.status).toBe("unresolved"); }); it("differentiation condition with actual fixture wording returns established", () => { // The differentiation concept family now includes "european equivalent" as a related keyword // so the evidence directly shares the differentiation concept with the condition. // Uses proper unknown + observation structure for edge-based assessment. const unk = makeNode( "u-diff", "Whether we have competitive differentiation against existing European players", { kind: "unknown", status: "resolved" }, ); const ev = makeNode( "obs-diff", "Our real-time collaboration feature has no direct European equivalent and aligns with EU procurement trends", { kind: "observation", status: "resolved" }, ); const graph = makeGraph([unk, ev], ["u-diff", "obs-diff"]); graph.edges = [{ id: "e-diff", fromNodeId: "obs-diff", toNodeId: "u-diff", relationship: "supports", confidence: "medium", description: "obs-diff -> u-diff" }]; const result = assessDecisionConditionStatus({ condition: "The product offers sufficient competitive differentiation", graph, }); expect(result.status).toBe("established"); }); }); /* ── 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"], ); // Use present-state condition (not future-feasibility) to test precedence. const result = assessDecisionConditionStatus({ condition: "The platform currently supports EU data residency requirements", 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 one condition determined (demand=established, compliance=unresolved due to different_timeframe)", () => { // Turn 2 resolved=[u-1, u-2]; obs-3 is NOT in resolved set // demand=established (u-1), compliance=unresolved (future-feasibility condition vs present evidence = different_timeframe) const turn2 = allResults.filter((r) => r.turn === 2); const nonUnresolved = turn2.filter((r) => r.status !== "unresolved").length; expect(nonUnresolved).toBe(1); // Compliance condition uses "is achievable" → different_timeframe scope → unresolved const complianceResult = turn2.find((r) => r.condition.includes("compliance")); expect(complianceResult.status).toBe("unresolved"); }); it("turn 3 has one non-unresolved condition (demand=established, others unresolved)", () => { // 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(1); // Compliance is unresolved (future-feasibility condition, present-state evidence) const complianceResult = turn3.find((r) => r.condition.includes("compliance")); expect(complianceResult.status).toBe("unresolved"); // 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 demand established, compliance unresolved, value_cost unresolved, differentiation established", () => { // Turn 4 resolved=[u-1,u-2,u-3,u-4] // demand=established (obs-2 direct_match + supports) // compliance=unresolved (future-feasibility condition "is achievable" vs present evidence = different_timeframe) // value_cost=unresolved (obs-4 informs — no directional signal) // differentiation=established (obs-5 "european equivalent" matches related keyword for differentiation → direct_match + supports) const turn4 = allResults.filter((r) => r.turn === 4); const demandResult = turn4.find((r) => r.condition.includes("demand")); expect(demandResult.status).toBe("established"); const complianceResult = turn4.find((r) => r.condition.includes("compliance")); expect(complianceResult.status).toBe("unresolved"); const valueCostResult = turn4.find((r) => r.condition.includes("value")); expect(valueCostResult.status).toBe("unresolved"); const diffResult = turn4.find((r) => r.condition.includes("competitive")); // obs-5 "european equivalent" matches related keyword → direct_match + supports → established 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); }); });