/** * v0.53 - Empty Done Orchestration Regression Test * * Proves that handleDoneForNowPromotion's gate is based on active-target * focused contributions, NOT on scenario-wide findings. * * Deterministic: exercises the exact ownership condition logic extracted * from the component boundary. No rendering, no fetch. */ import { describe, expect, it } from "vitest"; import { reopenResolvedUnknown } from "@/lib/graph/reopen-resolved-unknown.js"; /* - Extracted gate logic (mirrors scenario-form.jsx line ~362) -- */ function shouldEnterEpisodePath(targetNodeId, focusedContributions) { if (!targetNodeId) return false; const hasActiveTargetContent = (focusedContributions ?? []).some( (c) => c.targetNodeId === targetNodeId || c.originatingTargetNodeId === targetNodeId, ); return hasActiveTargetContent; } /* - Case A - empty active target -- */ describe("empty active target", () => { it("returns false when focusedContributions contains no contribution owned by the target", () => { const result = shouldEnterEpisodePath( "node-B", [ { id: "contrib-0001", targetNodeId: "node-A", originatingTargetNodeId: "node-A" }, { id: "contrib-0002", targetNodeId: "node-A", originatingTargetNodeId: "node-A" }, ], ); expect(result).toBe(false); }); it("returns false when focusedContributions is empty array", () => { const result = shouldEnterEpisodePath("node-B", []); expect(result).toBe(false); }); it("returns false when focusedContributions is undefined/null", () => { expect(shouldEnterEpisodePath("node-B", undefined)).toBe(false); expect(shouldEnterEpisodePath("node-B", null)).toBe(false); }); it("returns false when targetNodeId is empty string", () => { const result = shouldEnterEpisodePath("", [{ id: "contrib-0001", targetNodeId: "node-A" }]); expect(result).toBe(false); }); it("returns false when targetNodeId is null", () => { const result = shouldEnterEpisodePath(null, [{ id: "contrib-0001", targetNodeId: "node-A" }]); expect(result).toBe(false); }); it("originatingTargetNodeId ownership also gates correctly - mismatched origin", () => { const result = shouldEnterEpisodePath( "node-B", [{ id: "contrib-0001", targetNodeId: "node-A", originatingTargetNodeId: "node-C" }], ); expect(result).toBe(false); }); }); /* - Case A2 — immediate Done produces canonical parked state (graph coherence) -- */ function simulateImmediateDone(graph, nodeId) { /* Mirrors the onImmediateGraphChange logic in FocusedWorkspaceNavigation */ const resolvedIds = new Set(graph.resolvedNodeIds || []); resolvedIds.add(nodeId); const nextNodes = (graph.nodes || []).map((n) => n.id === nodeId ? { ...n, status: "resolved" } : n, ); return { ...graph, nodes: nextNodes, resolvedNodeIds: Array.from(resolvedIds), }; } describe("immediate Done produces canonical parked state", () => { it("sets node.status to resolved and adds ID to resolvedNodeIds for empty-Done target", () => { const graph = { nodes: [{ id: "node-empty", kind: "unknown", status: "unknown" }], resolvedNodeIds: [], }; const nextGraph = simulateImmediateDone(graph, "node-empty"); const targetNode = nextGraph.nodes.find((n) => n.id === "node-empty"); expect(targetNode.status).toBe("resolved"); expect(nextGraph.resolvedNodeIds).toContain("node-empty"); }); it("preserves other nodes unchanged", () => { const graph = { nodes: [ { id: "node-A", kind: "unknown", status: "unknown" }, { id: "node-B", kind: "unknown", status: "unknown" }, ], resolvedNodeIds: [], }; const nextGraph = simulateImmediateDone(graph, "node-B"); const nodeA = nextGraph.nodes.find((n) => n.id === "node-A"); expect(nodeA.status).toBe("unknown"); }); it("is idempotent for resolvedNodeIds — duplicate add does not create duplicates", () => { const graph = { nodes: [{ id: "node-X", kind: "unknown", status: "resolved" }], resolvedNodeIds: ["node-X"], }; const nextGraph = simulateImmediateDone(graph, "node-X"); expect(nextGraph.resolvedNodeIds.filter((id) => id === "node-X").length).toBe(1); }); }); /* - Case B - populated active target (regression guard) -- */ describe("populated active target", () => { it("returns true when a contribution's targetNodeId matches", () => { const result = shouldEnterEpisodePath( "node-B", [ { id: "contrib-0001", targetNodeId: "node-A" }, { id: "contrib-0002", targetNodeId: "node-B" }, ], ); expect(result).toBe(true); }); it("returns true when a contribution's originatingTargetNodeId matches", () => { const result = shouldEnterEpisodePath( "node-B", [ { id: "contrib-0001", targetNodeId: "node-A" }, { id: "contrib-0002", originatingTargetNodeId: "node-B", targetNodeId: "node-B" }, ], ); expect(result).toBe(true); }); it("returns true with a single contribution owned by the target", () => { const result = shouldEnterEpisodePath("node-B", [ { id: "contrib-0001", targetNodeId: "node-B" }, ]); expect(result).toBe(true); }); it("returns true when contributions exist for the target even if others are also present", () => { const result = shouldEnterEpisodePath( "node-B", [ { id: "contrib-0001", targetNodeId: "node-A" }, { id: "contrib-0002", originatingTargetNodeId: "node-B" }, { id: "contrib-0003", targetNodeId: "node-C" }, ], ); expect(result).toBe(true); }); }); /* - Integration: scenario-wide findings must not leak through -- */ describe("scenario-wide evidence does not influence gate", () => { it("findings for other questions do not cause empty target to enter episode path", () => { // Even if there are many findings elsewhere, the empty target (node-B) // must NOT enter episode processing. const focusedContributions = [ { id: "contrib-0001", targetNodeId: "node-A", originatingTargetNodeId: "node-A" }, { id: "contrib-0002", targetNodeId: "node-A", originatingTargetNodeId: "node-A" }, { id: "contrib-0003", targetNodeId: "node-A", originatingTargetNodeId: "node-A" }, ]; // node-B has zero focused contributions - should NOT enter episode path expect(shouldEnterEpisodePath("node-B", focusedContributions)).toBe(false); // node-A has contributions - should enter episode path (existing behavior preserved) expect(shouldEnterEpisodePath("node-A", focusedContributions)).toBe(true); }); }); /* - Case B2 — Re-open reverses canonical + local parking state -- */ describe("Re-open reverses canonical graph state", () => { it("changes node.status from resolved back to unknown", () => { const graph = { nodes: [ { id: "node-parked", kind: "unknown", status: "resolved" }, { id: "node-other", kind: "unknown", status: "unknown" }, ], resolvedNodeIds: ["node-parked"], }; const nextGraph = reopenResolvedUnknown(graph, "node-parked"); const targetNode = nextGraph.nodes.find((n) => n.id === "node-parked"); expect(targetNode.status).toBe("unknown"); }); it("removes target from resolvedNodeIds", () => { const graph = { nodes: [{ id: "node-parked", kind: "unknown", status: "resolved" }], resolvedNodeIds: ["node-parked"], }; const nextGraph = reopenResolvedUnknown(graph, "node-parked"); expect(nextGraph.resolvedNodeIds).not.toContain("node-parked"); }); it("preserves other resolved nodes", () => { const graph = { nodes: [ { id: "node-A", kind: "unknown", status: "resolved" }, { id: "node-B", kind: "unknown", status: "resolved" }, { id: "node-C", kind: "unknown", status: "unknown" }, ], resolvedNodeIds: ["node-A", "node-B"], }; const nextGraph = reopenResolvedUnknown(graph, "node-A"); expect(nextGraph.resolvedNodeIds).toContain("node-B"); expect(nextGraph.resolvedNodeIds).not.toContain("node-A"); }); it("returns original graph when node is not a resolved unknown", () => { const graph = { nodes: [{ id: "node-unknown", kind: "unknown", status: "unknown" }], resolvedNodeIds: [], }; const nextGraph = reopenResolvedUnknown(graph, "node-unknown"); expect(nextGraph).toBe(graph); }); it("returns original graph when node is not an unknown kind", () => { const graph = { nodes: [{ id: "node-assumption", kind: "assumption", status: "resolved" }], resolvedNodeIds: ["node-assumption"], }; const nextGraph = reopenResolvedUnknown(graph, "node-assumption"); expect(nextGraph).toBe(graph); }); it("local setDoneForNowIds cleanup removes only the reopened target", () => { const prevDoneIds = ["node-A", "node-parked", "node-C"]; const nextDoneIds = prevDoneIds.filter((id) => id !== "node-parked"); expect(nextDoneIds).toContain("node-A"); expect(nextDoneIds).toContain("node-C"); expect(nextDoneIds).not.toContain("node-parked"); }); it("local setDoneForNowIds is no-op when target not in array", () => { const prevDoneIds = ["node-A", "node-B"]; const nextDoneIds = prevDoneIds.filter((id) => id !== "node-parked"); expect(nextDoneIds).toEqual(prevDoneIds); }); }); /* - Case C — history preservation (graph structure invariance) -- */ describe("Re-open preserves contributions and findings", () => { it("does not create or delete nodes", () => { const originalNodes = [ { id: "node-parked", kind: "unknown", status: "resolved" }, { id: "node-A", kind: "assumption", status: "resolved" }, ]; const graph = { nodes: originalNodes, resolvedNodeIds: ["node-parked"], }; const nextGraph = reopenResolvedUnknown(graph, "node-parked"); expect(nextGraph.nodes.length).toBe(originalNodes.length); }); it("does not create or delete edges", () => { const graph = { nodes: [{ id: "node-parked", kind: "unknown", status: "resolved" }], resolvedNodeIds: ["node-parked"], edges: [ { from: "node-parked", to: "node-A", type: "supports" }, { from: "node-B", to: "node-parked", type: "refutes" }, ], }; const nextGraph = reopenResolvedUnknown(graph, "node-parked"); expect(nextGraph.edges.length).toBe(2); }); });