fix(confidence-engine): preserve follow-up progression ownership
This commit is contained in:
@@ -149,6 +149,20 @@ function FocusedQuestionBody({
|
|||||||
// Without this guard, selecting a follow-up question would erase "Previously answered" + "Your response".
|
// Without this guard, selecting a follow-up question would erase "Previously answered" + "Your response".
|
||||||
const hasCompletedContext = processingStep !== "active" && Boolean(focused?.result);
|
const hasCompletedContext = processingStep !== "active" && Boolean(focused?.result);
|
||||||
|
|
||||||
|
// ── Source of completed context: latest canonical Contribution when follow-up is active ──
|
||||||
|
// After setFollowUpQuestion() mutates focused.question/answer, derive from the
|
||||||
|
// latest completed Contribution so the narrative remains correct.
|
||||||
|
const hasActiveFollowUp = hasCompletedContext && !hasAnswer
|
||||||
|
&& (focused.result?.possibleFollowUpQuestions || []).some((q) => q === focused?.question);
|
||||||
|
const latestCompletedContrib = [...(focusedContributions || [])].reverse().find((c) => c?.question && c?.answer);
|
||||||
|
|
||||||
|
const displayedCompletedQuestion = hasActiveFollowUp
|
||||||
|
? (latestCompletedContrib?.question ?? focused?.question)
|
||||||
|
: focused?.question;
|
||||||
|
const displayedCompletedAnswer = hasActiveFollowUp
|
||||||
|
? (latestCompletedContrib?.answer ?? focused?.answer ?? "")
|
||||||
|
: focused?.answer;
|
||||||
|
|
||||||
// ── Local correction state (FQB-owned, not propagated upward) ─
|
// ── Local correction state (FQB-owned, not propagated upward) ─
|
||||||
const [editingFindingId, setEditingFindingId] = useState(null);
|
const [editingFindingId, setEditingFindingId] = useState(null);
|
||||||
const [draft, setDraft] = useState("");
|
const [draft, setDraft] = useState("");
|
||||||
@@ -180,15 +194,15 @@ function FocusedQuestionBody({
|
|||||||
<div className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
|
<div className="space-y-4 rounded-lg border border-gray-200 bg-white p-5">
|
||||||
{(hasAnswer || hasCompletedContext) && focused?.question?.trim() ? (
|
{(hasAnswer || hasCompletedContext) && focused?.question?.trim() ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Previously answered question */}
|
{/* Previously answered question — sourced from contribution when follow-up is active */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Previously answered</h3>
|
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Previously answered</h3>
|
||||||
<p className="text-base font-medium leading-relaxed text-gray-900">{focused.question}</p>
|
<p className="text-base font-medium leading-relaxed text-gray-900">{displayedCompletedQuestion}</p>
|
||||||
</div>
|
</div>
|
||||||
{/* User's verbatim response — distinct provenance from Engine-derived content */}
|
{/* User's verbatim response — distinct provenance from Engine-derived content */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Your response</h3>
|
<h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Your response</h3>
|
||||||
<p className="text-sm leading-relaxed text-gray-800">{focused.answer}</p>
|
<p className="text-sm leading-relaxed text-gray-800">{displayedCompletedAnswer}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : focused?.question?.trim() ? (
|
) : focused?.question?.trim() ? (
|
||||||
@@ -200,7 +214,7 @@ function FocusedQuestionBody({
|
|||||||
<p className="text-sm text-blue-600/70">{formulateMsg}</p>
|
<p className="text-sm text-blue-600/70">{formulateMsg}</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && !hasAnswer && (
|
{focused?.question?.trim() && processingStep !== "active" && focused.status === "formulated" && !hasAnswer && !hasActiveFollowUp && (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={`rw-answer-${nodeId}`} className="mb-2 block text-sm font-medium text-gray-700">Your response</label>
|
<label htmlFor={`rw-answer-${nodeId}`} className="mb-2 block text-sm font-medium text-gray-700">Your response</label>
|
||||||
<textarea id={`rw-answer-${nodeId}`} value={focusedAnswer} onChange={(e) => setFocusedAnswer(e.target.value)} rows={4} data-testid="response-textarea" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 disabled:cursor-not-allowed disabled:opacity-60" placeholder="What do you know about this?" />
|
<textarea id={`rw-answer-${nodeId}`} value={focusedAnswer} onChange={(e) => setFocusedAnswer(e.target.value)} rows={4} data-testid="response-textarea" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 disabled:cursor-not-allowed disabled:opacity-60" placeholder="What do you know about this?" />
|
||||||
@@ -212,9 +226,8 @@ function FocusedQuestionBody({
|
|||||||
|
|
||||||
{focused?.result && (
|
{focused?.result && (
|
||||||
<>
|
<>
|
||||||
{/* Prior accumulated learning (prior turns, current turn excluded — shown above) */}
|
{/* Prior accumulated learning removed from left pane — SecondaryPreviousLearning on the right owns historical Previous Learning exclusively */}
|
||||||
<PriorContributionsSummary nodeId={nodeId} contributions={focusedContributions || []} findings={findings} />
|
{/* PriorContributionsSummary was causing duplication in the two-column focused workspace */}
|
||||||
|
|
||||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h3><ul className="list-disc pl-5 space-y-2">{(currentFindings?.length ? currentFindings : (focused.result.observations || [])).map((item, i) => {
|
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">What this tells us</h3><ul className="list-disc pl-5 space-y-2">{(currentFindings?.length ? currentFindings : (focused.result.observations || [])).map((item, i) => {
|
||||||
const isFinding = typeof item === "object" && item !== null && "id" in item;
|
const isFinding = typeof item === "object" && item !== null && "id" in item;
|
||||||
const disposition = isFinding ? item.userDisposition : null;
|
const disposition = isFinding ? item.userDisposition : null;
|
||||||
@@ -284,6 +297,30 @@ function FocusedQuestionBody({
|
|||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-gray-400">None yet</p>
|
<p className="text-xs text-gray-400">None yet</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* In-place answer textarea for the active follow-up — renders only when a candidate is selected */}
|
||||||
|
{hasActiveFollowUp ? (
|
||||||
|
<div className="mt-3 space-y-2">
|
||||||
|
<p className="text-sm font-medium text-gray-900">{focused.question}</p>
|
||||||
|
<textarea
|
||||||
|
id={`rw-answer-fu-${nodeId}`}
|
||||||
|
value={focusedAnswer}
|
||||||
|
onChange={(e) => setFocusedAnswer(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
data-testid="follow-up-textarea"
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:border-gray-500 focus:outline-none focus:ring-2 focus:ring-gray-400 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
placeholder="What do you know about this?"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleDeconstructSubmit(nodeId, focusedAnswer); }}
|
||||||
|
disabled={!focusedAnswer.trim() || processingStep === "active"}
|
||||||
|
style={{ cursor: !focusedAnswer.trim() || processingStep === "active" ? "not-allowed" : "pointer" }}
|
||||||
|
className="rounded-lg border border-green-600 bg-white px-4 py-2 text-sm font-medium text-green-700 hover:bg-green-50 transition disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Submit response
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Assumptions</h3><ul className="list-disc pl-5 space-y-1">{(focused.result.assumptions || []).map((a, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{a}</li>))}</ul></div>
|
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Assumptions</h3><ul className="list-disc pl-5 space-y-1">{(focused.result.assumptions || []).map((a, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{a}</li>))}</ul></div>
|
||||||
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Connections</h3><ul className="list-disc pl-5 space-y-1">{(focused.result.relationships || []).map((r, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{r.from} → {r.to} ({r.type})</li>))}</ul></div>
|
<div><h3 className="mb-1 text-[11px] font-semibold tracking-widest uppercase text-gray-500">Connections</h3><ul className="list-disc pl-5 space-y-1">{(focused.result.relationships || []).map((r, i) => (<li key={i} className="text-sm leading-relaxed text-gray-700">{r.from} → {r.to} ({r.type})</li>))}</ul></div>
|
||||||
@@ -1074,7 +1111,7 @@ function FocusedInvestigationWorkspace({
|
|||||||
setFocusedPresentationItemId={setFocusedPresentationItemId}
|
setFocusedPresentationItemId={setFocusedPresentationItemId}
|
||||||
setDoneForNowIds={setDoneForNowIds}
|
setDoneForNowIds={setDoneForNowIds}
|
||||||
setFollowUpQuestion={setFollowUpQuestion}
|
setFollowUpQuestion={setFollowUpQuestion}
|
||||||
focusedContributions={hasResult ? [] : (focusedContributions || [])}
|
focusedContributions={focusedContributions || []}
|
||||||
currentFindings={currentFindings || []}
|
currentFindings={currentFindings || []}
|
||||||
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
onUpdateFindingDisposition={onUpdateFindingDisposition}
|
||||||
onUpdateFindingProposition={onUpdateFindingProposition}
|
onUpdateFindingProposition={onUpdateFindingProposition}
|
||||||
@@ -1208,6 +1245,9 @@ function OpenQuestionsPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export for testability of in-place follow-up ownership repair
|
||||||
|
export { FocusedQuestionBody, SecondaryPreviousLearning };
|
||||||
|
|
||||||
export default function ReasoningWorkspace({
|
export default function ReasoningWorkspace({
|
||||||
scenario,
|
scenario,
|
||||||
status,
|
status,
|
||||||
|
|||||||
@@ -2092,3 +2092,69 @@ previous question
|
|||||||
```
|
```
|
||||||
|
|
||||||
Exact UI labels and wording remain subject to later UX refinement. This principle is the durable separation of provenance; it should guide the upcoming completed-result presentation work.
|
Exact UI labels and wording remain subject to later UX refinement. This principle is the durable separation of provenance; it should guide the upcoming completed-result presentation work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### v0.49 FOLLOW-UP PRESENTATION OWNERSHIP REPAIR
|
||||||
|
|
||||||
|
**Status:** VERIFIED (tests + build + Playwright live)
|
||||||
|
**Branch:** `feature/finding-informed-understanding-v0.49`
|
||||||
|
|
||||||
|
#### Repair scope
|
||||||
|
|
||||||
|
The in-place follow-up repair (completed turn provenance preservation) introduced an overloaded responsibility: passing the full historical `Contributions` array into `FocusedQuestionBody` served two purposes simultaneously — recovering the latest completed Q3/A3 for "Previously Answered / Your Response" AND rendering "Previous Learning" history via `PriorContributionsSummary`.
|
||||||
|
|
||||||
|
Those are distinct presentation responsibilities. Separating them exposes a duplication defect: "Previous Learning" appeared on BOTH the left current-progression pane AND the right historical column.
|
||||||
|
|
||||||
|
#### What changed
|
||||||
|
|
||||||
|
- **In-place follow-up repair retained** — canonical Q3/A3 provenance recovered from the latest completed Contribution via `latestCompletedContribution` derivation; top-textarea suppression via `hasActiveFollowUp`; in-place Q4 textarea under "Questions This Raises"; Previous Learning newest-first ordering.
|
||||||
|
- **Duplicate Previous Learning removed** — `PriorContributionsSummary` embedded rendering removed from `FocusedQuestionBody` inside the two-column focused workspace (`FocusedInvestigationWorkspace`).
|
||||||
|
- **Presentation ownership separated:**
|
||||||
|
- `latestCompletedContribution` (derived once inside `FocusedQuestionBody`) supplies provenance context for Q3/A3 independently of history presentation.
|
||||||
|
- Left pane now owns only current/latest progression: Previously Answered, Your Response, What This Tells Us, Still Unclear, Questions This Raises, active follow-up controls.
|
||||||
|
- Right-side `SecondaryPreviousLearning` exclusively owns older-turn history with "Previous Learning" heading and newest-first ordering.
|
||||||
|
- **Exactly one "Previous Learning" surface** across the focused workspace.
|
||||||
|
- **Post-answer promotion preserved:** After submitting Q4, it became the latest completed narrative; Turn 3 (the previous current turn) promoted to first item in Previous Learning; Turn 2 and Turn 1 follow in order.
|
||||||
|
|
||||||
|
#### Test results
|
||||||
|
|
||||||
|
- **Tests passed:** 116/116 (`tests/open-questions-vs-assumptions.test.jsx`)
|
||||||
|
- **Build:** PASS
|
||||||
|
- **Full Vitest:** NOT RUN (bounded scope)
|
||||||
|
|
||||||
|
#### Playwright live verification
|
||||||
|
|
||||||
|
- Correct Q3 retained under "Previously Answered": YES
|
||||||
|
- Correct A3 retained under "Your Response": YES
|
||||||
|
- Q4 remains in place as follow-up textarea: YES
|
||||||
|
- Visible textarea count: 1
|
||||||
|
- Top duplicate "Question" heading: ABSENT
|
||||||
|
- Left-side Previous Learning visible: ABSENT (removed)
|
||||||
|
- Right-side Previous Learning visible: YES (exactly one)
|
||||||
|
- Previous Learning heading count across workspace: 1
|
||||||
|
- Previous Learning order: newest-first (Turn 2 → Turn 1 before fix; Turn 3 → Turn 2 → Turn 1 after post-answer submission)
|
||||||
|
|
||||||
|
#### Post-answer verification
|
||||||
|
|
||||||
|
- Natural focused answers submitted: 1 (Q4 inline answer)
|
||||||
|
- New turn became latest narrative: YES (Q4 text under "Previously Answered")
|
||||||
|
- Previous turn became first historical item: YES (Turn 3 as first entry in Previous Learning on the right)
|
||||||
|
- Single Previous Learning surface preserved: YES
|
||||||
|
|
||||||
|
#### Critical regression boundaries
|
||||||
|
|
||||||
|
- Persistence changed: NO
|
||||||
|
- Contribution schema changed: NO
|
||||||
|
- Finding semantics changed: NO
|
||||||
|
- Graph reasoning changed: NO
|
||||||
|
- Current Understanding changed: NO
|
||||||
|
- Workspace controls changed: NO
|
||||||
|
|
||||||
|
#### Next position
|
||||||
|
|
||||||
|
- Completed-result provenance: CLOSED
|
||||||
|
- In-place follow-up continuation: CLOSED
|
||||||
|
- Previous Learning single-owner presentation: CLOSED
|
||||||
|
- Previous Learning newest-first: CLOSED
|
||||||
|
- Post-answer promotion: PRESERVED
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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
|
// ── Simulated rendering logic extracted from reasoning-workspace.jsx
|
||||||
// Mirrors the exact filter + conditional structure used in the
|
// 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);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+1
-1
@@ -6,6 +6,6 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: { globals: true },
|
test: { globals: true, environment: "jsdom" },
|
||||||
resolve: { alias: { "@": path.resolve(__dirname, ".") } },
|
resolve: { alias: { "@": path.resolve(__dirname, ".") } },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user