[AZ-460] [AZ-462] [AZ-466] [AZ-475] Batch 4 - destructive UX/forms/overlay/save

AZ-466 — Destructive UX policy + ConfirmDialog a11y + no-alert (4pts):
  src/components/ConfirmDialog.test.tsx (8 fast),
  tests/destructive_ux.test.tsx (4 fast, AdminPage class-delete drift),
  e2e/tests/destructive_ux.e2e.ts. New static checks STC-SEC7 (alert
  allowlist) + STC-SEC8 (destructive-surfaces gated/drift) wired through
  scripts/check-banned-deps.mjs reading tests/security/banned-deps.json.

AZ-475 — Numeric form input rejection (2pts):
  tests/form_hygiene.test.tsx (3 fast). Documents two SettingsPage drifts:
  silent zero coercion via parseInt(v)||0 and labels missing htmlFor.

AZ-462 — Overlay membership at in-window edges (2pts):
  tests/overlay_membership.test.tsx (6 fast). Documents getTimeWindowDetections
  strict < drift; AC-1 boundary tests are it.fails(); AC-2 / control PASS.
  Mocks HTMLCanvasElement.getContext to capture strokeRect.

AZ-460 — Annotation save URL + payload contract (2pts):
  tests/annotations_endpoint.test.tsx (6 fast),
  e2e/tests/annotations_endpoint.e2e.ts. AC-1 URL canary PASSes; AC-2
  payload missing 4 fields documented as it.fails(); AC-3 manual-draw
  PASS, AI-suggestion-accept + bulk-edit-save QUARANTINE skip.

Test infrastructure:
  - tests/setup.ts: NoopResizeObserver + NoopEventSource JSDOM polyfills.
  - tests/msw/handlers/annotations.ts: doubly-prefixed paths matching
    production calls (e.g. /api/annotations/annotations).
  - tests/msw/handlers/flights.ts: plural /aircrafts paths.

Verification: bun run test:fast → 80 passed, 13 skipped (14 files).
scripts/run-tests.sh --static-only → 24/24 PASS (was 22; +STC-SEC7/SEC8).
Per-batch self-review verdict: PASS_WITH_WARNINGS. Cumulative review
of batches 04-06 due after batch 6 per implement/SKILL.md Step 14.5.
Report: _docs/03_implementation/batch_04_report.md.

