Streaming chat checklist

Each row is a failure that keeps showing up in real chat UIs, what this app does about it, and the automated test that proves it. Copy the tests into your own app and point them at your selectors.

Failure modes, the handling in this app, and the test that covers each
FailureWhat this app doesTest
Screen reader announces every tokenStreaming text is not in a live region. Whole sentences go to a separate polite region, at most one announcement per 1.2 s. Code blocks are summarised.e2e/a11y.spec.ts › no token flood
Auto-scroll yanks the reader back to the bottomFollow mode detaches on any upward user gesture and only re-attaches when the user returns to the bottom or presses “Jump to latest”.e2e/scroll.spec.ts
Dropped stream shown as a finished answerA stream with no finish_reason and no [DONE] is marked interrupted. The partial text is kept and Retry is offered.e2e/faults.spec.ts › disconnect
Hung stream spins foreverAn idle watchdog shows “still waiting” after 2 s and gives up after 8 s, cancelling the body.e2e/faults.spec.ts › stall, hang
429 retried immediately or not at allHonours retry-after-ms / retry-after, shows a countdown, retries once.e2e/faults.spec.ts › rate limit
Error frame mid-stream loses the partial replyPartial text stays, the message is marked failed, the error is announced assertively.e2e/faults.spec.ts › server error
One bad frame kills the whole replyMalformed JSON frames are skipped and logged; the reply finishes.src/lib/chat-controller.test.ts
Split multi-byte characters render as �Bytes are decoded with a streaming TextDecoder before SSE parsing.e2e/faults.spec.ts › utf-8
Tool-call arguments parsed before they finishFragments are joined by index; the tool runs only after finish_reason=tool_calls and the JSON parses.e2e/faults.spec.ts › tool call
Stop button hides the UI but the request keeps runningStop aborts the fetch and cancels the body; the route handler passes request.signal upstream.src/lib/chat-controller.test.ts › stop
Re-rendering on every tokenFrames update a draft; one commit per animation frame. The inspector shows frames vs commits.e2e/perf.spec.ts
Open code fence flashes raw backticksAn unclosed fence renders as an open code block until it closes.src/lib/blocks.test.ts