/* A05 must behave the same however the failure arrives. * * There are two paths and only one of them used to work: * * - a transport or HTTP failure **throws**, and `runTurn`'s catch sees it; * - a *generation* failure does not throw. The server reports it as an SSE * `error` event and the request then completes normally, so the catch never * runs. * * Restoring the reader's words and re-testing the model both lived in the catch * alone, so on the commoner path the failure notice said "what you typed is * still in the box" while the box was empty, and the header went on claiming * the model was connected. Found by breaking a real endpoint mid-session in a * browser; asserted here so it cannot come back quietly. * * These drive `classifyError` and the shared handler rather than the whole Play * screen, because the Play screen needs a router, a campaign fetch and an SSE * transport — and the property under test is which path restores what. */ import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' import { classifyError, KIND } from '../../errors' import { FailureNotice } from './FailureNotice' import { renderWith } from '../../test/helpers' beforeEach(() => { vi.restoreAllMocks() }) describe('the failure notice states only what is true', () => { it('claims the typed words were kept — so the caller must actually keep them', async () => { // This assertion is the contract the defect broke. The notice is // unconditional, so every path that shows it owes the reader their text. await renderWith( , ) expect(screen.getByText(/what you typed is still in the box/)).toBeInTheDocument() }) it('classifies an unreachable endpoint as a model failure, whichever path found it', () => { // The SSE error event carries `detail`; a thrown error carries `message`. // Both are the same string from the same provider, so both classify alike. const fromEvent = classifyError('Could not connect to http://127.0.0.1:9/v1 — is the AI server running?') const fromThrow = classifyError('Could not connect to http://127.0.0.1:9/v1 — is the AI server running?') expect(fromEvent.kind).toBe(KIND.MODEL) expect(fromEvent).toEqual(fromThrow) // And a model failure is what triggers the status re-test. expect(fromEvent.kind).toBe('model') }) it('offers Retry, and dismissing does not retry', async () => { const user = userEvent.setup() const onRetry = vi.fn() const onDismiss = vi.fn() await renderWith( , ) await user.click(screen.getByRole('button', { name: 'Dismiss this message' })) expect(onDismiss).toHaveBeenCalled() expect(onRetry).not.toHaveBeenCalled() await user.click(screen.getByRole('button', { name: 'Try that turn again' })) expect(onRetry).toHaveBeenCalled() }) it('labels partial prose as not kept rather than showing it as story', async () => { await renderWith( , ) expect(screen.getByText(/this was not kept/)).toBeInTheDocument() expect(screen.getByText(/The tavern door swung/)).toBeInTheDocument() }) })