fix(confidence-engine): preserve follow-up progression ownership

This commit is contained in:
2026-08-30 08:28:29 +01:00
parent 17c6048047
commit 8bded90094
4 changed files with 408 additions and 9 deletions
@@ -1,4 +1,8 @@
import { describe, expect, it } from "vitest";
import "@testing-library/jest-dom/vitest";
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { FocusedQuestionBody, SecondaryPreviousLearning } from "@/components/reasoning-workspace";
// ── Simulated rendering logic extracted from reasoning-workspace.jsx
// Mirrors the exact filter + conditional structure used in the
@@ -1903,4 +1907,293 @@ describe("v0.49 Case A — completed turn provenance narrative", () => {
expect(focused.result.observations.some((f) => f === focused.answer)).toBe(false);
});
});
});
// ── v0.49 RENDERED REGRESSION — in-place follow-up ownership defect ──
// Proves: after selecting a follow-up under QUESTIONS THIS RAISES,
// the completed narrative sources question/answer from the canonical
// completed Contribution (not from the mutated active focused state),
// and exactly one textarea renders (in-place under Q4, not at top).
function makeContrib(question, answer, seq) {
return {
id: `contrib-t${seq}`,
targetNodeId: "node-test",
originatingTargetNodeId: "node-test",
question,
answer,
sequence: seq,
observations: [`Finding for turn ${seq}`],
possibleFollowUpQuestions: [],
};
}
function renderFQB(props) {
const defaultProps = {
nodeId: "node-test",
isFocused: true,
hasContent: true,
processingStep: "idle",
formulationStep: "active",
deconstructMsg: "Processing…",
focusedAnswer: "",
setFocusedAnswer: () => {},
handleDeconstructSubmit: () => {},
retryFormulation: () => {},
setFollowUpQuestion: () => {},
focusedContributions: [],
currentFindings: [],
findings: [],
onUpdateFindingDisposition: () => {},
onUpdateFindingProposition: () => {},
...props,
};
return render(<FocusedQuestionBody {...defaultProps} />);
}
describe("v0.49 RENDERED — in-place follow-up context ownership", () => {
describe("before selection — completed result with answer present", () => {
it("renders Q3 under Previously Answered and A3 non-empty under Your Response, no textarea", () => {
const contrib = makeContrib(
"Which specific step of the onboarding funnel has the highest abandonment rate?",
"Step 3 — account verification / phone confirmation.",
1,
);
renderFQB({
focused: {
question: contrib.question,
answer: contrib.answer,
status: "formulated",
result: {
observations: [contrib.observations[0]],
uncertainties: ["Is this a UX problem or trust issue?"],
possibleFollowUpQuestions: [
"What drives the Step 3 abandonment rate?",
"Can reducing friction at Step 3 reduce overall abandonment?",
],
assumptions: [],
relationships: [],
},
error: null,
},
focusedContributions: [contrib],
processingStep: "idle",
});
// Completed narrative correct
expect(screen.getByText("Previously answered")).toBeInTheDocument();
expect(screen.getByText(contrib.question)).toBeInTheDocument();
expect(screen.getByText("Your response")).toBeInTheDocument();
expect(screen.getByText(contrib.answer)).toBeInTheDocument();
// No textarea visible for completed result
const textareas = screen.queryAllByTestId("response-textarea");
expect(textareas).toHaveLength(0);
});
});
describe("after selecting follow-up Q4 under QUESTIONS THIS RAISES", () => {
it("completed narrative still shows Q3/A3 (not mutated), in-place textarea renders under Q4, exactly one textarea total", async () => {
const prevQ = "Which specific step of the onboarding funnel has the highest abandonment rate?";
const prevA = "Step 3 — account verification / phone confirmation.";
const followUpQ = "What drives the Step 3 abandonment rate?";
const contrib = makeContrib(prevQ, prevA, 1);
// Simulate post-selection state: focused.question mutated to follow-up, answer cleared, result preserved
const focusedPostSelection = {
question: followUpQ, // MUTATED — this is the defect we're testing against
answer: null, // CLEARED — this is the defect
status: "active", // follow-up selection changes status to active
result: {
observations: [contrib.observations[0]],
uncertainties: ["Is this a UX problem or trust issue?"],
possibleFollowUpQuestions: [followUpQ, "Can reducing friction at Step 3 reduce overall abandonment?"],
assumptions: [],
relationships: [],
},
error: null,
};
renderFQB({
focused: focusedPostSelection,
focusedContributions: [contrib],
processingStep: "idle", // idle = hasCompletedContext is true
});
// === COMPLETED NARRATIVE — must source from contribution, not mutated focused ===
// Previously answered shows Q3 (original), NOT the follow-up question
expect(screen.getByText("Previously answered")).toBeInTheDocument();
const pqText = screen.getByText(prevQ);
expect(pqText).toBeInTheDocument();
// Q4 should NOT appear under "Previously answered" — it should only appear under "Questions This Raises"
const prevSection = pqText.closest("div");
expect(prevSection?.textContent?.includes(followUpQ)).toBe(false);
// Your response shows A3 and is non-empty
expect(screen.getByText("Your response")).toBeInTheDocument();
const arText = screen.getByText(prevA);
expect(arText).toBeInTheDocument();
expect(arText.textContent?.trim().length).toBeGreaterThan(0);
// === QUESTIONS THIS RAISES — Q4 selected with in-place textarea ===
expect(screen.getByText("Questions this raises")).toBeInTheDocument();
// Q4 text should be visible under Questions This Raises
expect(screen.getByText(followUpQ)).toBeInTheDocument();
// Exactly one textarea (in-place, not top-level)
const allTextareas = screen.getAllByRole("textbox");
expect(allTextareas).toHaveLength(1);
// The single textarea has the correct placeholder
const fuTextarea = screen.getByPlaceholderText("What do you know about this?");
expect(fuTextarea).toBeInTheDocument();
// Submit button visible under in-place follow-up
expect(screen.getByRole("button", { name: /submit/i })).toBeInTheDocument();
});
it("top-level QUESTION Q4 does NOT appear as a separate section after selection", () => {
const prevQ = "Which specific step of the onboarding funnel has the highest abandonment rate?";
const followUpQ = "What drives the Step 3 abandonment rate?";
const contrib = makeContrib(prevQ, "Step 3 answer.", 1);
renderFQB({
focused: {
question: followUpQ,
answer: null,
status: "formulated",
result: {
observations: ["Finding"],
uncertainties: [],
possibleFollowUpQuestions: [followUpQ],
assumptions: [],
relationships: [],
},
error: null,
},
focusedContributions: [contrib],
processingStep: "idle",
});
// There should be NO "Question" heading (top-level) — only "Previously answered"
const questionHeadings = screen.queryAllByText("Question");
expect(questionHeadings).toHaveLength(0);
});
});
describe("v0.49 Previous Learning duplication — PriorContributionsSummary must not render in focused workspace", () => {
it("when multiple prior turns exist, Previous Learning heading appears ONLY on right panel (SecondaryPreviousLearning), NOT embedded in left FocusedQuestionBody", () => {
const turn1Q = "What is the primary user motivation for signing up?";
const turn1A = "To access premium features faster than free users.";
const turn2Q = "Which feature combination drives the most retention?";
const turn2A = "Analytics dashboard + automated reports at $29/mo tier.";
const currentQ = "What drives the Step 3 abandonment rate?";
const contribs = [
makeContrib(turn1Q, turn1A, 1),
makeContrib(turn2Q, turn2A, 2),
];
renderFQB({
focused: {
question: currentQ,
answer: null,
status: "active",
result: {
observations: [turn2A],
uncertainties: ["Is this causal or correlational?"],
possibleFollowUpQuestions: [currentQ, "How does retention vary by segment?"],
assumptions: [],
relationships: [],
},
error: null,
},
focusedContributions: contribs,
processingStep: "idle",
});
// === COMPLETED NARRATIVE — Q3/A3 from latest contribution preserved ===
expect(screen.getByText("Previously answered")).toBeInTheDocument();
const pqText = screen.getByText(turn2Q);
expect(pqText).toBeInTheDocument();
expect(screen.getByText("Your response")).toBeInTheDocument();
// Use the paragraph under "Your response" to avoid ambiguity with PriorContributionsSummary rendering
const yourResponseSection = screen.getByText("Your response").parentElement;
const arText = yourResponseSection?.querySelector("p");
expect(arText).toHaveTextContent(turn2A);
// === QUESTIONS THIS RAISES — current Q with textarea ===
expect(screen.getByText("Questions this raises")).toBeInTheDocument();
expect(screen.getByText(currentQ)).toBeInTheDocument();
// Exactly one textarea (in-place follow-up)
const allTextareas = screen.getAllByRole("textbox");
expect(allTextareas).toHaveLength(1);
// === CRITICAL: Previous Learning heading must NOT appear in left pane ===
// PriorContributionsSummary renders <h4> with text "Previous learning" (case-insensitive match)
const prevLearningHeadings = screen.queryAllByText(/previous learning/i);
expect(prevLearningHeadings.length).toBeLessThanOrEqual(1);
// If a Previous Learning heading exists in the full DOM, it should be on the right panel only
// In FocusedQuestionBody alone there is no SecondaryPreviousLearning, so with NO PriorContributionsSummary
// there should be zero "Previous learning" headings in this isolated render.
expect(prevLearningHeadings.length).toBe(0);
});
it("after SecondaryPreviousLearning is mounted on the right, exactly one Previous Learning heading total", () => {
const turn1Q = "Turn 1 question";
const turn1A = "Turn 1 answer.";
const turn2Q = "Turn 2 question";
const turn2A = "Turn 2 answer.";
const currentQ = "Current focused question?";
// Two prior contributions (the most recent will be excluded by slice(0,-1), Turn 1 remains)
const contribs = [makeContrib(turn1Q, turn1A, 1), makeContrib(turn2Q, turn2A, 2)];
// Render FQB + SecondaryPreviousLearning as the workspace does
render(
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
<FocusedQuestionBody
nodeId="node-test"
isFocused={true}
hasContent={true}
processingStep="idle"
formulationStep="active"
deconstructMsg=""
focusedAnswer=""
setFocusedAnswer={() => {}}
handleDeconstructSubmit={() => {}}
retryFormulation={() => {}}
setFollowUpQuestion={() => {}}
focused={
{
question: currentQ,
answer: null,
status: "active",
result: {
observations: ["finding"],
uncertainties: [],
possibleFollowUpQuestions: [currentQ],
assumptions: [],
relationships: [],
},
error: null,
}
}
focusedContributions={contribs}
/>
<SecondaryPreviousLearning nodeId="node-test" contributions={contribs} findings={[]} />
</div>,
);
// Exactly one Previous Learning heading across the two-column workspace
const prevLearningHeadings = screen.queryAllByText(/previous learning/i);
expect(prevLearningHeadings).toHaveLength(1);
});
});
});