fix(confidence-engine): preserve follow-up context while processing
This commit is contained in:
@@ -2250,4 +2250,253 @@ describe("v0.49 RENDERED — in-place follow-up context ownership", () => {
|
||||
expect(currentQuestionLabels).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── v0.51 CASE A: processing preserves workspace context ─────────────
|
||||
describe("processing state preserves completed context and active follow-up", () => {
|
||||
it("completed narrative remains visible during processing (hasCompletedContext stays true via contributions fallback)", 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-submit processing: result still exists (hasCompletedContext stays true),
|
||||
// but processingStep === "active" used to collapse context.
|
||||
renderFQB({
|
||||
focused: {
|
||||
question: followUpQ,
|
||||
answer: null,
|
||||
status: "formulated",
|
||||
result: {
|
||||
observations: [contrib.observations[0]],
|
||||
uncertainties: ["Is this causal?"],
|
||||
possibleFollowUpQuestions: [followUpQ, "How does it compare to competitors?"],
|
||||
assumptions: [],
|
||||
relationships: [],
|
||||
},
|
||||
error: null,
|
||||
},
|
||||
focusedContributions: [contrib],
|
||||
processingStep: "active", // ← THIS IS THE DEFECT: hasCompletedContext becomes false
|
||||
deconstructMsg: "Working through your response…", // matches DECONSTRUCT_MESSAGES[0]
|
||||
});
|
||||
|
||||
// Completed context must remain during processing
|
||||
expect(screen.getByText("Previously answered")).toBeInTheDocument();
|
||||
expect(screen.getByText(prevQ)).toBeInTheDocument();
|
||||
expect(screen.getByText("Your response")).toBeInTheDocument();
|
||||
expect(screen.getByText(prevA)).toBeInTheDocument();
|
||||
|
||||
// Active follow-up question remains visible under Questions this raises
|
||||
expect(screen.getByText(followUpQ)).toBeInTheDocument();
|
||||
expect(screen.getByText("Questions this raises")).toBeInTheDocument();
|
||||
|
||||
// Processing message present (spinner + text)
|
||||
expect(screen.getByText(/Working through your response/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("active follow-up textarea present but disabled during processing, spinner shown", async () => {
|
||||
const contrib = makeContrib("Q3", "A3", 1);
|
||||
|
||||
renderFQB({
|
||||
focused: {
|
||||
question: "Q4",
|
||||
answer: null,
|
||||
status: "formulated",
|
||||
result: { observations: [], uncertainties: [], possibleFollowUpQuestions: ["Q4"] },
|
||||
error: null,
|
||||
},
|
||||
focusedContributions: [contrib],
|
||||
processingStep: "active",
|
||||
deconstructMsg: "Working through your response…",
|
||||
});
|
||||
|
||||
// In-place textarea visible (not hidden) but disabled during processing
|
||||
const followUpTextarea = screen.queryAllByTestId("follow-up-textarea");
|
||||
expect(followUpTextarea).toHaveLength(1);
|
||||
expect(followUpTextarea[0].disabled).toBe(true);
|
||||
|
||||
// Submit button also disabled
|
||||
const submitBtn = screen.getByRole("button", { name: /submit/i });
|
||||
expect(submitBtn.disabled).toBe(true);
|
||||
|
||||
// Processing message visible
|
||||
expect(screen.getByText(/Working through your response/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("previous contributions data available during processing for derived sections", async () => {
|
||||
const contrib = makeContrib("Q3", "A3", 1);
|
||||
|
||||
renderFQB({
|
||||
focused: {
|
||||
question: "Q4",
|
||||
answer: null,
|
||||
status: "formulated",
|
||||
result: { observations: [], uncertainties: [], possibleFollowUpQuestions: ["Q4"] },
|
||||
error: null,
|
||||
},
|
||||
focusedContributions: [contrib],
|
||||
processingStep: "active",
|
||||
});
|
||||
|
||||
// Derived sections (from priorContribs fallback) remain visible — "What this tells us" etc.
|
||||
expect(screen.getByText("What this tells us")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("no top-level QUESTION Q4 screen during processing", async () => {
|
||||
const contrib = makeContrib("Q3", "A3", 1);
|
||||
|
||||
renderFQB({
|
||||
focused: {
|
||||
question: "Q4",
|
||||
answer: null,
|
||||
status: "formulated",
|
||||
result: { observations: [], uncertainties: [], possibleFollowUpQuestions: ["Q4"] },
|
||||
error: null,
|
||||
},
|
||||
focusedContributions: [contrib],
|
||||
processingStep: "active",
|
||||
});
|
||||
|
||||
// Should NOT show a top-level "Question" heading (the active block only)
|
||||
const questionHeadings = screen.queryAllByText(/^Question$/);
|
||||
expect(questionHeadings).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── v0.51 CASE B: error preserves workspace context ────────────────
|
||||
describe("error state preserves completed context and active follow-up", () => {
|
||||
it("completed narrative remains visible after deconstruction failure", 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?";
|
||||
|
||||
// Prior contribution includes possibleFollowUpQuestions (real deconstruction always returns them)
|
||||
const contrib = { ...makeContrib(prevQ, prevA, 1), possibleFollowUpQuestions: [followUpQ] };
|
||||
|
||||
// Simulate error state: result cleared to null by handleDeconstructSubmit catch block
|
||||
renderFQB({
|
||||
focused: {
|
||||
question: followUpQ,
|
||||
answer: null,
|
||||
status: "formulated",
|
||||
result: null, // ← NULLED BY ERROR HANDLER (but priorContribs still has the data)
|
||||
error: "Deconstruction failed",
|
||||
},
|
||||
focusedContributions: [contrib],
|
||||
processingStep: "idle",
|
||||
});
|
||||
|
||||
// Completed context from contributions must survive the error
|
||||
expect(screen.getByText("Previously answered")).toBeInTheDocument();
|
||||
expect(screen.getByText(prevQ)).toBeInTheDocument();
|
||||
expect(screen.getByText("Your response")).toBeInTheDocument();
|
||||
expect(screen.getByText(prevA)).toBeInTheDocument();
|
||||
|
||||
// Active follow-up remains under Questions This Raises
|
||||
expect(screen.getByText(followUpQ)).toBeInTheDocument();
|
||||
expect(screen.getByText("Questions this raises")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("failed submitted response visible with YOUR RESPONSE label", async () => {
|
||||
const prevQ = "Q3";
|
||||
const failedAnswer = "My detailed answer that couldn't be processed.";
|
||||
const contrib = makeContrib(prevQ, "Previous turn answer", 1);
|
||||
|
||||
renderFQB({
|
||||
focused: {
|
||||
question: "Q4",
|
||||
answer: null, // no fresh answer to show yet
|
||||
status: "formulated",
|
||||
result: null,
|
||||
error: "Deconstruction failed",
|
||||
},
|
||||
focusedAnswer: failedAnswer, // ← this is what the user typed before failure
|
||||
focusedContributions: [contrib],
|
||||
processingStep: "idle",
|
||||
});
|
||||
|
||||
// Error message visible
|
||||
expect(screen.getByText(/unable to process/i)).toBeInTheDocument();
|
||||
|
||||
// Retry button visible
|
||||
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("derived sections remain during error (priorContribs fallback active)", async () => {
|
||||
const contrib = makeContrib("Q3", "A3", 1);
|
||||
|
||||
renderFQB({
|
||||
focused: {
|
||||
question: "Q4",
|
||||
answer: null,
|
||||
status: "formulated",
|
||||
result: null,
|
||||
error: "Deconstruction failed",
|
||||
},
|
||||
focusedContributions: [contrib],
|
||||
processingStep: "idle",
|
||||
});
|
||||
|
||||
// Derived sections from priorContribs fallback remain visible during error
|
||||
expect(screen.getByText("What this tells us")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("no top-level QUESTION Q4 screen during error", async () => {
|
||||
const contrib = makeContrib("Q3", "A3", 1);
|
||||
|
||||
renderFQB({
|
||||
focused: {
|
||||
question: "Q4",
|
||||
answer: null,
|
||||
status: "formulated",
|
||||
result: null,
|
||||
error: "Deconstruction failed",
|
||||
},
|
||||
focusedContributions: [contrib],
|
||||
processingStep: "idle",
|
||||
});
|
||||
|
||||
// Should NOT show a top-level "Question" heading (follow-up stays in place)
|
||||
const questionHeadings = screen.queryAllByText(/^Question$/);
|
||||
expect(questionHeadings).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ── v0.51 CASE C: retry returns to processing path ────────────────
|
||||
describe("retry returns to processing without duplicating follow-up", () => {
|
||||
it("retry does not clear previous context or duplicate the follow-up question", 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?";
|
||||
|
||||
// Prior contribution includes possibleFollowUpQuestions (real deconstruction always returns them)
|
||||
const contrib = { ...makeContrib(prevQ, prevA, 1), possibleFollowUpQuestions: [followUpQ] };
|
||||
|
||||
// Simulate pre-retry state: error was just retried, processing re-activates
|
||||
renderFQB({
|
||||
focused: {
|
||||
question: followUpQ,
|
||||
answer: null,
|
||||
status: "formulated",
|
||||
result: null, // still null until retry completes
|
||||
error: null, // cleared by retry before re-submitting
|
||||
},
|
||||
focusedContributions: [contrib],
|
||||
processingStep: "active", // retry re-enters processing
|
||||
deconstructMsg: "Working through your response…",
|
||||
});
|
||||
|
||||
// Previous completed turn remains visible (from contributions)
|
||||
expect(screen.getByText("Previously answered")).toBeInTheDocument();
|
||||
expect(screen.getByText(prevQ)).toBeInTheDocument();
|
||||
|
||||
// Follow-up question appears exactly once in active block (not duplicated)
|
||||
const q4Elements = screen.getAllByText(followUpQ);
|
||||
expect(q4Elements).toHaveLength(1);
|
||||
|
||||
// Processing indicator shows again
|
||||
expect(screen.getByText(/Working through your response/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user