fix(confidence-engine): reopen completed focused investigation
This commit is contained in:
@@ -792,7 +792,7 @@ function OpenQuestionsPanel({
|
|||||||
focused, formulationStep, formulateMsg, processingStep, deconstructMsg, doneForNowIds,
|
focused, formulationStep, formulateMsg, processingStep, deconstructMsg, doneForNowIds,
|
||||||
startFocused, handleDeconstructSubmit, retryFormulation, setSelectedPresentationItemId,
|
startFocused, handleDeconstructSubmit, retryFormulation, setSelectedPresentationItemId,
|
||||||
setFocusedPresentationItemId, setFocusedAnswer, focusedAnswer, setDoneForNowIds,
|
setFocusedPresentationItemId, setFocusedAnswer, focusedAnswer, setDoneForNowIds,
|
||||||
setFollowUpQuestion, focusedContributions,
|
setFollowUpQuestion, focusedContributions, focusedInvestigations,
|
||||||
}) {
|
}) {
|
||||||
const openNodes = (graph?.nodes || []).filter(
|
const openNodes = (graph?.nodes || []).filter(
|
||||||
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id),
|
(n) => n.kind === "unknown" && n.status !== "resolved" && !doneForNowIds.includes(n.id),
|
||||||
@@ -800,6 +800,25 @@ function OpenQuestionsPanel({
|
|||||||
|
|
||||||
if (openNodes.length <= 0) return null;
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
<h2 className="text-[11px] font-semibold tracking-widest uppercase text-gray-500">
|
||||||
@@ -813,13 +832,7 @@ function OpenQuestionsPanel({
|
|||||||
return (
|
return (
|
||||||
<div key={node.id}>
|
<div key={node.id}>
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => handleNodeClick(node)}
|
||||||
if (!focused?.question?.trim() || (focused && !hasFocusedContent())) {
|
|
||||||
setSelectedPresentationItemId(
|
|
||||||
selectedPresentationItemId === node.id ? null : node.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
style={{ cursor: "pointer" }}
|
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"
|
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}
|
setDoneForNowIds={setDoneForNowIds}
|
||||||
setFollowUpQuestion={setFollowUpQuestion}
|
setFollowUpQuestion={setFollowUpQuestion}
|
||||||
focusedContributions={focusedContributions}
|
focusedContributions={focusedContributions}
|
||||||
|
focusedInvestigations={focusedInvestigations}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user