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