/** * Regression: Reopen a Done-for-now thread should resume from accumulated * contribution history instead of restarting from the original focused question. * * Invariants verified by these simulations (mirroring reasoning-workspace.jsx * logic exactly): * A. Fresh thread → startFocused resets + formulates (unchanged) * B. Reopened+has → startFocused preserves accumulated state, no formulate * C. Prior contrib preserved and visible * D. No follow-up auto-selected * E. User selects follow-up B -> B becomes active, same original targetNodeId * F. Submit answer to B -> new contribution appended, previous still present */ import { describe, expect, it } from "vitest"; // --- helpers that mirror production code exactly --- function simulateAppendContribution(contributions, contribution) { return [ ...contributions, { ...contribution, id: `contrib-${String(contributions.length + 1).padStart(4, "0")}`, sequence: contributions.length + 1, }, ]; } /** * Simulates startFocused(nodeId) with priorContribs check. * Mirrors reasoning-workspace.jsx:1118-1158 exactly. * Returns {investigations, formulationCalled}. */ function simulateStartFocused(nodeId, focusedInvestigations, focusedContributions) { const target = nodeId; let updatedInvestigations = { ...focusedInvestigations }; let formulationCalled = false; const priorContribs = (focusedContributions || []).filter( (c) => c.targetNodeId === target, ); if (priorContribs.length > 0) { // Reopen path - resumes from accumulated contribution history. const latest = priorContribs[priorContribs.length - 1]; updatedInvestigations = { ...updatedInvestigations, [target]: { status: "formulated", question: latest.question || "", answer: latest.answer ?? null, result: latest.possibleFollowUpQuestions ? { observations: latest.observations || [], uncertainties: latest.uncertainties || [], assumptions: latest.assumptions || [], relationships: latest.relationships || [], possibleFollowUpQuestions: latest.possibleFollowUpQuestions, } : null, error: null, }, }; // doFormulate is NOT called formulationCalled = false; } else { // Fresh thread path - unchanged original behaviour. updatedInvestigations = { ...updatedInvestigations, [target]: { status: "formulating", question: "", answer: null, result: null, error: null, }, }; formulationCalled = true; } return { investigations: updatedInvestigations, formulationCalled }; } /** Simulates setFollowUpQuestion(followUpText). */ function simulateSetFollowUpQuestion(investigations, targetNodeId, followUpText) { const updated = { ...investigations }; if (!updated[targetNodeId]) return updated; updated[targetNodeId] = { ...updated[targetNodeId], question: followUpText.trim(), answer: null, }; return updated; } /** Simulates handleDeconstructSubmit -> appends contribution + updates investigations. */ function simulateDeconstructSubmit(investigations, focusedContributions, targetNodeId, answerText, deconstructResult) { // Append contribution const newContrib = simulateAppendContribution(focusedContributions, { targetNodeId, question: investigations[targetNodeId]?.question || "", answer: answerText, observations: deconstructResult.observations || [], uncertainties: deconstructResult.uncertainties || [], assumptions: deconstructResult.assumptions || [], relationships: deconstructResult.relationships || [], possibleFollowUpQuestions: deconstructResult.possibleFollowUpQuestions || [], }); // Update investigations entry const updatedInvestigations = { ...investigations, [targetNodeId]: { ...investigations[targetNodeId], result: deconstructResult, answer: answerText, }, }; return { investigations: updatedInvestigations, contributions: newContrib }; } // --- A. Fresh thread reopen retains existing fresh-thread behaviour --- describe("reopen - fresh thread (no prior contributions)", () => { it("A: startFocused resets state and calls doFormulate when no contributions exist", () => { const nodeId = "nk-fresh"; const initialInvestigations = {}; const contributions = []; // empty - this is a fresh thread const result = simulateStartFocused(nodeId, initialInvestigations, contributions); expect(result.formulationCalled).toBe(true); expect(result.investigations[nodeId].status).toBe("formulating"); expect(result.investigations[nodeId].question).toBe(""); expect(result.investigations[nodeId].result).toBeNull(); }); it("A2: existing investigation state for a different node is preserved", () => { const freshId = "nk-fresh-2"; const otherId = "nk-other"; const priorState = { [otherId]: { status: "formulated", question: "Some other question?", answer: "Answer to other", result: null, error: null, }, }; const result = simulateStartFocused(freshId, priorState, []); expect(result.investigations[otherId].question).toBe("Some other question?"); expect(result.investigations[freshId].status).toBe("formulating"); }); }); // --- B. Reopened thread with contributions does NOT restart from original question --- describe("reopen - thread with prior contributions", () => { it("B: startFocused does NOT call doFormulate when prior contributions exist", () => { const nodeId = "nk-thread"; const contribution = simulateAppendContribution([], { targetNodeId: nodeId, question: "What is the founder's tacit knowledge?", answer: "Most processes are undocumented.", observations: ["documentation is minimal"], uncertainties: ["formal docs can't capture tacit knowledge"], assumptions: ["documentation is primary mechanism"], relationships: [{ from: "founder", to: "processes", type: "holds" }], possibleFollowUpQuestions: [ "What specific processes does the founder hold?", "How does knowledge transfer work in practice?", ], }); const result = simulateStartFocused(nodeId, {}, contribution); expect(result.formulationCalled).toBe(false); expect(result.investigations[nodeId].status).toBe("formulated"); expect(result.investigations[nodeId].question).toBe("What is the founder's tacit knowledge?"); expect(result.investigations[nodeId].result.possibleFollowUpQuestions).toEqual([ "What specific processes does the founder hold?", "How does knowledge transfer work in practice?", ]); }); it("B2: question from original (not new formulation) is preserved", () => { const nodeId = "nk-thread-orig"; const origQuestion = "Can the founder's processes be captured in documentation?"; const contribution = simulateAppendContribution([], { targetNodeId: nodeId, question: origQuestion, answer: "Partially - but critical knowledge remains tacit.", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: ["What specific processes are lost?"], }); const result = simulateStartFocused(nodeId, {}, contribution); expect(result.investigations[nodeId].question).toBe(origQuestion); }); }); // --- C. Prior contribution remains visible/preserved --- describe("prior contribution preservation", () => { it("C: prior contribution fields survive reopen", () => { const nodeId = "nk-preserve"; const contrib = simulateAppendContribution([], { targetNodeId: nodeId, question: "What are the core assumptions?", answer: "That the board has full information.", observations: ["board receives monthly reports"], uncertainties: ["whether reports contain sufficient detail"], assumptions: ["monthly cadence is adequate"], relationships: [{ from: "reports", to: "decisions", type: "informs" }], possibleFollowUpQuestions: ["What if reports are incomplete?"], }); const result = simulateStartFocused(nodeId, {}, contrib); expect(result.investigations[nodeId].answer).toBe("That the board has full information."); expect(result.investigations[nodeId].result.observations).toEqual(["board receives monthly reports"]); expect(result.investigations[nodeId].result.uncertainties).toEqual(["whether reports contain sufficient detail"]); expect(result.investigations[nodeId].result.assumptions).toEqual(["monthly cadence is adequate"]); expect(result.investigations[nodeId].result.relationships).toEqual([ { from: "reports", to: "decisions", type: "informs" }, ]); }); it("C2: multiple prior contributions - latest is restored", () => { const nodeId = "nk-multi"; let contributions = simulateAppendContribution([], { targetNodeId: nodeId, question: "Q1?", answer: "A1", observations: ["o1"], uncertainties: ["u1"], assumptions: ["a1"], relationships: [], possibleFollowUpQuestions: ["fuq-from-Q1"], }); contributions = simulateAppendContribution(contributions, { targetNodeId: nodeId, question: "Q2?", answer: "A2", observations: ["o2"], uncertainties: ["u2"], assumptions: ["a2"], relationships: [], possibleFollowUpQuestions: ["fuq-from-Q2"], }); const result = simulateStartFocused(nodeId, {}, contributions); expect(result.investigations[nodeId].question).toBe("Q2?"); expect(result.investigations[nodeId].answer).toBe("A2"); expect(result.investigations[nodeId].result.observations).toEqual(["o2"]); }); }); // --- D. No follow-up auto-selected after reopen --- describe("no automatic follow-up selection on reopen", () => { it("D: reopened thread shows follow-ups but does not set any as active", () => { const nodeId = "nk-no-auto"; const contributions = simulateAppendContribution([], { targetNodeId: nodeId, question: "What remains unclear?", answer: "The transition timeline.", observations: [], uncertainties: ["timing is uncertain"], assumptions: [], relationships: [], possibleFollowUpQuestions: ["What triggers Phase 2?", "Who owns Phase 3?"], }); const result = simulateStartFocused(nodeId, {}, contributions); // Follow-ups are visible in result but not auto-selected as the active question. expect(result.investigations[nodeId].result.possibleFollowUpQuestions).toHaveLength(2); // The active question remains what it was from prior work (not a follow-up). expect(result.investigations[nodeId].question).toBe("What remains unclear?"); }); }); // --- E. User selects follow-up B -> B becomes active --- describe("follow-up selection after reopen", () => { it("E: selecting a follow-up question updates the focused question, retains targetNodeId", () => { const nodeId = "nk-followup-e"; let contributions = simulateAppendContribution([], { targetNodeId: nodeId, question: "What are the key risks?", answer: "Regulatory and market risks.", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: ["Follow-up A", "Follow-up B"], }); let { investigations } = simulateStartFocused(nodeId, {}, contributions); // Verify the question from the contribution is active (not a follow-up) expect(investigations[nodeId].question).toBe("What are the key risks?"); // User clicks on "Follow-up B" investigations = simulateSetFollowUpQuestion(investigations, nodeId, "Follow-up B"); expect(investigations[nodeId].question).toBe("Follow-up B"); // targetNodeId is unchanged (the original graph node ID is retained) expect(contributions[0].targetNodeId).toBe(nodeId); }); }); // --- F. Submit answer to follow-up -> appends contribution --- describe("submitting follow-up answer appends new contribution", () => { it("F: deconstruct on follow-up B appends new contribution, preserves prior", () => { const nodeId = "nk-append-f"; // Step 1: initial contribution (from prior session) let contributions = simulateAppendContribution([], { targetNodeId: nodeId, question: "Initial investigation question?", answer: "Initial answer.", observations: ["o1"], uncertainties: ["u1"], assumptions: [], relationships: [], possibleFollowUpQuestions: ["Follow-up A", "Follow-up B"], }); // Step 2: reopen (simulated by startFocused restoring) let result = simulateStartFocused(nodeId, {}, contributions); let investigations = result.investigations; expect(investigations[nodeId].question).toBe("Initial investigation question?"); // Step 3: user selects follow-up B investigations = simulateSetFollowUpQuestion(investigations, nodeId, "Follow-up B"); expect(investigations[nodeId].question).toBe("Follow-up B"); // Step 4: submit answer to follow-up B (re-enters existing deconstruct path) const deconstructResult = { observations: ["o2", "o3"], uncertainties: ["u2"], assumptions: ["a1"], relationships: [{ from: "x", to: "y", type: "depends_on" }], possibleFollowUpQuestions: ["Next-level question?"], }; const submitResult = simulateDeconstructSubmit( investigations, contributions, nodeId, "Answer to follow-up B.", deconstructResult, ); // New contribution appended expect(submitResult.contributions).toHaveLength(2); expect(submitResult.contributions[1].question).toBe("Follow-up B"); expect(submitResult.contributions[1].answer).toBe("Answer to follow-up B."); // Previous contribution still present and unchanged expect(submitResult.contributions[0].question).toBe("Initial investigation question?"); expect(submitResult.contributions[0].targetNodeId).toBe(nodeId); // Same original targetNodeId retained expect(submitResult.contributions[0].targetNodeId).toBe(submitResult.contributions[1].targetNodeId); }); });