Files
resolutionflow/frontend/e2e/assistant-chat-prefill.spec.ts
Michael Chihlas b56da2facd fix(chat): sync currentChatRef when prefill creates a new chat session
The dashboard prefill flow in AssistantChatPage set activeChatId after
creating a new session but never updated currentChatRef.current. Every
later handleSend / handleTaskSubmit then tripped the
`currentChatRef.current !== sentForChatId` guard that was supposed to
discard responses for stale chats — and silently dropped the AI's
follow-up. The user saw their submitted message but no assistant
reply, no toast, no task-lane update.

Mirrors what handleNewChat and handleResumeNew already do. Adds an
e2e regression test that drives the dashboard prefill, submits a
partial task-lane response, and asserts the second AI turn renders.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 00:24:02 -04:00

112 lines
4.5 KiB
TypeScript

import { expect, test } from '@playwright/test'
/**
* Regression test for the prefill-handoff `currentChatRef` bug.
*
* Symptom: a chat session created via the dashboard prefill flow
* looked fine on the first AI turn, but submitting partial answers
* from the task lane silently dropped the AI's follow-up response.
* The user saw their answers in the chat, no assistant reply, no
* toast.
*
* Root cause: the prefill effect in `AssistantChatPage` set
* `activeChatId` without also updating `currentChatRef.current`, so
* the `currentChatRef.current !== sentForChatId` guard in
* `handleTaskSubmit` (and `handleSend`) tripped on every subsequent
* request and discarded the AI response.
*
* Strategy: drive the real prefill flow against the real backend, but
* intercept the `/chat` endpoint with `page.route` so we get
* deterministic question payloads on turn 1 and a deterministic
* follow-up on turn 2. The fix is what makes turn 2 visible.
*/
test.describe('AssistantChatPage — prefill handoff regression', () => {
test('AI follow-up renders after submitting partial task lane answers', async ({ page }) => {
let chatCallCount = 0
// Clear any persisted active-chat-id so the page does not auto-resume a
// stale session left behind by a sibling spec.
await page.addInitScript(() => {
try {
sessionStorage.removeItem('rf-active-chat-id')
sessionStorage.removeItem('rf-tasklane-meta')
} catch { /* ignore */ }
})
// Intercept only the chat endpoint. Session creation, listSessions,
// facts, suggested-fixes, etc. all hit the real backend so the page
// renders normally — only the LLM call is deterministic. The pattern
// matches `/ai-sessions/<uuid>/chat` and nothing nested beneath it.
await page.route(/\/api\/v1\/ai-sessions\/[^/]+\/chat$/, async (route) => {
if (route.request().method() !== 'POST') {
await route.fallback()
return
}
chatCallCount += 1
if (chatCallCount === 1) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
content: 'Initial diagnostic plan. Please answer the questions in the task lane.',
suggested_flows: [],
fork: null,
actions: [],
questions: [
{ text: 'Has the user recently changed their password?' },
{ text: 'Is the lockout happening at a consistent time of day?' },
],
}),
})
return
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
content: 'Got it — based on your answer, here is what to check next.',
suggested_flows: [],
fork: null,
actions: [],
questions: [],
}),
})
})
// Drive the prefill flow exactly the way the dashboard does. The textarea
// is keyed by its placeholder copy on QuickStartPage.
await page.goto('/')
const prefillBox = page.getByPlaceholder(/Describe the issue/i)
await expect(prefillBox).toBeVisible({ timeout: 10_000 })
await prefillBox.fill('User locked out of AD weekly')
await prefillBox.press('Enter')
// After the prefill submits we land on /pilot and the first stubbed AI
// turn surfaces the task-lane question text.
await expect(page).toHaveURL(/\/pilot/)
await expect(
page.getByText('Has the user recently changed their password?'),
).toBeVisible({ timeout: 15_000 })
// Answer the first question. UI flow: click "Answer" to open the
// textarea, type, click the inline "Answer" button to mark done.
await page.getByRole('button', { name: /^Answer$/ }).first().click()
await page.getByPlaceholder('Type your answer...').fill('No, password is months old')
await page.getByRole('button', { name: /^Answer$/ }).first().click()
// Submit the partial response. Pre-fix: the response was silently dropped
// here because `currentChatRef.current` still held the mount-time value.
await page.getByRole('button', { name: /Send 1 of 2 Responses/ }).click()
// Bug repro: the assistant message must render. Pre-fix this assertion
// fails because `handleTaskSubmit` early-returns at the
// `currentChatRef.current !== sentForChatId` guard.
await expect(
page.getByText('Got it — based on your answer, here is what to check next.'),
).toBeVisible({ timeout: 15_000 })
// Both chat calls must have actually happened.
expect(chatCallCount).toBe(2)
})
})