v1.1 WP-C: browser release coverage

Drives in a real browser the reader workflows v1 proved only through the API
or the component suite, including an export that leaves the browser as a
file. Final run: 91 checks (the 38 existing M11 checks plus 53 new), 0 failed,
0 skipped, on the production build over trusted-LAN HTTPS.

- tools/m11_browser.py: scenarios for Retry and takes, Save Point create /
  restore / Redo, state correction (accepted, and a refused correction with
  its reason), narration length reaching each turn's prompt, failed
  generation (an unserved model blocked up front; a listed model that cannot
  narrate failing in the open) and recovery, and export download from the
  library and from campaign settings, imported into a fresh application.
  Rows are tagged M11 / WP-C and counted separately; --only for development.
  The M11 checks now wait on conditions instead of sleeping.
- tools/m11_webdriver.py: Firefox download preferences, a $HOME-only
  download folder, a download wait that ignores partial, empty, pre-existing
  and still-growing files, centred real clicks, tabs, and condition waits.
- tests/test_v11_c_browser_helpers.py: the download wait, prefs and $HOME
  guard, without a browser.
- frontend: a correction the story refused was presented as "Generation
  failed" with a Retry offer and a typed-input claim. It is now "That
  correction was not applied", not retryable, with the reason kept
  (errors.js, FailureNotice.jsx; 3 regression tests).
- DEVELOPMENT.md: the harness command, download profile and $HOME rule,
  what counts as a finished download, and the no-sleep rule.
- docs: V1.1-PLAN, VERSION v4.4, planning README,
  reports/v1.1/V1.1-WP-C-REPORT.md.

Open for the owner: "Correct" on an Important Facts row is always refused
(K1), and a partly refused correction is not reachable from the reader UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VvegagkhuCZoFPdv4M1egY
This commit is contained in:
JesseMarkowitz
2026-09-15 18:29:45 -04:00
co-authored by Claude Opus 5
parent 0c1ba836ba
commit 59b5ebc2d8
11 changed files with 1711 additions and 157 deletions
+35 -2
View File
@@ -29,8 +29,8 @@ 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.
// This assertion is the contract the defect broke. Every failed turn shows
// it, so every path that shows it owes the reader their text.
await renderWith(
<FailureNotice failure={classifyError('Could not connect to http://127.0.0.1:9/v1')}
partial={null} onRetry={vi.fn()} onDismiss={vi.fn()} />,
@@ -64,6 +64,39 @@ describe('the failure notice states only what is true', () => {
expect(onRetry).toHaveBeenCalled()
})
// v1.1 WP-C, found in the browser: a correction the story refused fell through
// to "Generation failed", offered to try the turn again, and claimed the
// typed words were kept — none of which a State-panel correction involves.
const REFUSAL = "That correction can't be applied — no fact 'f1' to invalidate."
it('classifies a refused correction as a state refusal, not a failed turn', () => {
const refusal = classifyError(REFUSAL)
expect(refusal.kind).toBe(KIND.STATE)
expect(refusal.title).toBe('That correction was not applied')
expect(refusal.retryable).toBe(false)
expect(refusal.keptInput).toBe(false)
expect(refusal.detail).toBe(REFUSAL)
})
it('shows a refused correction with its reason, and no turn to retry', async () => {
await renderWith(
<FailureNotice failure={classifyError(REFUSAL)} partial={null}
onRetry={vi.fn()} onDismiss={vi.fn()} />,
)
expect(screen.getByText('That correction was not applied')).toBeInTheDocument()
expect(screen.getByText(/no fact 'f1' to invalidate/)).toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'Try that turn again' })).toBeNull()
expect(screen.queryByText(/what you typed is still in the box/)).toBeNull()
})
it('still claims the typed words were kept for a failed turn', async () => {
await renderWith(
<FailureNotice failure={classifyError('boom')} partial={null}
onRetry={vi.fn()} onDismiss={vi.fn()} />,
)
expect(screen.getByText(/what you typed is still in the box/)).toBeInTheDocument()
})
it('labels partial prose as not kept rather than showing it as story', async () => {
await renderWith(
<FailureNotice failure={classifyError('boom')} partial="The tavern door swung"