Files
confidence-engine/tests/empty-done-orchestration.test.jsx
robbond f2c9e4c0b2 fix(confidence-engine): align empty done and reopen state
- Empty Done immediate transition now sets node.status to resolved
  alongside resolvedNodeIds/doneForNowIds — same canonical parked
  shape as populated Done (no server call required)
- Clarified-question Re-open removes target from doneForNowIds so
  the question visibly returns to Open Questions
- Immediate graph mutation creates new node objects immutably
  (React state semantics), touching only the target node
2026-09-02 13:37:15 +01:00

304 lines
10 KiB
React

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