From 81dda77392fa59c8e913f3ba9c2411b8f906cb0a Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 26 Aug 2026 10:20:45 +0100 Subject: [PATCH] fix(confidence-engine): reopen completed focused investigation --- components/reasoning-workspace.jsx | 30 +++- tests/open-questions-vs-assumptions.test.jsx | 179 +++++++++++++++++++ 2 files changed, 201 insertions(+), 8 deletions(-) diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx index 656ae74..126e875 100644 --- a/components/reasoning-workspace.jsx +++ b/components/reasoning-workspace.jsx @@ -792,7 +792,7 @@ function OpenQuestionsPanel({ focused, formulationStep, formulateMsg, processingStep, deconstructMsg, doneForNowIds, startFocused, handleDeconstructSubmit, retryFormulation, setSelectedPresentationItemId, setFocusedPresentationItemId, setFocusedAnswer, focusedAnswer, setDoneForNowIds, - setFollowUpQuestion, focusedContributions, + setFollowUpQuestion, focusedContributions, focusedInvestigations, }) { const openNodes = (graph?.nodes || []).filter( (n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id), @@ -800,6 +800,25 @@ function OpenQuestionsPanel({ if (openNodes.length <= 0) return null; + // Check whether a node has a completed focused result stored locally. + const hasCompletedInvestigation = (nid) => { + const inv = focusedInvestigations?.[nid]; + return Boolean(inv && inv.status === "formulated" && inv.result && typeof inv.question === "string" && inv.question.trim()); + }; + + // When a completed result exists and this node is already selected, reopen it. + function handleNodeClick(node) { + if (!focused?.question?.trim() || (focused && !hasFocusedContent())) { + if (hasCompletedInvestigation(node.id) && selectedPresentationItemId === node.id) { + setFocusedPresentationItemId(node.id); + return; + } + setSelectedPresentationItemId( + selectedPresentationItemId === node.id ? null : node.id, + ); + } + } + return (

@@ -813,13 +832,7 @@ function OpenQuestionsPanel({ return (
{ - if (!focused?.question?.trim() || (focused && !hasFocusedContent())) { - setSelectedPresentationItemId( - selectedPresentationItemId === node.id ? null : node.id, - ); - } - }} + onClick={() => handleNodeClick(node)} style={{ cursor: "pointer" }} className="w-full text-left rounded-lg border border-gray-200 px-4 py-3 transition hover:border-gray-300 hover:bg-white" > @@ -1522,6 +1535,7 @@ export default function ReasoningWorkspace({ setDoneForNowIds={setDoneForNowIds} setFollowUpQuestion={setFollowUpQuestion} focusedContributions={focusedContributions} + focusedInvestigations={focusedInvestigations} /> )} diff --git a/tests/open-questions-vs-assumptions.test.jsx b/tests/open-questions-vs-assumptions.test.jsx index d34848d..3f14275 100644 --- a/tests/open-questions-vs-assumptions.test.jsx +++ b/tests/open-questions-vs-assumptions.test.jsx @@ -565,3 +565,182 @@ describe("Done-for-now must not hide unrelated unresolved Open Questions", () => expect(graphWithTwoUnknownsAndAssumptions.nodes.find((n) => n.id === "u1").status).toBe(aStatusBefore); }); }); + +// ── Regression: same-node focused result reopen (FOCUSED-REOPEN) ─ + +describe("same-node focused result reopen", () => { + // Simulates the key state values in ReasoningWorkspace / OpenQuestionsPanel + function simulateFocusedReopenState() { + let selectedPresentationItemId = "u1"; // user had node u1 selected + let focusedPresentationItemId = null; // "Back to open questions" cleared this + let focusedInvestigations = {}; + let formulationStep = "idle"; + let processingStep = "idle"; + + function getFocusedInvestigation() { + if (!focusedPresentationItemId) return null; + return focusedInvestigations[focusedPresentationItemId] || null; + } + + function hasFocusedContent(focusedItem) { + if (!focusedItem) return false; + const q = focusedItem.question; + return Boolean(q?.trim()) || formulationStep === "active" || processingStep === "active"; + } + + // Simulate deconstruct success — result populated for u1 + function simulateDeconstructComplete() { + focusedInvestigations["u1"] = { + status: "formulated", + question: "What is the impact of X?", + answer: "Some answer", + result: { + observations: ["Obs 1"], + uncertainties: [], + assumptions: [], + relationships: [], + possibleFollowUpQuestions: [], + }, + error: null, + }; + } + + // Simulate user clicking "Back to open questions" (clears focusedPresentationItemId) + function simulateBackToOpen() { + focusedPresentationItemId = null; + } + + // The OLD buggy click handler logic + function handleNodeClick_OLD(nodeId) { + const focusedItem = getFocusedInvestigation(); + if (!focusedItem?.question?.trim() || (focusedItem && !hasFocusedContent(focusedItem))) { + return selectedPresentationItemId === nodeId ? null : nodeId; + } + return null; + } + + // The NEW fixed click handler logic + function hasCompletedInvestigation(nid) { + const inv = focusedInvestigations?.[nid]; + return inv && inv.status === "formulated" && inv.result && typeof inv.question === "string" && inv.question.trim(); + } + + function handleNodeClick_NEW(nodeId) { + const focusedItem = getFocusedInvestigation(); + if (!focusedItem?.question?.trim() || (focusedItem && !hasFocusedContent(focusedItem))) { + if (hasCompletedInvestigation(nodeId) && selectedPresentationItemId === nodeId) { + focusedPresentationItemId = nodeId; // reopen! + return "reopened"; + } + return selectedPresentationItemId === nodeId ? null : nodeId; + } + return null; + } + + return { + getFocusedInvestigation, hasFocusedContent, simulateDeconstructComplete, + simulateBackToOpen, handleNodeClick_OLD, handleNodeClick_NEW, + get state() { + return { selectedPresentationItemId, focusedPresentationItemId, focusedInvestigations }; + }, + }; + } + + it("BUG: old handler — clicking same node after panel collapse does nothing", () => { + const s = simulateFocusedReopenState(); + // 1. User has u1 selected + s.selectedPresentationItemId = "u1"; + expect(s.state.focusedPresentationItemId).toBe(null); + + // 2. Submit focused answer — deconstruct completes + s.simulateDeconstructComplete(); + // Result data exists but display is collapsed because focusedPresentationItemId was never set during initial selection + + // 3. Panel collapse (or user navigates away) + s.simulateBackToOpen(); + + // 4. Click same node again — OLD handler + const result = s.handleNodeClick_OLD("u1"); + // BUG: result is null because selectedPresentationItemId === "u1" → toggles to null + expect(result).toBe(null); + }); + + it("FIX: new handler — clicking same node after panel collapse reopens completed result", () => { + const s = simulateFocusedReopenState(); + // 1. User has u1 selected + s.selectedPresentationItemId = "u1"; + s.focusedPresentationItemId = null; + + // 2. Deconstruct completes — result exists in focusedInvestigations + s.simulateDeconstructComplete(); + + // 3. User clicks "Back to open questions" → collapses panel + s.simulateBackToOpen(); + + // 4. Click same node again — NEW handler + const result = s.handleNodeClick_NEW("u1"); + // FIX: should reopen because hasCompletedInvestigation("u1") is true and selected == u1 + expect(result).toBe("reopened"); + expect(s.state.focusedPresentationItemId).toBe("u1"); + + // 5. After reopening, focused state is restored + const focused = s.getFocusedInvestigation(); + expect(focused).not.toBe(null); + expect(focused.result.observations).toEqual(["Obs 1"]); + }); + + it("FIX: clicking a different node first then original still works (workaround no longer required)", () => { + const s = simulateFocusedReopenState(); + s.selectedPresentationItemId = "u1"; + s.focusedPresentationItemId = null; + s.simulateDeconstructComplete(); + s.simulateBackToOpen(); + + // OLD workaround: click u2 first + const resultU2 = s.handleNodeClick_NEW("u2"); + expect(resultU2).toBe("u2"); // now selected is u2 + + // Then click u1 again — should still reopen + const resultU1 = s.handleNodeClick_NEW("u1"); + expect(resultU1).toBe("reopened"); + expect(s.state.focusedPresentationItemId).toBe("u1"); + }); + + it("FIX: new node with no completed result toggles selection normally", () => { + const s = simulateFocusedReopenState(); + s.selectedPresentationItemId = null; + s.focusedPresentationItemId = null; + + // Click a fresh node (no results) — should set selection + const result = s.handleNodeClick_NEW("u2"); + expect(result).toBe("u2"); + }); + + it("INVARIANT: hasCompletedInvestigation only returns true for formulated+result", () => { + const focusedInvestigations = {}; + + function hasCompletedInvestigation(nid) { + const inv = focusedInvestigations?.[nid]; + return Boolean(inv && inv.status === "formulated" && inv.result && typeof inv.question === "string" && inv.question.trim()); + } + + // Empty → false + expect(hasCompletedInvestigation("u1")).toBe(false); + + // Formulating (not complete) → false + focusedInvestigations["u2"] = { status: "formulating", question: "", result: null }; + expect(hasCompletedInvestigation("u2")).toBe(false); + + // No result → false + focusedInvestigations["u3"] = { status: "formulated", question: "?", result: null }; + expect(hasCompletedInvestigation("u3")).toBe(false); + + // Empty question → false + focusedInvestigations["u4"] = { status: "formulated", question: "", result: {} }; + expect(hasCompletedInvestigation("u4")).toBe(false); + + // Complete → true + focusedInvestigations["u5"] = { status: "formulated", question: "What?", result: { observations: [] } }; + expect(hasCompletedInvestigation("u5")).toBe(true); + }); +});