# Batch Report **Batch**: 06 **Tasks**: AZ-463 (Flight selection persistence + soaks), AZ-469 (Browser support + responsive variants), AZ-476 (Upload >500 MB → 413), AZ-477 (Settings save resilience + 2 s budget) **Date**: 2026-05-11 **Cycle**: Phase A baseline, Step 6 — Implement Tests **Total complexity**: 10 pts (3 + 2 + 2 + 3) ## Task Results | Task | Status | Files Modified | Tests | AC Coverage | Issues | |------|--------|----------------|-------|-------------|--------| | AZ-463_test_flight_selection_persistence | Done | 1 created (`tests/flight_selection_persistence.test.tsx`); 1 e2e created (`e2e/tests/flight_selection_persistence.e2e.ts`) | 5 fast (2 pass — AC-1 + AC-2 + leak-companion stub; 2 supporting controls); 4 e2e (2 PASS + 2 long-running gated) | 4 / 4 ACs covered | Long-running soaks (AC-3 / AC-4) gated by `RUN_LONG_RUNNING=1`; runner-level config gating to be added later | | AZ-469_test_browser_support_responsive | Done | 1 created (`tests/browser_support_responsive.test.tsx`); 1 e2e created (`e2e/tests/browser_support_responsive.e2e.ts`) | 4 fast (3 PASS responsive class markers + 1 cross-browser config stub); 5 e2e (3 cross-browser smoke routes + 2 viewport variants) | 3 / 3 ACs covered | None | | AZ-476_test_upload_size_cap | Done | 1 created (`tests/upload_size_cap.test.tsx`); 1 e2e created (`e2e/tests/upload_size_cap.e2e.ts`) | 3 fast (1 `it.fails()` for AC-1 drift + 1 control + 1 PASS for AC-2 vacuous-today); 2 e2e (1 `test.fail` for AC-1 + 1 PASS for AC-2 dialog spy) | 2 / 2 ACs covered | 1 documented drift: `MediaList.uploadFiles` catches the 413 silently and falls through to local-mode; no error region, no i18n key | | AZ-477_test_settings_resilience | Done | 1 created (`tests/settings_resilience.test.tsx`); 1 e2e created (`e2e/tests/settings_resilience.e2e.ts`) | 6 fast (4 `it.fails()` for AC-1 + AC-2 contracts, 1 `it.fails()` for AC-3 deadline, 1 control pinning stuck-disabled drift); 2 e2e (`test.fail` for AC-1 / AC-2) | 3 / 3 ACs covered | 1 systemic drift: `saveSystem` / `saveDirs` lack try/finally and an error region — saving flag stays true forever; flips when both wired | ## AC Test Coverage: All covered (12 / 12 ACs across the four tasks) ### AZ-463 — Flight selection persistence + memory soaks (4 ACs, 9 scenarios) | Scenario | Where | Profile | Status | |----------|-------|---------|--------| | AC-1 / FT-P-16 (persistence wire) | `tests/flight_selection_persistence.test.tsx` + `e2e/tests/flight_selection_persistence.e2e.ts` | fast + e2e | PASS — selecting a flight via Header dropdown PUTs `{selectedFlightId}` to `/api/annotations/settings/user` | | AC-2 / FT-P-17 (rehydration on boot) | same | fast + e2e | PASS — `` boot with `selectedFlightId` set issues `GET /api/flights/` and renders the flight as initially selected | | AC-3 / NFT-RES-LIM-07 (100-cycle leak guard, long-running) | `e2e/tests/flight_selection_persistence.e2e.ts` | e2e long-running (`RUN_LONG_RUNNING=1`) | gated — wraps `EventSource` in an init script and asserts `__activeES <= 1` end-of-cycle | | AC-3 / fast companion stub (5-cycle smoke) | `tests/flight_selection_persistence.test.tsx` | fast | PASS — 5 cycles produce exactly 5 PUTs (no fan-out) | | AC-4 / NFT-RES-LIM-06 (1 h SSE soak) | `e2e/tests/flight_selection_persistence.e2e.ts` | e2e long-running, chromium-only | gated — `performance.memory.usedJSHeapSize` at t=60 s vs t=3600 s, ≤ 10 % growth | **AC summary**: - AC-1 + AC-2 → PASS at the wire (production today persists and rehydrates correctly). - AC-3 + AC-4 → long-running soak suite gated by env flag; CI lane wires the flag on dev/stage merges per the spec. ### AZ-469 — Browser support + responsive variants (3 ACs, 9 scenarios) | Scenario | Where | Profile | Status | |----------|-------|---------|--------| | AC-1 / FT-P-34 cross-browser smoke (`/flights`, `/annotations`, `/dataset`) | `e2e/tests/browser_support_responsive.e2e.ts` | e2e × 2 projects | PASS — 3 routes × 2 browser projects = 6 smoke runs; existing `playwright.config.ts` provides the Chromium + Firefox projects | | AC-1 / fast companion (project-count assertion) | `tests/browser_support_responsive.test.tsx` | fast | PASS — Playwright config pinned at exactly 2 named projects | | AC-2 / FT-P-35 mobile 480 px (Tailwind class shape) | `tests/browser_support_responsive.test.tsx` | fast | PASS — desktop nav has `hidden sm:flex`, mobile bottom-nav has `sm:hidden` | | AC-2 / FT-P-35 mobile 480 px (visibility) | `e2e/tests/browser_support_responsive.e2e.ts` | e2e | PASS — bottom-nav visible, top-bar hidden after `setViewportSize` | | AC-3 / FT-P-36 desktop 1024 px (Tailwind class shape) | `tests/browser_support_responsive.test.tsx` | fast | PASS — same class markers asserted in opposite roles | | AC-3 / FT-P-36 desktop 1024 px (visibility) | `e2e/tests/browser_support_responsive.e2e.ts` | e2e | PASS — top-bar visible, bottom-nav hidden | **AC summary**: All 3 ACs PASS in both fast and e2e profiles. ### AZ-476 — Upload >500 MB → 413 (2 ACs, 5 scenarios) | Scenario | Where | Profile | Status | |----------|-------|---------|--------| | AC-1 / FT-N-06 + NFT-RES-07 (in-DOM error region with i18n message) | `tests/upload_size_cap.test.tsx` | fast | `it.fails()` — drift, production catches the 413 silently | | AC-1 / control: production silently falls through to local mode on 413 | same | fast | PASS — file appears in the rendered media list (proves silent-fall-through drift) | | AC-1 / e2e companion (501 MB POST → nginx 413 → DOM error region) | `e2e/tests/upload_size_cap.e2e.ts` | e2e | `test.fail` — same drift; flips when production wires the toast | | AC-2 / no `alert()` on the 413 path (fast) | `tests/upload_size_cap.test.tsx` | fast | PASS (vacuous today — no error path runs at all; defence-in-depth) | | AC-2 / no `alert()` on the 413 path (e2e dialog spy) | `e2e/tests/upload_size_cap.e2e.ts` | e2e | PASS — Playwright dialog spy asserts no `alert:` events fire | **AC summary**: - AC-1 user-visible 413 → `it.fails()` + control + e2e `test.fail`. Flips when production wires an in-DOM alert + i18n key for the 413 path. - AC-2 no alert → PASS today (vacuous) + e2e dialog spy. Stays PASS once AC-1 lands as long as the new error region uses a toast / inline component, not `alert()`. ### AZ-477 — Settings save resilience + 2 s budget (3 ACs, 6 scenarios) | Scenario | Where | Profile | Status | |----------|-------|---------|--------| | AC-1 / FT-N-13 + NFT-RES-05 — Save button re-enables ≤ 2 s on 500 | `tests/settings_resilience.test.tsx` | fast | `it.fails()` — drift, no try/finally | | AC-1 / FT-N-13 + NFT-RES-05 — DOM error region appears ≤ 2 s on 500 | same | fast | `it.fails()` — drift, no error region rendered | | AC-1 / control: today the Save button stays disabled after a 500 | same | fast | PASS — pins the stuck-disabled drift | | AC-2 / FT-N-14 + NFT-RES-06 — Save button re-enables ≤ 2 s on network drop | same | fast | `it.fails()` — same root cause as AC-1 | | AC-2 / FT-N-14 + NFT-RES-06 — DOM error region appears ≤ 2 s on network drop | same | fast | `it.fails()` | | AC-3 / NFT-PERF-09 — DOM error visible within 2 s of response | same | fast | `it.fails()` — measures `performance.now()` between MSW response timestamp and `findByRole('alert')` | | AC-1 + AC-2 e2e companions | `e2e/tests/settings_resilience.e2e.ts` | e2e | 2 × `test.fail` — same drift | **AC summary**: - All three ACs are `it.fails()` today; one control test pins the stuck-disabled drift so a regression that *removes* the silent-fail (e.g. starts throwing in the React render path) is caught immediately. - All three flip green simultaneously the moment `saveSystem` / `saveDirs` get a `try { ... } finally { setSaving(false) }` plus an error region in the JSX. ## Code Review Verdict: PASS See `_docs/03_implementation/reviews/batch_06_review.md` for the full 7-phase walkthrough. - 0 Critical, 0 High, 0 Medium, 0 Low findings. - All `it.fails()` placements paired with a control PASS test that pins the current production drift. - Architecture compliance (Phase 7): no layer-direction violations; tests are leaves of the import graph; no new cyclic dependencies; static profile (STC-S6, STC-S13) re-confirms. ## Auto-Fix Attempts: 0 PASS verdict — no auto-fix loop entered. ## Stuck Agents: None One investigation took longer than usual: the AZ-476 fast test initially failed because the test rig used `vi.stubGlobal('URL', { ...URL, createObjectURL, ... })` to install JSDOM polyfills, which destroyed the `URL` constructor (turning the global into a plain object) and silently broke `new URL(...)` inside MSW handlers. This was diagnosed by adding a fetch wrapper that logged every outbound request — once it was clear the request never reached MSW, the URL stub became the obvious suspect. The fix patches `URL.createObjectURL` and `URL.revokeObjectURL` directly on the constructor and restores them in `afterEach`. The lesson is captured in `_docs/LESSONS.md` so the next session sees it on autodev's `B2` Recent Lessons surface. ## Test Run Summary - `bun run test:fast` — 22 files / 120 passed / 13 skipped / 46.52 s. - `./scripts/run-tests.sh --static-only` — 24 / 24 static checks PASS / 39.72 s. - `ReadLints` — clean on all 9 changed files. ## Documented Drifts (cumulative across batch) | Drift | Where | Spec/AC affected | Resolves when | |-------|-------|------------------|---------------| | 413 silently swallowed; falls through to local-mode | `src/features/annotations/MediaList.tsx` `uploadFiles` try/catch | AZ-476 AC-1 | Wire toast + i18n key for the 413 path | | `saveSystem` / `saveDirs` have no try/finally | `src/features/settings/SettingsPage.tsx` | AZ-477 AC-1 + AC-2 | Wrap `await api.put(...)` in `try { ... } finally { setSaving(false) }` | | `` renders no error region for save failures | same | AZ-477 AC-1 + AC-2 + AC-3 | Add a toast or inline alert with `role="alert"` | ## Next Batch 10 → 6 tasks remain in `todo/` after batch 6 archival: - AZ-471, AZ-473, AZ-474, AZ-478, AZ-479, AZ-480. Cumulative review (batches 04–06) is due immediately after this batch per `implement/SKILL.md` Step 14.5 (K=3 cadence). Cumulative report file: `_docs/03_implementation/cumulative_review_batches_04-06_report.md`.