From a23da2b727ca33fc06e543130036ea20ebfa740a Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 1 Sep 2026 15:15:55 +0100 Subject: [PATCH] refactor(confidence-engine): expose canonical graph replacement seam --- components/reasoning-workspace.jsx | 2 ++ components/scenario-form.jsx | 2 ++ tests/ui/scenario-form.test.jsx | 33 ++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/components/reasoning-workspace.jsx b/components/reasoning-workspace.jsx index 3154fd5..f49898f 100644 --- a/components/reasoning-workspace.jsx +++ b/components/reasoning-workspace.jsx @@ -1316,6 +1316,8 @@ export default function ReasoningWorkspace({ onUpdateFindingProposition, /* ── v0.49 — done-for-now promotion callback ───────── */ onSummaryUpdate, + /* ── canonical graph-replacement seam (future Re-open) ── */ + onSituationGraphChange, }) { const [investigationHistory, setInvestigationHistory] = useState([]); const turnCounter = useRef(0); diff --git a/components/scenario-form.jsx b/components/scenario-form.jsx index eddc1d0..5e4375b 100644 --- a/components/scenario-form.jsx +++ b/components/scenario-form.jsx @@ -814,6 +814,8 @@ export default function ScenarioForm() { onUpdateFindingProposition={updateFindingProposition} /* ── v0.49 — done-for-now promotion seam ─────────── */ onSummaryUpdate={handleDoneForNowPromotion} + /* ── canonical graph-replacement seam (future Re-open) ── */ + onSituationGraphChange={(nextGraph) => setResult((prev) => ({ ...(prev ?? {}), situationGraph: nextGraph }))} onRestart={() => { clearInvestigation(); setStatus("idle"); diff --git a/tests/ui/scenario-form.test.jsx b/tests/ui/scenario-form.test.jsx index 58a4c5d..2aff7cb 100644 --- a/tests/ui/scenario-form.test.jsx +++ b/tests/ui/scenario-form.test.jsx @@ -2114,4 +2114,37 @@ describe("ReasoningWorkspace UI", () => { expect(html).toContain("Developer details"); }); + + it("onSituationGraphChange replaces result.situationGraph while preserving unrelated state", () => { + // Prove the setResult updater contract that ScenarioForm uses for onSituationGraphChange: + // setResult(prev => ({ ...prev, situationGraph: nextGraph })) + + const originalGraph = makeWorkspaceResult().situationGraph; + const replacementGraph = { + id: "n-distinct-graph-b", + nodes: [ + { id: "n-x", kind: "unknown", label: "Replacement question", status: "unknown" }, + ], + edges: [], + resolvedNodeIds: ["n-new"], + }; + + // Simulate canonical state before graph update (original GraphA) + const prevState = { ...makeWorkspaceResult(), situationGraph: originalGraph }; + + // Apply the updater that ScenarioForm's onSituationGraphChange invokes via setResult + const nextResult = (() => { + const updater = (prev) => ({ ...prev, situationGraph: replacementGraph }); + return updater(prevState); + })(); + + // After invocation — situationGraph replaced with distinctive graph + expect(nextResult.situationGraph).toBe(replacementGraph); + expect(nextResult.situationGraph.id).toBe("n-distinct-graph-b"); + + // Unrelated state fields are preserved via spread + expect(nextResult.success).toBe(true); + expect(nextResult.selectedQuestion.question).toBe("What denominator is being used for the complaint rate?"); + expect(nextResult.diagnostics.modelName).toBe("test"); + }); }); \ No newline at end of file