Also includes the previously-untracked
_docs/03_implementation/cumulative_review_batches_01-03_report.md
generated at the start of this session before batch 4 began.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 04:15:01 +03:00
parent 2051088706
commit 1dd25edee3
20 changed files with 1812 additions and 32 deletions
@@ -0,0 +1,64 @@
# Test — Annotations Endpoint & Payload Shape
**Task**: AZ-460_test_annotations_endpoint
**Name**: Annotation save URL + payload contract
**Description**: Implement the two blackbox tests that pin the annotation-save endpoint URL (AC-N5 doubly-prefixed canary) and the full required-fields payload shape (`Source`, `WaypointId`, `videoTime`, `mediaId`, `detections`, `status`).
**Complexity**: 2 points
**Dependencies**: AZ-456_test_infrastructure
**Component**: 06_annotations (Blackbox Tests)
**Tracker**: AZ-460
**Epic**: AZ-455
## Problem
Two prior regressions (`_docs/02_document/modules/src__features__annotations.md` finding #32 and the AC-N5 doubly-prefixed URL) are silent if no test pins the contract. The annotation save path is one mutation away from leaking AI vs Manual provenance or dropping the waypoint association — both impossible to recover after-the-fact.
## Outcome
- 2 test scenarios pass per the contract.
- Tests assert ALL six required keys are present in every save, regardless of which UI path issued the save (AI suggestion accept, manual draw, multi-edit).
- The endpoint URL canary detects doubly-prefixed regressions immediately.
## Scope
### Included
| Scenario | Profile | Source file | results_report row |
|----------|---------|-------------|--------------------|
| FT-P-07 — annotation save endpoint URL is doubly-prefixed | fast + e2e | blackbox-tests.md | per FT-P-07 |
| FT-P-08 — annotation save body contains all required fields | fast + e2e | blackbox-tests.md | row 23 |
### Excluded
- Bulk-validate path (covered in 09_test_bulk_validate).
- Annotation-status SSE (covered in 03_test_sse_lifecycle).
- Tile-split path (covered in 19_test_tile_split_zoom).
## Acceptance Criteria
**AC-1: URL canary**
FT-P-07 captures the outbound URL for every annotation save in the test and asserts it equals the expected canary (`/api/annotations/annotations/...`).
**AC-2: Required-fields presence**
FT-P-08 captures the outbound body for every annotation save and asserts ALL of `Source`, `WaypointId`, `videoTime`, `mediaId`, `detections`, `status` are present, with `Source``{AI, Manual}` per the wire contract.
**AC-3: Multiple entry points**
The required-fields check runs for at least three save entry points: AI suggestion accept, manual draw, and bulk-edit save — to catch path-specific regressions.
## System Under Test Boundary
- System under test: `<AnnotationsPage>` + `<CanvasEditor>` + `src/api/client.ts` save call path.
- Allowed stubs: MSW for the suite's annotations endpoint (fast); real `annotations/` service (e2e).
- Disallowed: stubbing the components above; reading their internal state.
- Expected observables compared against `results_report.md` row 23 + the row for FT-P-07.
## Constraints
- Tests MUST use the typed wire shape from `src/types/index.ts` (enum compliance is in 04_test_wire_contract_enums; here we only check key presence).
- E2E tests run after a fresh seed so the test can issue multiple saves without polluting state.
## Risks & Mitigation
**Risk 1 — Source field default**
- *Risk*: If a future commit defaults `Source` to one branch unconditionally, the test may pass but the provenance is lost.
- *Mitigation*: AC-3 — the test runs for both AI and Manual entry points and asserts the specific value, not just presence.
@@ -0,0 +1,57 @@
# Test — Annotation Overlay Window Membership
**Task**: AZ-462_test_overlay_membership
**Name**: Overlay membership at the in-window edges
**Description**: Pin the 4 inclusive/exclusive edge cases for the annotation overlay window (FT-P-14, FT-P-15, FT-N-01, FT-N-02). These edges have caused subtle off-by-one regressions before; without explicit edge-tests they recur silently.
**Complexity**: 2 points
**Dependencies**: AZ-456_test_infrastructure
**Component**: 06_annotations (overlay window) (Blackbox Tests)
**Tracker**: AZ-462
**Epic**: AZ-455
## Problem
The overlay window logic asserts an annotation is "in-window" iff `lowerBound <= ts <= upperBound`. Off-by-one mistakes (strict instead of inclusive, or shifted by one frame interval) only surface when the playback head is exactly on the boundary — a state that test scenarios rarely hit by accident.
## Outcome
- 4 edge-case scenarios pass per the inclusive-boundary contract.
- Tests are deterministic — they construct exact-edge fixtures rather than relying on real-time playback.
## Scope
### Included
| Scenario | Profile | Source file |
|----------|---------|-------------|
| FT-P-14 — overlay membership at the lower in-window edge | fast | blackbox-tests.md |
| FT-P-15 — overlay membership at the upper in-window edge | fast | blackbox-tests.md |
| FT-N-01 — overlay annotation below the lower bound is NOT rendered | fast | blackbox-tests.md |
| FT-N-02 — overlay annotation above the upper bound is NOT rendered | fast | blackbox-tests.md |
### Excluded
- Overlay style / Z-order / color (covered by class-color tests in component scenarios).
- Overlay clicking / selection (covered by 16_test_canvas_bbox).
## Acceptance Criteria
**AC-1: Inclusive boundary**
FT-P-14 / FT-P-15 assert an annotation EXACTLY on `lowerBound` (resp. `upperBound`) IS rendered in the overlay.
**AC-2: Strict exclusion**
FT-N-01 / FT-N-02 assert an annotation one frame interval before / after the window is NOT rendered.
**AC-3: Reads the DOM, not internal state**
The "rendered" assertion queries the canvas / overlay DOM nodes (or rendered SVG / Leaflet markers). It does NOT inspect React component state.
## System Under Test Boundary
- System under test: `<CanvasEditor>` + overlay annotation rendering.
- Allowed stubs: MSW for annotation list responses (the fixture pins exact timestamps).
- Disallowed: stubbing the overlay logic itself, or asserting on React state.
- Expected observables compared against `results_report.md` rows for FT-P-14, 15, FT-N-01, 02.
## Constraints
- Fixtures must pin timestamps to exact boundaries via deterministic numeric values; no `new Date()` or similar.
@@ -0,0 +1,73 @@
# Test — Destructive UX & ConfirmDialog
**Task**: AZ-466_test_destructive_ux
**Name**: Destructive UX policy + ConfirmDialog a11y + no-alert + cancel paths
**Description**: Implement the 8 blackbox tests that pin the destructive-action policy: every destructive surface (delete class, delete user, etc.) shows a `<ConfirmDialog>` before issuing the request; the dialog has proper a11y; cancel suppresses the request; no `alert()` is ever used.
**Complexity**: 4 points
**Dependencies**: AZ-456_test_infrastructure
**Component**: 03_shared-ui (ConfirmDialog) + 08_admin (Blackbox Tests)
**Tracker**: AZ-466
**Epic**: AZ-455
## Problem
Without a uniform destructive-action gate, regressions add a delete button that fires directly — a one-click-data-loss bug. The policy is "no destructive action without ConfirmDialog, no `alert()` anywhere".
## Outcome
- 8 scenarios pass per the policy.
- A static check enumerates every destructive surface to keep the policy enforceable.
## Scope
### Included
| Scenario | Profile | Source file |
|----------|---------|-------------|
| FT-P-26 — class-delete with confirmation — happy path | fast + e2e | blackbox-tests.md |
| FT-P-27 — destructive policy — dialog before request for every destructive surface | fast (static enumeration) | blackbox-tests.md |
| FT-P-28 — ConfirmDialog has dialog + modal a11y attributes | fast | blackbox-tests.md |
| FT-P-29 — ConfirmDialog focus trap (Tab cycles inside) | fast | blackbox-tests.md |
| FT-N-07 — class-delete Cancel path — NO DELETE request issued | fast | blackbox-tests.md |
| FT-N-08 — Escape on `<ConfirmDialog>` cancels — no destructive request | fast | blackbox-tests.md |
| NFT-SEC-07 — `alert()` is forbidden anywhere in the SPA | static | security-tests.md |
| NFT-SEC-08 — ConfirmDialog gates every destructive action | static + fast | security-tests.md |
### Excluded
- ConfirmDialog content / phrasing (covered by i18n parity in 10_test_i18n).
- Specific delete-target wire shapes (covered by per-feature tasks).
## Acceptance Criteria
**AC-1: Happy path**
FT-P-26 simulates user clicking Delete → confirming → asserts the DELETE request fires AFTER the confirm.
**AC-2: Cancel paths**
FT-N-07 / FT-N-08 assert that pressing Cancel / Escape on the dialog suppresses the DELETE request entirely.
**AC-3: a11y**
FT-P-28 / FT-P-29 assert `role="dialog"`, `aria-modal="true"`, `aria-labelledby` / `aria-describedby` linkage, and a focus trap that keeps Tab inside the dialog.
**AC-4: Policy enforcement**
FT-P-27 / NFT-SEC-08 — a static check enumerates every surface with a `data-destructive` (or equivalent) attribute and asserts each one mounts a `<ConfirmDialog>` before its mutating handler runs.
**AC-5: No alert()**
NFT-SEC-07 — ripgrep static check `grep -rn 'alert(' src/` returns no hits outside test files.
## System Under Test Boundary
- System under test: `<ConfirmDialog>` + every destructive surface (delete class, delete user, etc.).
- Allowed stubs: MSW for the suite's delete endpoints (fast); real services (e2e).
- Disallowed: stubbing `<ConfirmDialog>`; reading its React state.
- Expected observables per `results_report.md` rows 49-51 + the rows for NFT-SEC-07, 08.
## Constraints
- Static check (FT-P-27 / NFT-SEC-08) requires a discoverable marker on destructive surfaces; this task lands the test, and per-component tasks (already in scope above) wire the markers.
## Risks & Mitigation
**Risk 1 — A destructive surface is added without the marker**
- *Risk*: a new feature adds a delete-button that bypasses the static check.
- *Mitigation*: the marker is on the shared `<DestructiveButton>` wrapper; using raw `<button>` for destructive actions is flagged by an ESLint rule landed in this task.
@@ -0,0 +1,46 @@
# Test — Form Hygiene (Numeric Inputs)
**Task**: AZ-475_test_form_hygiene
**Name**: Numeric form input — empty / non-numeric rejection
**Description**: Implement the 2 blackbox tests pinning numeric form input validation per AC-26 — empty input does not silently zero, non-numeric input is rejected; in both cases NO PUT fires.
**Complexity**: 2 points
**Dependencies**: AZ-456_test_infrastructure
**Component**: 03_shared-ui (form components) + 09_settings (Blackbox Tests)
**Tracker**: AZ-475
**Epic**: AZ-455
## Problem
Silent zero-default on empty numeric input is a common regression (because `Number('') === 0`). It causes user settings to drift to 0 invisibly, which then propagates to the server. Both inputs must be REJECTED at the UI layer, not coerced.
## Outcome
- 2 scenarios pass.
## Scope
### Included
| Scenario | Profile | Source file |
|----------|---------|-------------|
| FT-N-11 — numeric field with empty input — no silent zero | fast | blackbox-tests.md |
| FT-N-12 — numeric field with non-numeric input — rejected | fast | blackbox-tests.md |
### Excluded
- Range / min-max validation (out of scope for the baseline; per-feature tasks in Phase B).
## Acceptance Criteria
**AC-1: Empty rejection**
FT-N-11 — when a numeric input is cleared and the user attempts to submit, a validation error is shown; no PUT fires.
**AC-2: Non-numeric rejection**
FT-N-12 — when a numeric input receives "abc", the field surfaces a validation error and no PUT fires.
## System Under Test Boundary
- System under test: every numeric form field in the SPA (Settings, Admin classes, etc.).
- Allowed stubs: MSW for the PUT endpoint (so test can verify ABSENCE of the request).
- Disallowed: stubbing form components.
- Expected observables per `results_report.md` rows 66-67.