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//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) }) })