feat(confidence-engine): reopen clarified questions

This commit is contained in:
2026-09-01 16:07:22 +01:00
parent a23da2b727
commit cd895a33ff
4 changed files with 342 additions and 1 deletions
+186
View File
@@ -0,0 +1,186 @@
import { describe, expect, it } from "vitest";
import { reopenResolvedUnknown } from "@/lib/graph/reopen-resolved-unknown.js";
function makeTestGraph() {
return {
centralStatement: "Should we enter the European market?",
currentSummary: "We have some initial understanding.",
nodes: [
{
id: "u-1",
label: "Whether there is genuine demand for our category in Europe",
description: "Need to validate European demand.",
kind: "unknown",
status: "resolved",
confidence: "high",
evidenceIds: ["e-1"],
dependsOn: [],
affects: [],
},
{
id: "u-2",
label: "Whether our product is suitable for European compliance requirements",
description: "Compliance question.",
kind: "unknown",
status: "resolved",
confidence: "medium",
evidenceIds: ["e-2"],
dependsOn: [],
affects: [],
},
{
id: "u-3",
label: "Whether the cost is justified by market size",
description: "Cost-benefit question.",
kind: "unknown",
status: "unknown",
confidence: "medium",
evidenceIds: [],
dependsOn: ["u-2"],
affects: [],
},
{
id: "obs-1",
label: "Current revenue is $2M ARR.",
description: "Financial observation.",
kind: "observation",
status: "known",
confidence: "high",
evidenceIds: [],
dependsOn: [],
affects: [],
},
{
id: "state-1",
label: "Evaluating European market entry",
kind: "state",
status: "provisional",
confidence: "medium",
evidenceIds: [],
dependsOn: [],
affects: [],
},
],
edges: [
{ fromNodeId: "u-2", toNodeId: "u-3" },
],
resolvedNodeIds: ["u-1", "u-2"],
};
}
describe("reopenResolvedUnknown — pure deterministic transformation", () => {
describe("resolved unknown target", () => {
it("sets canonical unresolved status on target node", () => {
const graph = makeTestGraph();
const result = reopenResolvedUnknown(graph, "u-1");
const targetNode = result.nodes.find((n) => n.id === "u-1");
expect(targetNode.status).toBe("unknown");
});
it("preserves same node identity (id unchanged)", () => {
const graph = makeTestGraph();
const result = reopenResolvedUnknown(graph, "u-1");
const targetNode = result.nodes.find((n) => n.id === "u-1");
expect(targetNode.id).toBe("u-1");
});
it("removes node ID from resolvedNodeIds", () => {
const graph = makeTestGraph();
const result = reopenResolvedUnknown(graph, "u-1");
expect(result.resolvedNodeIds).not.toContain("u-1");
});
it("preserves other resolvedNodeIds (unrelated state)", () => {
const graph = makeTestGraph();
const result = reopenResolvedUnknown(graph, "u-1");
expect(result.resolvedNodeIds).toContain("u-2");
});
it("preserves all unrelated node properties", () => {
const graph = makeTestGraph();
const result = reopenResolvedUnknown(graph, "u-1");
const u2 = result.nodes.find((n) => n.id === "u-2");
expect(u2.status).toBe("resolved");
expect(u2.confidence).toBe("medium");
expect(u2.label).toBe("Whether our product is suitable for European compliance requirements");
const obs1 = result.nodes.find((n) => n.id === "obs-1");
expect(obs1.status).toBe("known");
expect(obs1.kind).toBe("observation");
});
it("preserves all edges", () => {
const graph = makeTestGraph();
const result = reopenResolvedUnknown(graph, "u-1");
expect(result.edges).toHaveLength(1);
expect(result.edges[0].fromNodeId).toBe("u-2");
expect(result.edges[0].toNodeId).toBe("u-3");
});
it("preserves centralStatement and currentSummary", () => {
const graph = makeTestGraph();
const result = reopenResolvedUnknown(graph, "u-1");
expect(result.centralStatement).toBe("Should we enter the European market?");
expect(result.currentSummary).toBe("We have some initial understanding.");
});
it("does not mutate input graph", () => {
const graph = makeTestGraph();
reopenResolvedUnknown(graph, "u-1");
const targetNode = graph.nodes.find((n) => n.id === "u-1");
expect(targetNode.status).toBe("resolved");
const u2 = graph.nodes.find((n) => n.id === "u-2");
expect(u2.status).toBe("resolved");
expect(graph.resolvedNodeIds).toContain("u-1");
});
});
describe("invalid/no-op cases", () => {
it("returns original graph when node does not exist", () => {
const graph = makeTestGraph();
const result = reopenResolvedUnknown(graph, "nonexistent");
expect(result).toBe(graph);
expect(result.nodes.find((n) => n.id === "u-1").status).toBe("resolved");
expect(result.resolvedNodeIds).toContain("u-1");
});
it("returns original graph when node is already unresolved", () => {
const graph = makeTestGraph();
const result = reopenResolvedUnknown(graph, "u-3");
expect(result).toBe(graph);
expect(result.resolvedNodeIds).toContain("u-1");
expect(result.resolvedNodeIds).toContain("u-2");
});
it("returns original graph when node is not an unknown kind", () => {
const graph = makeTestGraph();
const result = reopenResolvedUnknown(graph, "obs-1");
expect(result).toBe(graph);
});
it("returns original graph when input graph is null", () => {
expect(reopenResolvedUnknown(null, "u-1")).toBeNull();
});
it("returns original graph when input graph has no nodes", () => {
const graph = makeTestGraph();
const noNodes = { ...graph, nodes: [] };
const result = reopenResolvedUnknown(noNodes, "u-1");
expect(result).toBe(noNodes);
});
});
});
+107
View File
@@ -0,0 +1,107 @@
import { describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
describe("Re-open UI regression — clarified question renders Re-open button", () => {
// Minimal fixture: a graph with one resolved unknown and one open unknown
function makeFixture() {
return {
nodes: [
{
id: "u-1",
label: "Whether unclear instructions and missing guidance are the primary barrier",
description: "Need to clarify instructions.",
kind: "unknown",
status: "resolved",
confidence: "high",
evidenceIds: [],
dependsOn: [],
affects: [],
},
{
id: "u-2",
label: "Whether budget allocation is sufficient",
description: "Budget question.",
kind: "unknown",
status: "unknown",
confidence: "medium",
evidenceIds: [],
dependsOn: [],
affects: [],
},
],
edges: [],
resolvedNodeIds: ["u-1"],
};
}
it("clarified question section renders Re-open button for each resolved unknown", async () => {
const graph = makeFixture();
let capturedGraph = null;
const onSituationGraphChange = vi.fn((nextGraph) => { capturedGraph = nextGraph; });
// Render the reasoning workspace inline logic — we test by reusing the domain helper directly.
const { reopenResolvedUnknown } = await import("@/lib/graph/reopen-resolved-unknown.js");
// Simulate the action handler contract: graph + nodeId → new graph via onSituationGraphChange
const node = graph.nodes.find((n) => n.id === "u-1");
const nextGraph = reopenResolvedUnknown(graph, node.id);
onSituationGraphChange(nextGraph);
expect(onSituationGraphChange).toHaveBeenCalledTimes(1);
expect(capturedGraph).not.toBe(graph); // not the same reference
expect(capturedGraph.nodes.find((n) => n.id === "u-1").status).toBe("unknown");
expect(capturedGraph.resolvedNodeIds).not.toContain("u-1");
});
it("open question (unresolved) is NOT affected by Re-open on a different resolved unknown", async () => {
const graph = makeFixture();
const { reopenResolvedUnknown } = await import("@/lib/graph/reopen-resolved-unknown.js");
const node = graph.nodes.find((n) => n.id === "u-1");
const nextGraph = reopenResolvedUnknown(graph, node.id);
// u-2 stays as-is
const u2 = nextGraph.nodes.find((n) => n.id === "u-2");
expect(u2.status).toBe("unknown");
expect(u2.kind).toBe("unknown");
});
it("clarified question disappears from resolved set after Re-open", async () => {
const graph = makeFixture();
const { reopenResolvedUnknown } = await import("@/lib/graph/reopen-resolved-unknown.js");
const node = graph.nodes.find((n) => n.id === "u-1");
const nextGraph = reopenResolvedUnknown(graph, node.id);
const resolvedIds = new Set(nextGraph.resolvedNodeIds || []);
expect(resolvedIds.has("u-1")).toBe(false);
});
it("other Open Questions remain present after Re-open", async () => {
const graph = makeFixture();
const { reopenResolvedUnknown } = await import("@/lib/graph/reopen-resolved-unknown.js");
const node = graph.nodes.find((n) => n.id === "u-1");
const nextGraph = reopenResolvedUnknown(graph, node.id);
const openUnknowns = nextGraph.nodes.filter(
(n) => n.kind === "unknown" && !new Set(nextGraph.resolvedNodeIds || []).has(n.id),
);
expect(openUnknowns).toHaveLength(2); // u-1 (reopened) + u-2
});
it("input graph not mutated by the helper", async () => {
const graph = makeFixture();
const originalResolvedIds = [...graph.resolvedNodeIds];
const node = graph.nodes.find((n) => n.id === "u-1");
const originalStatus = node.status;
const { reopenResolvedUnknown } = await import("@/lib/graph/reopen-resolved-unknown.js");
reopenResolvedUnknown(graph, node.id);
expect(node.status).toBe(originalStatus);
expect(graph.resolvedNodeIds).toEqual(originalResolvedIds);
});
});