Move EVIDENCE_DIRECTION_GROUPS out of the mock fixture library into lib/graph/evidence-direction.js where it belongs. Remove unused DECISION_CONDITIONS and CONTRADICTION_KEYWORDS exports from scenarios. Add Experiment 24A entry to the design log.
291 lines
10 KiB
JavaScript
291 lines
10 KiB
JavaScript
/**
|
|
* Experiment 24A — Tests for assessEvidenceDirection.
|
|
*
|
|
* Validates evidence direction classification (supports, contradicts,
|
|
* informs, cannot_determine) against explicit conditions and the
|
|
* long-investigation scenario fixture.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import { assessEvidenceDirection } from "@/lib/graph/evidence-direction.js";
|
|
import { buildScenarioFixture } from "@/lib/mocks/scenarios.js";
|
|
|
|
/* ── Helpers ─────────────────────────────────────────────── */
|
|
|
|
const DECISION_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 || "observation",
|
|
status: opts.status || "resolved",
|
|
};
|
|
}
|
|
|
|
/* ── supports: evidence confirms the condition ───────────── */
|
|
|
|
describe("assessEvidenceDirection — supports", () => {
|
|
it("returns supports when observation text contains supporting keywords for demand", () => {
|
|
const evidence = makeNode("obs-2", "European analytics SaaS market valued at approximately €8B and growing 15% annually");
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[0] },
|
|
evidenceNode: evidence,
|
|
});
|
|
|
|
expect(result.direction).toBe("supports");
|
|
expect(typeof result.reason).toBe("string");
|
|
expect(result.reason.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("returns supports when observation contains a clear supporting keyword for differentiation", () => {
|
|
const evidence = makeNode("obs-5", "Our real-time collaboration feature has no direct European equivalent");
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[3] },
|
|
evidenceNode: evidence,
|
|
});
|
|
|
|
expect(result.direction).toBe("supports");
|
|
});
|
|
});
|
|
|
|
/* ── contradicts: evidence directly weakens the condition ─ */
|
|
|
|
describe("assessEvidenceDirection — contradicts", () => {
|
|
it("returns contradicts when observation contains a contradiction phrase for compliance", () => {
|
|
const evidence = makeNode("obs-3", "Our platform does not support EU data residency requirements");
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[1] },
|
|
evidenceNode: evidence,
|
|
});
|
|
|
|
expect(result.direction).toBe("contradicts");
|
|
});
|
|
});
|
|
|
|
/* ── informs: evidence provides relevant context only ───── */
|
|
|
|
describe("assessEvidenceDirection — informs", () => {
|
|
it("returns informs when observation is relevant but contains only neutral keywords", () => {
|
|
const evidence = makeNode("obs-4", "Achieving compliance would require approximately 6 months and $500K engineering investment");
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[2] },
|
|
evidenceNode: evidence,
|
|
});
|
|
|
|
expect(result.direction).toBe("informs");
|
|
});
|
|
|
|
it("returns informs when observation is related but does not strongly support or contradict", () => {
|
|
const evidence = makeNode("obs-1", "Current revenue is $2M ARR in the US market only");
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: "cost" },
|
|
evidenceNode: evidence,
|
|
});
|
|
|
|
expect(result.direction).toBe("informs");
|
|
});
|
|
});
|
|
|
|
/* ── cannot_determine: insufficient data ─────────────────── */
|
|
|
|
describe("assessEvidenceDirection — cannot_determine", () => {
|
|
it("returns cannot_determine when condition is missing", () => {
|
|
const evidence = makeNode("obs-2", "European analytics SaaS market valued at €8B");
|
|
const result = assessEvidenceDirection({ evidenceNode: evidence });
|
|
expect(result.direction).toBe("cannot_determine");
|
|
});
|
|
|
|
it("returns cannot_determine when condition is null", () => {
|
|
const result = assessEvidenceDirection({ condition: null, evidenceNode: makeNode("t-1", "test") });
|
|
expect(result.direction).toBe("cannot_determine");
|
|
});
|
|
|
|
it("returns cannot_determine when evidenceNode is missing", () => {
|
|
const result = assessEvidenceDirection({ condition: { text: "demand" } });
|
|
expect(result.direction).toBe("cannot_determine");
|
|
});
|
|
|
|
it("returns cannot_determine when evidenceNode is null", () => {
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: "demand" },
|
|
evidenceNode: null,
|
|
});
|
|
expect(result.direction).toBe("cannot_determine");
|
|
});
|
|
|
|
it("returns cannot_determine when condition has empty text", () => {
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: "" },
|
|
evidenceNode: makeNode("t-1", "test"),
|
|
});
|
|
expect(result.direction).toBe("cannot_determine");
|
|
});
|
|
|
|
it("returns cannot_determine when evidenceNode has empty description", () => {
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[0] },
|
|
evidenceNode: makeNode("t-1", ""),
|
|
});
|
|
expect(result.direction).toBe("cannot_determine");
|
|
});
|
|
|
|
it("returns cannot_determine when neither input is provided", () => {
|
|
const result = assessEvidenceDirection({});
|
|
expect(result.direction).toBe("cannot_determine");
|
|
});
|
|
});
|
|
|
|
/* ── Deterministic output ───────────────────────────────── */
|
|
|
|
describe("assessEvidenceDirection — determinism", () => {
|
|
it("returns identical results for identical inputs across multiple calls", () => {
|
|
const evidence = makeNode("det-1", "European analytics market growing 15% annually");
|
|
const input = {
|
|
condition: { text: DECISION_CONDITIONS[0] },
|
|
evidenceNode: evidence,
|
|
};
|
|
|
|
const r1 = assessEvidenceDirection(input);
|
|
const r2 = assessEvidenceDirection(input);
|
|
expect(r1).toEqual(r2);
|
|
});
|
|
|
|
it("deterministic across 5 calls with long-investigation data", () => {
|
|
const fixture = buildScenarioFixture("long", 2);
|
|
const obs3Node = makeNode("obs-3", "Our platform does not support EU data residency requirements");
|
|
const input = {
|
|
condition: { text: DECISION_CONDITIONS[1] },
|
|
evidenceNode: obs3Node,
|
|
};
|
|
|
|
const results = [1, 2, 3, 4, 5].map(() => assessEvidenceDirection(input));
|
|
for (let i = 1; i < results.length; i++) {
|
|
expect(results[i]).toEqual(results[0]);
|
|
}
|
|
});
|
|
});
|
|
|
|
/* ── Input immutability ─────────────────────────────────── */
|
|
|
|
describe("assessEvidenceDirection — input immutability", () => {
|
|
it("does not mutate the condition object", () => {
|
|
const cond = { text: DECISION_CONDITIONS[0] };
|
|
const originalText = cond.text;
|
|
const evidence = makeNode("imm-1", "European market valued at €8B");
|
|
|
|
assessEvidenceDirection({ condition: cond, evidenceNode: evidence });
|
|
expect(cond.text).toBe(originalText);
|
|
});
|
|
|
|
it("does not mutate the evidenceNode object", () => {
|
|
const node = makeNode("imm-2", "test description");
|
|
const originalDesc = node.description;
|
|
const condition = { text: DECISION_CONDITIONS[3] };
|
|
|
|
assessEvidenceDirection({ condition, evidenceNode: node });
|
|
expect(node.description).toBe(originalDesc);
|
|
});
|
|
});
|
|
|
|
/* ── Four long-investigation examples ───────────────────── */
|
|
|
|
describe("assessEvidenceDirection — long investigation examples", () => {
|
|
it("demand example: supports (obs-2)", () => {
|
|
const fixture = buildScenarioFixture("long", 1);
|
|
const graphNodes = fixture.situationGraph.nodes;
|
|
const obs2Node = graphNodes.find((n) => n.id === "obs-2");
|
|
|
|
expect(obs2Node).toBeDefined();
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[0] },
|
|
evidenceNode: obs2Node,
|
|
});
|
|
|
|
expect(result.direction).toBe("supports");
|
|
});
|
|
|
|
it("compliance example: contradicts (obs-3)", () => {
|
|
const fixture = buildScenarioFixture("long", 2);
|
|
const graphNodes = fixture.situationGraph.nodes;
|
|
const obs3Node = graphNodes.find((n) => n.id === "obs-3");
|
|
|
|
expect(obs3Node).toBeDefined();
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[1] },
|
|
evidenceNode: obs3Node,
|
|
});
|
|
|
|
expect(result.direction).toBe("contradicts");
|
|
});
|
|
|
|
it("value versus cost example: informs (obs-4)", () => {
|
|
const fixture = buildScenarioFixture("long", 3);
|
|
const graphNodes = fixture.situationGraph.nodes;
|
|
const obs4Node = graphNodes.find((n) => n.id === "obs-4");
|
|
|
|
expect(obs4Node).toBeDefined();
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[2] },
|
|
evidenceNode: obs4Node,
|
|
});
|
|
|
|
expect(result.direction).toBe("informs");
|
|
});
|
|
|
|
it("differentiation example: supports (obs-5)", () => {
|
|
const fixture = buildScenarioFixture("long", 4);
|
|
const graphNodes = fixture.situationGraph.nodes;
|
|
const obs5Node = graphNodes.find((n) => n.id === "obs-5");
|
|
|
|
expect(obs5Node).toBeDefined();
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[3] },
|
|
evidenceNode: obs5Node,
|
|
});
|
|
|
|
expect(result.direction).toBe("supports");
|
|
});
|
|
});
|
|
|
|
/* ── Unrelated evidence ─────────────────────────────────── */
|
|
|
|
describe("assessEvidenceDirection — unrelated evidence", () => {
|
|
it("returns informs when evidence is about a different concept area", () => {
|
|
const evidence = makeNode("unrel-1", "Our team has 15 engineers in London");
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[0] },
|
|
evidenceNode: evidence,
|
|
});
|
|
|
|
expect(result.direction).toBe("informs");
|
|
});
|
|
|
|
it("returns informs when observation contains no category-matching keywords", () => {
|
|
const evidence = makeNode("unrel-2", "The office lease expires in December 2026");
|
|
|
|
const result = assessEvidenceDirection({
|
|
condition: { text: DECISION_CONDITIONS[1] },
|
|
evidenceNode: evidence,
|
|
});
|
|
|
|
expect(result.direction).toBe("informs");
|
|
});
|
|
});
|