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