fix(confidence-engine): preserve follow-up progression ownership
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user