Files
confidence-engine/tests/focused-promotion-boundary.test.js
T

274 lines
11 KiB
JavaScript

/**
* v0.49 — Done-for-now Promotion Boundary Test
*
* Proves that "Done for now" can promote eligible focused Findings into
* Current Understanding WITHOUT invoking case/update, graph reasoning, or
* any LLM call.
*
* Deterministic: pure function assertions only. No rendering, no fetch.
*/
import { describe, expect, it } from "vitest";
import { produceFindingInformedSummary } from "@/lib/graph/finding-helpers.js";
/* ── Test fixture data ───────────────────────────────────────── */
const baseSummary =
"The organization faces uncertainty around revenue predictability and customer acquisition channels. Market dynamics are shifting, requiring adaptive strategy.";
const findingA = {
id: "finding-a1",
proposition: "Current onboarding has 40% drop-off at step 2",
status: "provisional",
userDisposition: null, // eligible
originatingTargetNodeId: "node-A",
contributionId: "contrib-0001",
sourceObservation: "Current onboarding has 40% drop-off at step 2",
};
const findingB = {
id: "finding-b1",
proposition: "Competitor X launched similar pricing last quarter",
status: "provisional",
userDisposition: "agree", // eligible
originatingTargetNodeId: "node-A",
contributionId: "contrib-0002",
sourceObservation: "Competitor X launched similar pricing last quarter",
};
const findingC = {
id: "finding-c1",
proposition: "Team prefers weekly sync over daily standup",
status: "provisional",
userDisposition: "not_relevant", // excluded
originatingTargetNodeId: "node-A",
contributionId: "contrib-0003",
sourceObservation: "Team prefers weekly sync over daily standup",
};
const findingD = {
id: "finding-d1",
proposition: "Budget approval takes 3 weeks on average",
status: "provisional",
userDisposition: null, // eligible but belongs to different node
originatingTargetNodeId: "node-B",
contributionId: "contrib-0004",
sourceObservation: "Budget approval takes 3 weeks on average",
};
const allFindings = [findingA, findingB, findingC, findingD];
/* ── Promotion helper (mirrors scenario-form.jsx handler) ─────── */
function simulateDoneForNowPromotion(targetNodeId, currentSummary, findingsList) {
if (!targetNodeId || !findingsList?.length) return currentSummary;
// v0.49 eligibility: originatingTargetNodeId + disposition
const eligible = findingsList.filter(
(f) =>
f.originatingTargetNodeId === targetNodeId &&
(f.userDisposition === null || f.userDisposition === "agree"),
);
if (eligible.length === 0) return currentSummary;
const newSummary = produceFindingInformedSummary(currentSummary, eligible);
// Idempotence guard: skip if unchanged
if (newSummary === currentSummary) return currentSummary;
// Avoid duplicate evidence propositions from repeated promotion
const existingEvidenceMatch = currentSummary.match(/Evidence:\s*\[([^\]]+)\]/);
let isDuplicate = false;
if (existingEvidenceMatch) {
const existingTexts = existingEvidenceMatch[1].split("; ").map((t) => t.trim());
isDuplicate = eligible.every((f) => existingTexts.includes(f.proposition));
}
if (isDuplicate) return currentSummary;
return newSummary;
}
/* ── Tests ───────────────────────────────────────────────────── */
describe("v0.49 — Done-for-now Promotion Boundary", () => {
describe("promotion semantics", () => {
it("SELECTS eligible null disposition Finding for target node A", () => {
const result = simulateDoneForNowPromotion(
"node-A", baseSummary, [findingA],
);
expect(result).toContain("Evidence:");
expect(result).toContain(findingA.proposition);
});
it("SELECTS eligible agree disposition Finding for target node A", () => {
const result = simulateDoneForNowPromotion(
"node-A", baseSummary, [findingB],
);
expect(result).toContain("Evidence:");
expect(result).toContain(findingB.proposition);
});
it("EXCLUDES not_relevant disposition Finding for target node A", () => {
const result = simulateDoneForNowPromotion(
"node-A", baseSummary, [findingC],
);
// no eligible findings → summary unchanged
expect(result).toBe(baseSummary);
});
it("EXCLUDES findings belonging to different target node", () => {
const result = simulateDoneForNowPromotion(
"node-A", baseSummary, [findingD],
);
// findingD has originatingTargetNodeId === "node-B" → excluded
expect(result).toBe(baseSummary);
});
it("SELECTS multiple eligible findings for target node A simultaneously", () => {
const result = simulateDoneForNowPromotion(
"node-A", baseSummary, [findingA, findingB],
);
expect(result).toContain("Evidence:");
expect(result).toContain(findingA.proposition);
expect(result).toContain(findingB.proposition);
});
it("PRESERVES original summary as prefix when findings are eligible", () => {
const result = simulateDoneForNowPromotion(
"node-A", baseSummary, [findingA],
);
expect(result.startsWith(baseSummary)).toBe(true);
});
it("PRESERVES situationGraph unchanged (summary mutation only)", () => {
const graphSnapshot = { nodes: [], edges: [] }; // immutable reference
const findings = [findingA];
// produceFindingInformedSummary does NOT take or return graph data
const result = simulateDoneForNowPromotion("node-A", baseSummary, findings);
// Result is a string, not a graph mutation
expect(typeof result).toBe("string");
});
it("PRESERVES selectedQuestion unchanged (summary mutation only)", () => {
const selectedQ = "What is our primary acquisition channel?";
// Promotion only affects summary/currentUnderstanding
const result = simulateDoneForNowPromotion(
"node-A", baseSummary, [findingA],
);
expect(result !== selectedQ).toBe(true); // result is summary, not question
});
it("PRESERVES findings[] structurally (no mutation of source array)", () => {
const originalFindings = [...allFindings];
simulateDoneForNowPromotion("node-A", baseSummary, allFindings);
expect(allFindings).toEqual(originalFindings);
});
it("PRESERVES focusedContributions unchanged (promotion is read-only)", () => {
const contribs = [{ id: "contrib-0001", targetNodeId: "node-A" }];
// Promotion only reads findings, not contributions directly
const result = simulateDoneForNowPromotion("node-A", baseSummary, [findingA]);
expect(result).toContain(findingA.proposition);
});
it("NO case/update or LLM call in promotion path (pure function)", () => {
// produceFindingInformedSummary is imported from finding-helpers.js
// It does NOT import any API client, fetch, or LLM provider.
expect(typeof produceFindingInformedSummary).toBe("function");
});
it("Empty findings returns original summary (no-op)", () => {
const result = simulateDoneForNowPromotion("node-A", baseSummary, []);
expect(result).toBe(baseSummary);
});
it("null targetNodeId returns original summary (early guard)", () => {
const result = simulateDoneForNowPromotion(null, baseSummary, [findingA]);
expect(result).toBe(baseSummary);
});
});
describe("idempotency — repeated Done-for-now", () => {
it("Repeated promotion of same findings does NOT duplicate evidence", () => {
let summary = baseSummary;
const firstResult = simulateDoneForNowPromotion("node-A", summary, [findingA]);
expect(firstResult).not.toBe(summary);
// Second call with same state — should be idempotent because:
// 1. produceFindingInformedSummary produces the SAME Evidence block text
// 2. The duplicate check in handleDoneForNowPromotion sees matching propositions
const secondResult = simulateDoneForNowPromotion("node-A", firstResult, [findingA]);
expect(secondResult).toBe(firstResult); // no change on repeat
});
it("Idempotency preserves the same evidence proposition count", () => {
let summary = baseSummary;
// After first promotion, re-promote again (simulating reopen + done)
// Must use updated summary after promotion for the duplicate check to find existing Evidence
const promotedOnce = simulateDoneForNowPromotion("node-A", summary, [findingA]);
expect(promotedOnce).not.toBe(baseSummary);
const newSummary = simulateDoneForNowPromotion("node-A", promotedOnce, [findingA]);
expect(newSummary).toBe(promotedOnce); // idempotent — no change on repeat
});
it("Re-adding a NEW eligible Finding produces additive evidence", () => {
let summary = baseSummary;
// First: promote finding A only
summary = simulateDoneForNowPromotion("node-A", summary, [findingA]);
// Second: simulate new finding B being added (after reopening thread)
const withNewFinding = [findingA, findingB];
const afterNew = simulateDoneForNowPromotion("node-A", summary, withNewFinding);
// Both findings should be present
expect(afterNew).toContain(findingA.proposition);
expect(afterNew).toContain(findingB.proposition);
});
});
describe("target isolation", () => {
it("Only Findings for targetNodeId are promoted", () => {
const result = simulateDoneForNowPromotion(
"node-A", baseSummary, allFindings,
);
expect(result).toContain(findingA.proposition); // eligible, node-A
expect(result).toContain(findingB.proposition); // eligible, node-A
expect(result).not.toContain(findingC.proposition); // not_relevant
expect(result).not.toContain(findingD.proposition); // different target
});
it("Target B findings are NOT promoted when promoting target A", () => {
const result = simulateDoneForNowPromotion(
"node-A", baseSummary, [findingD],
);
expect(result).toBe(baseSummary);
});
it("Target A and B promotions are isolated (neither affects the other's base)", () => {
const aResult = simulateDoneForNowPromotion(
"node-A", baseSummary, [findingA],
);
const bResult = simulateDoneForNowPromotion(
"node-B", baseSummary, [findingD],
);
expect(aResult).toContain(findingA.proposition);
expect(bResult).toContain(findingD.proposition);
// Neither affects the other's base summary
expect(bResult).not.toContain(findingA.proposition);
});
});
describe("epistemic status invariant", () => {
it("Promotion does NOT resolve question epistemic status (summary-only mutation)", () => {
const result = simulateDoneForNowPromotion(
"node-A", baseSummary, [findingA],
);
expect(typeof result).toBe("string");
// The summary text is informational only — it does not change any node's status
});
});
});