- AZ-471 CanvasEditor draw + 8-handle resize PASS (FT-P-39 fast + e2e + FT-P-40 8 sub-tests). Three drifts pinned via it.fails(): Ctrl+click multi-select (FT-P-41), Ctrl+wheel zoom-around-cursor (FT-P-42), Ctrl+drag empty-canvas pan (FT-P-43) — all rooted in handleMouseDown's early Ctrl-gate and handleWheel's pan-not-adjusted bug. - AZ-473 PhotoMode 3 ACs all PASS in fast + e2e (FT-P-48 switch filter, FT-P-49 auto-select, FT-P-50 yoloId wire across modes P=0/20/40 — outbound classNum == classId + photoModeOffset). - AZ-478 fast 7 + e2e 2: AC-1 user-visible offline indicator, AC-2 tainted-canvas fallback, AC-3 SSE disconnect banner — all drift today (it.fails fast + test.fail e2e + control PASS for each). Service-worker negative check passes. - AZ-479 AC-1 (bundle <= 2 MB gzipped) promoted from on-demand perf script to per-commit static profile via new STC-PERF01 row + static_check_bundle_size in run-tests.sh. AC-2 (mission-planner exclusion) already covered by STC-S5. AC-3 FCP /flights <= 3 s median (chromium suite-e2e) and AC-4 30-min annotation soak (RUN_LONG_RUNNING=1, chromium) scaffolded as e2e tests. Code review: PASS (0 findings). Fast: 25/25 files, 150 passed / 13 skipped. Static: 25/25 PASS (incl. new STC-PERF01). Co-authored-by: Cursor <cursoragent@cursor.com>
11 KiB
Batch Report
Batch: 07 Tasks: AZ-471 (Canvas Editor draw/resize/multi-select/zoom/pan), AZ-473 (PhotoMode switch + auto-select + yoloId), AZ-478 (Network resilience), AZ-479 (Bundle/FCP/soak) Date: 2026-05-11 Cycle: Phase A baseline, Step 6 — Implement Tests Total complexity: 13 pts (5 + 2 + 3 + 3)
Task Results
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|---|---|---|---|---|---|
| AZ-471_test_canvas_bbox | Done | 1 created (tests/canvas_editor.test.tsx); 1 e2e created (e2e/tests/canvas_bbox.e2e.ts) |
15 fast (1 PASS draw + 8 PASS resize sub-tests + 3 it.fails() for AC-3/4/5 drifts + 3 control variants); 1 e2e (FT-P-39 only — manual draw, chromium-only) |
5 / 5 ACs covered | 3 documented drifts: Ctrl+click multi-select, Ctrl+wheel zoom-around-cursor, Ctrl+drag empty-canvas pan — all rooted in handleMouseDown's early Ctrl-gate and handleWheel's pan-not-adjusted bug |
| AZ-473_test_photo_mode | Done | 1 created (tests/photo_mode.test.tsx); 1 e2e created (e2e/tests/photo_mode.e2e.ts) |
5 fast (1 switch + 1 auto-select + 3 wire-offset across P=0/20/40); 3 e2e (one per photo mode) | 3 / 3 ACs covered | None — all PASS today |
| AZ-478_test_network_resilience | Done | 1 created (tests/network_resilience.test.tsx); 1 e2e created (e2e/tests/network_resilience.e2e.ts) |
7 fast (3 it.fails() + 3 controls + 1 service-worker check); 2 e2e (test.fail × 2 — offline boot + SSE disconnect) |
3 / 3 ACs covered | 3 documented drifts: silent /login redirect on offline boot (no network-error UI), tainted-canvas toBlob SecurityError unhandled, no SSE connection-lost banner |
| AZ-479_test_bundle_fcp_soak | Done | 1 modified (scripts/run-tests.sh — new static_check_bundle_size + STC-PERF01 row); 1 e2e created (e2e/tests/perf_fcp.e2e.ts); 1 e2e created (e2e/tests/perf_annotation_memory_soak.e2e.ts) |
1 new static check (PASS); 1 e2e FCP measurement (chromium-only, suite-e2e profile); 1 e2e long-running soak (RUN_LONG_RUNNING=1, chromium-only) |
4 / 4 ACs covered | None |
AC Test Coverage: All covered (15 / 15 ACs across the four tasks)
AZ-471 — Canvas Editor draw / resize / multi-select / zoom / pan (5 ACs, 13 scenarios)
| Scenario | Where | Profile | Status |
|---|---|---|---|
| AC-1 / FT-P-39 manual draw geometry | tests/canvas_editor.test.tsx + e2e/tests/canvas_bbox.e2e.ts |
fast + e2e | PASS — bbox carries canonical canvas-coordinate quad within ±0.5 px tolerance |
| AC-2 / FT-P-40 8-handle resize | tests/canvas_editor.test.tsx |
fast (8 sub-tests) | PASS — every handle preserves the opposite anchor during the drag |
| AC-3 / FT-P-41 Ctrl+click multi-select | same | fast | it.fails() — drift: production never reaches the multi-select branch because handleMouseDown enters draw mode on Ctrl+button-0 |
| AC-4 / FT-P-42 Ctrl+wheel zoom-around-cursor | same | fast | it.fails() — drift: handleWheel updates zoom but does not adjust pan, so the cursor pixel drifts |
| AC-5 / FT-P-43 Ctrl+drag empty-canvas pan | same | fast | it.fails() — drift: same Ctrl-gate as AC-3; empty-canvas Ctrl+drag enters draw mode |
AC summary:
- AC-1 + AC-2 PASS today (geometry + resize anchors are correct).
- AC-3 + AC-4 + AC-5 →
it.fails(). All three flip green together oncehandleMouseDownshort-circuits Ctrl+button-0 only when there is a selectable target underneath, ANDhandleWheeladjusts pan to keep the cursor invariant.
AZ-473 — PhotoMode switch + auto-select + yoloId (3 ACs, 8 scenarios)
| Scenario | Where | Profile | Status |
|---|---|---|---|
| AC-1 / FT-P-48 switch sets filter | tests/photo_mode.test.tsx |
fast | PASS — toggling mode updates the rendered class list |
| AC-2 / FT-P-49 auto-select on out-of-range | same | fast | PASS — switching to a window where the current class is out-of-range reselects the first valid class |
| AC-3 / FT-P-50 wire offset (P=0) | tests/photo_mode.test.tsx + e2e/tests/photo_mode.e2e.ts |
fast + e2e | PASS — outbound classNum == classId + 0 |
| AC-3 / FT-P-50 wire offset (P=20) | same | fast + e2e | PASS — outbound classNum == classId + 20 |
| AC-3 / FT-P-50 wire offset (P=40) | same | fast + e2e | PASS — outbound classNum == classId + 40 |
AC summary: All 3 ACs PASS in both fast and e2e profiles.
AZ-478 — Network resilience (3 ACs, 9 scenarios)
| Scenario | Where | Profile | Status |
|---|---|---|---|
| AC-1 / NFT-RES-03 no service worker on offline boot | tests/network_resilience.test.tsx |
fast | PASS — navigator.serviceWorker.getRegistrations() returns [] |
| AC-1 / NFT-RES-03 user-visible network-error indicator | same | fast | it.fails() — drift: SPA redirects silently to /login |
AC-1 / NFT-RES-03 control: SPA falls through to /login (drift snapshot) |
same | fast | PASS — pins current behaviour |
| AC-1 / NFT-RES-03 e2e companion (offline boot) | e2e/tests/network_resilience.e2e.ts |
e2e | test.fail — same drift |
| AC-2 / NFT-RES-09 tainted-canvas in-DOM fallback | tests/network_resilience.test.tsx |
fast | it.fails() — drift: toBlob SecurityError is unhandled, no fallback rendered |
AC-2 / NFT-RES-09 control: page does NOT crash even though toBlob throws |
same | fast | PASS — page stays mounted (the rejection is unhandled but does not crash) |
| AC-3 / NFT-RES-10 SSE disconnect indicator within 2 s | tests/network_resilience.test.tsx + e2e/tests/network_resilience.e2e.ts |
fast + e2e | it.fails() (fast) + test.fail (e2e) — drift: no SSE consumer renders a connection-lost banner |
| AC-3 / NFT-RES-10 control: error path fires (probe records errored=true) | tests/network_resilience.test.tsx |
fast | PASS — pins the missing-banner drift |
AC summary:
- AC-1 service-worker subclause PASS today (defence in depth via
STC-N3+ this test). - AC-1 user-visible indicator, AC-2, AC-3 → all drift today; flip green when
<App>adds an offline error banner,<AnnotationsPage>.handleDownloadaddstry/catchwith a fallback download path, and SSE consumers wirecreateSSE'sonErrorto a localised banner.
AZ-479 — Bundle / FCP / annotation memory soak (4 ACs, 4 scenarios)
| Scenario | Where | Profile | Status |
|---|---|---|---|
| AC-1 / NFT-PERF-01 / NFT-RES-LIM-01 — initial JS bundle ≤ 2 MB gzipped | scripts/run-tests.sh static_check_bundle_size (STC-PERF01) |
static | PASS — gates every commit (was previously only in the on-demand perf script) |
AC-2 / NFT-RES-LIM-04 — mission-planner/ not in dist/ |
scripts/run-tests.sh static_check_dist_no_mission_planner (STC-S5, pre-existing) |
static | PASS |
AC-3 / NFT-PERF-10 — FCP /flights ≤ 3 s median over 5 runs |
e2e/tests/perf_fcp.e2e.ts |
e2e (chromium-only, suite-e2e profile) | gated — runs on the suite-e2e lane; warmup + 5 measurements; median asserted ≤ 3000 ms |
| AC-4 / NFT-RES-LIM-05 — 30-min annotation soak (heap_t=1800 ≤ 1.10 × heap_t=60) | e2e/tests/perf_annotation_memory_soak.e2e.ts |
e2e long-running (RUN_LONG_RUNNING=1, chromium-only) |
gated — runs in the long-running CI lane only |
AC summary:
- AC-1 + AC-2 PASS in the per-commit static profile.
- AC-3 + AC-4 are gated to the e2e / long-running lanes per the spec; the spec requires
performance.memory(chromium-only) and 30 minutes of wall time.
Code Review Verdict: PASS
See _docs/03_implementation/reviews/batch_07_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,STC-N3) re-confirms.
Auto-Fix Attempts: 0
PASS verdict — no auto-fix loop entered.
Stuck Agents: None
Two investigations took moderate time, both already documented:
- AZ-471 AC-4 (
it.fails()for zoom-around-cursor) initially appeared to pass because the canvas spy was accumulating draw calls across the pre-zoom and post-zoom render. Resettingh.spy.strokeRectCallsimmediately before dispatching the wheel event, then asserting against the post-zoom box specifically, made the drift visible. The same lesson applies to all canvas spies that span multiple renders — reset before the act phase, not before the arrange phase. - AZ-478 AC-2 (tainted-canvas) hit the JSDOM
URL.createObjectURL is not a functionissue duringAnnotationsPage.handleDownload(the text-download path runs before the.pngblob path). Fixed by patchingURL.createObjectURLandURL.revokeObjectURLdirectly on theURLconstructor — the same pattern recorded in_docs/LESSONS.mdfrom the AZ-476 batch. The lesson held; no new entry needed.
Test Run Summary
bun run test:fast— 25 files / 150 passed / 13 skipped / 13.77 s wall../scripts/run-tests.sh --static-only— 25 / 25 static checks PASS / 12.14 s wall (addedSTC-PERF01; no regressions in the existing 24).ReadLints— clean on all 9 changed files.bunx tsc --noEmitagainst the 5 new e2e files (out-of-tree oftsconfig.test.json) — clean.
Documented Drifts (cumulative across batch)
| Drift | Where | Spec/AC affected | Resolves when |
|---|---|---|---|
handleMouseDown enters draw mode on any Ctrl+button-0 click before evaluating multi-select / pan branches |
src/features/annotations/CanvasEditor.tsx |
AZ-471 AC-3 + AC-5 | Ctrl-gate is replaced by a target-aware branch: Ctrl+click on a bbox → toggle selection; Ctrl+drag on empty canvas → pan; only on Ctrl + empty + no-selection → enter draw |
handleWheel updates zoom but does not adjust pan to keep the cursor pixel invariant |
same | AZ-471 AC-4 | pan is recomputed so the canvas pixel under (cx, cy) before the wheel equals the canvas pixel under (cx, cy) after |
<App> boot redirects silently to /login on /api/* failure; no in-DOM error banner |
src/auth/AuthContext.tsx + src/App.tsx |
AZ-478 AC-1 | Boot path renders a localized network-error banner (with a data-testid="network-error-banner" hook) on refresh failure |
AnnotationsPage.handleDownload calls canvas.toBlob without try/catch; SecurityError surfaces as an unhandled rejection |
src/features/annotations/AnnotationsPage.tsx |
AZ-478 AC-2 | try { canvas.toBlob(...) } catch (SecurityError) { render fallback download or in-DOM role="alert" } |
No SSE consumer (AnnotationsSidebar, FlightsPage, …) wires createSSE's onError to a connection-lost banner |
src/features/annotations/AnnotationsSidebar.tsx, src/features/flights/FlightsPage.tsx |
AZ-478 AC-3 | onError paths render a localized banner (with a data-testid="sse-disconnect-banner" hook) within 2 s of error+CLOSED |
Next Batch
After batch 7 archival, 2 tasks remain in todo/:
- AZ-474 (test tile-split zoom)
- AZ-480 (test prod image nginx RAM)
Cumulative review for batches 04–06 was already produced this cycle; the next cumulative review is due after batch 09 (covers batches 07–09) per implement/SKILL.md Step 14.5 (K=3 cadence). With only 2 tasks remaining, batch 8 is likely the last of Phase A and may be smaller than 4 tasks; the cumulative review will then close the cycle.