Steps 12-15 closure for cycle 4 (AZ-512 admin class inline edit):
- Step 12 (Test-Spec Sync): traceability O9 -> Covered; new FT-P-62
+ FT-N-18 in blackbox-tests.md.
- Step 13 (Update Docs): AdminPage module doc gains the inline-edit
state slots, four new handlers, PATCH integrations row, expanded
i18n key list, tests section. architecture.md row 272 now lists
PATCH /api/admin/classes/{id} with AZ-513 deploy-gate caveat.
- Step 14 (Security Audit): cycle-4 delta report records one new
LOW finding (F-SAST-CY4-1 lost-update / mid-air-collision on
PATCH, by design per spec); verdict carries PASS_WITH_WARNINGS;
bun audit re-run clean.
- Step 15 (Performance Test): NFT-PERF-01 bundle = 291 332 B
(+757 B / +0.26% vs cycle 3; ~13.89% of 2 MB budget); PASS.
Tests 243 passed / 13 skipped / 0 failed (+12 AZ-512 cases).
Co-authored-by: Cursor <cursoragent@cursor.com>
60 KiB
Blackbox Tests
Every test is observed at the SPA's public surface — DOM, ARIA, outbound network, EventSource state, browser storage, console — never via src/ imports beyond the typed wire-contract enums (P9). Each test trails an Expected result source: results_report.md row N line; the comparison method, tolerance, and reference file for the assertion come from that row. Profile (fast / e2e / static) decides which runner picks the test up — see environment.md.
Positive Scenarios
FT-P-01: Bootstrap refresh sends credentials:'include'
Summary: On <AuthContext> init, the bootstrap refresh call includes the HttpOnly refresh cookie.
Traces to: AC-01
Category: Auth — bootstrap
Profile: fast
Preconditions:
- Refresh cookie present in the browser jar (test stubs it on the test domain).
- No bearer in memory.
Input data: app mount in a fresh browser session — results_report.md row 02.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Mount <App> (test renderer or e2e navigation to /) |
Outbound fetch to /api/admin/auth/refresh is observed |
| 2 | Inspect the RequestInit of that fetch |
credentials === 'include'; cookie sent by the test runtime |
Expected outcome: see row 02 (exact: credentials: 'include').
Max execution time: 5s.
Expected result source: results_report.md row 02.
FT-P-02: 401-retry sequence — refresh and retry the original request
Summary: A 401 on an authenticated request triggers POST /api/admin/auth/refresh (cookie-bound) then a retry of the original request with the new bearer.
Traces to: AC-01, AC-23
Profile: fast (with MSW) and e2e
Preconditions:
- Authenticated session active; bearer about to expire.
Input data: any authenticated GET /api/admin/* issued via apiClient — results_report.md row 03.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Trigger an authenticated call whose first response is 401 (stub) | The SPA issues POST /api/admin/auth/refresh with credentials:'include' |
| 2 | Refresh stub returns 200 with new bearer | The SPA retries the original request with the new bearer |
| 3 | Inspect the final response observed by the calling code | 200 (success) |
Expected outcome: sequence exactly per row 03; exactly one refresh call per refresh cycle (row 12).
Max execution time: 5s.
Expected result source: results_report.md rows 03, 12.
FT-P-03: Refresh transparency — <ProtectedRoute> does not unmount
Summary: Auth refresh occurring mid-session does not unmount the routed view. Traces to: AC-23 Profile: fast
Preconditions:
- User on
/flights; bearer about to expire.
Input data: in-flight refresh while <FlightsPage> is mounted — results_report.md row 11.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Mount /flights; record render counter on <FlightsPage> |
initial render = 1 |
| 2 | Force a 401 → refresh → retry cycle | refresh handled |
| 3 | Inspect render counter post-refresh | delta ≤ 1 re-render |
Expected outcome: row 11 — <ProtectedRoute> children stay mounted; ≤1 re-render delta.
Max execution time: 5s.
Expected result source: results_report.md row 11.
FT-P-04: AnnotationStatus enum on the wire
Summary: Outbound annotation save body's status is a number from the spec set {0,10,20,30,40}.
Traces to: AC-04
Profile: fast (static + payload assertion)
Preconditions: _docs/00_problem/input_data/enum_spec_snapshot.json is up-to-date (Phase 3 gate).
Input data: POST /api/annotations/annotations body captured at save — results_report.md row 18; also static check of src/types/index.ts per row 14.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Static read AnnotationStatus from src/types/index.ts |
{None:0, Created:10, Edited:20, Validated:30, Deleted:40} |
| 2 | Trigger an annotation save in <AnnotationsPage> (test mount) |
POST /api/annotations/annotations issued |
| 3 | Inspect body | body.status ∈ {0,10,20,30,40} |
Expected outcome: rows 14 (enum map) and 18 (wire payload).
Max execution time: 3s.
Expected result source: results_report.md rows 14, 18.
FT-P-05: MediaStatus / Affiliation / CombatReadiness enums match the spec
Summary: Each enum in src/types/index.ts matches the spec member set and numeric values pinned in enum_spec_snapshot.json.
Traces to: AC-04
Profile: static
Preconditions: snapshot present (Phase 3 gate).
Input data: static read of src/types/index.ts against the snapshot — results_report.md rows 15, 16, 17.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Parse src/types/index.ts for enum MediaStatus, Affiliation, CombatReadiness |
parsed without error |
| 2 | Compare member sets to snapshot | sets equal per row |
| 3 | Compare numeric values member-by-member | equal per row |
Expected outcome: rows 15, 16, 17 (set_contains + exact).
Max execution time: 1s.
Expected result source: results_report.md rows 15, 16, 17.
FT-P-06: Detection wire payload — affiliation + combatReadiness in spec value sets
Summary: Every Detection element of an outbound annotation save has valid enum members.
Traces to: AC-04
Profile: fast
Preconditions: snapshot present; an annotation with N detections about to be saved.
Input data: POST /api/annotations/annotations body — results_report.md row 19.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Save an annotation with N≥2 detections, mixed affiliations and readiness |
request observed |
| 2 | Inspect every detections[i] |
affiliation and combatReadiness are in spec value sets |
Expected outcome: row 19.
Max execution time: 3s.
Expected result source: results_report.md row 19.
FT-P-07: Annotation save endpoint URL is doubly-prefixed
Summary: Save POSTs to /api/annotations/annotations, not the single-prefix path.
Traces to: AC-05
Profile: fast and e2e
Preconditions: user is editing an annotation in <AnnotationsPage>.
Input data: click Save — results_report.md row 22.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Trigger Save | exactly one POST observed |
| 2 | Inspect URL | matches ^/api/annotations/annotations$ |
Expected outcome: row 22.
Max execution time: 3s.
Expected result source: results_report.md row 22.
FT-P-08: Annotation save body contains all required fields
Summary: Save body has {Source, WaypointId, videoTime, mediaId, detections, status} and NOT the legacy time key.
Traces to: AC-05
Profile: fast
Preconditions: an annotation with all spec-required fields present in the editor state.
Input data: click Save — results_report.md row 23.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Trigger Save with Source=AI, a WaypointId, a videoTime |
POST observed |
| 2 | Inspect body keys | {Source, WaypointId, videoTime, mediaId, detections, status} ⊆ keys; time absent |
Expected outcome: row 23.
Max execution time: 3s.
Expected result source: results_report.md row 23.
FT-P-09: Annotation-status SSE opens on <AnnotationsPage> mount
Traces to: AC-09 Profile: fast (EventSource test double) and e2e
Input data: mount /annotations — results_report.md row 24.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Mount /annotations |
exactly one EventSource constructed |
| 2 | Inspect URL | matches `^/api/annotations/annotations/events(? |
Expected outcome: row 24.
Max execution time: 3s.
Expected result source: results_report.md row 24.
FT-P-10: Annotation-status SSE closes on unmount
Traces to: AC-09 Profile: fast
Preconditions: continuation of FT-P-09.
Input data: unmount /annotations — results_report.md row 25.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Unmount the route | EventSource.readyState → CLOSED (2) within 1 s |
Expected outcome: row 25 (CLOSED within ≤ 1 000 ms).
Max execution time: 3s.
Expected result source: results_report.md row 25.
FT-P-11: Sync image detect endpoint
Traces to: AC-25 (sync path) Profile: fast and e2e
Input data: click Detect on a MediaType.Image — results_report.md row 26.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Open <AnnotationsSidebar> on an image; click Detect |
exactly one POST observed |
| 2 | Inspect URL | matches ^/api/detect/[0-9]+$ |
Expected outcome: row 26.
Max execution time: 30s (server-side detect time included).
Expected result source: results_report.md row 26.
FT-P-12: Async video detect endpoint + SSE (target — Phase B)
Traces to: AC-25 (async path)
Profile: fast (mocked) — quarantined until F7 lands
Status: target — UI does not implement async video detect today (04_verification_log.md F7). Test is written so it activates the day the feature ships.
Input data: click Detect on a MediaType.Video (behind feature flag) — results_report.md row 27.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Trigger detect on a video | one POST ^/api/detect/video/[0-9]+$ |
| 2 | Inspect response | JSON {jobId: <int>} |
| 3 | Wait | EventSource opens to `^/api/detect/stream/[0-9]+(? |
Expected outcome: row 27 (3 assertions).
Max execution time: 10s.
Expected result source: results_report.md row 27.
FT-P-13: Long-video detect carries X-Refresh-Token header
Traces to: AC-25
Profile: fast — quarantined until F7 lands (and the header is added per Step 4)
Status: target.
Input data: long-video async detect — results_report.md row 28.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Trigger long-video detect | request headers observed |
| 2 | Inspect headers | X-Refresh-Token present and non-empty |
Expected outcome: row 28.
Max execution time: 5s.
Expected result source: results_report.md row 28.
FT-P-14: Overlay membership at the lower in-window edge
Summary: An annotation at videoTime = T - 30 ms (inside [-50, +150] window around T) renders.
Traces to: AC-28
Profile: fast (component test on <CanvasEditor>)
Input data: currentTime = T, annotation at T - 30 ms — results_report.md row 29.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Render <CanvasEditor> with the annotation and set currentTime = T |
overlay rect rendered |
Expected outcome: row 29 (range check).
Max execution time: 1s.
Expected result source: results_report.md row 29.
FT-P-15: Overlay membership at the upper in-window edge
Summary: An annotation at videoTime = T + 120 ms renders.
Traces to: AC-28
Profile: fast
Input data: currentTime = T, annotation at T + 120 ms — derived from results_report.md row 29.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Render <CanvasEditor> with the annotation and currentTime = T |
overlay rendered |
Expected outcome: present.
Max execution time: 1s.
Expected result source: results_report.md row 29.
FT-P-16: Flight selection persists via PUT /api/annotations/settings/user
Traces to: AC-06 Profile: fast
Input data: FlightContext.selectFlight(<id>) — results_report.md row 32.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Trigger selectFlight for a known flight id |
exactly one PUT observed |
| 2 | Inspect URL | matches ^/api/annotations/settings/user$ |
| 3 | Inspect body | {selectedFlightId: <id>} (subset) |
| 4 | Code-search the test domain | NO call to /api/flights/select |
Expected outcome: row 32.
Max execution time: 3s.
Expected result source: results_report.md row 32.
FT-P-17: Selected-flight rehydration on boot
Traces to: AC-06
Profile: e2e (and fast with stubbed UserSettings)
Preconditions: UserSettings.selectedFlightId known in seed.
Input data: full reload — results_report.md row 33.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Hard reload as the seeded user | app boots |
| 2 | Inspect FlightContext state via DOM (selected flight indicator) |
matches UserSettings.selectedFlightId |
Expected outcome: row 33.
Max execution time: 10s.
Expected result source: results_report.md row 33.
FT-P-18: Live-GPS SSE opens within 5 s of flight select
Traces to: AC-08
Profile: e2e (uses flights/ live-gps simulator)
Input data: select a flight — results_report.md row 34.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Select a flight in <Header> |
EventSource opens |
| 2 | Inspect URL | matches `^/api/flights/[0-9]+/live-gps(? |
| 3 | Wait | readyState === OPEN (1) within ≤ 5 000 ms |
Expected outcome: row 34.
Max execution time: 10s.
Expected result source: results_report.md row 34.
FT-P-19: Live-GPS SSE closes within 1 s of deselect
Traces to: AC-08 Profile: e2e
Input data: deselect — results_report.md row 35.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Deselect the flight | EventSource state transitions |
| 2 | Inspect | all ^/api/flights/[0-9]+/live-gps sources CLOSED (2) within ≤ 1 000 ms |
Expected outcome: row 35.
Max execution time: 5s.
Expected result source: results_report.md row 35.
FT-P-20: Bulk-validate request URL and body
Traces to: AC-07 Profile: fast and e2e
Input data: select N items, click Validate — results_report.md row 36.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Select N items on <DatasetPage> |
selection state observable |
| 2 | Click Validate | one POST observed |
| 3 | Inspect URL + body | /api/annotations/dataset/bulk-status; body {ids: <length-N>, targetStatus: 30} |
Expected outcome: row 36 (targetStatus: 30 after AC-04 fix).
Max execution time: 5s.
Expected result source: results_report.md row 36.
FT-P-21: Bulk-validate UI reflects new status within 2 s
Traces to: AC-07 Profile: fast
Preconditions: continuation of FT-P-20.
Input data: server returns 200 — results_report.md row 37.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Stub a 200 response | UI updates |
| 2 | Inspect each selected row within ≤ 2 000 ms | status badge reads Validated |
Expected outcome: row 37.
Max execution time: 5s.
Expected result source: results_report.md row 37.
FT-P-22: i18n key parity en ↔ ua
Traces to: AC-12 Profile: static
Input data: src/i18n/en.json, src/i18n/ua.json — results_report.md row 45.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Read both files | parsed |
| 2 | Compute deep, sorted key sets | set_equals |
Expected outcome: row 45.
Max execution time: 1s.
Expected result source: results_report.md row 45.
FT-P-23: No raw user-visible strings outside t(...)
Traces to: AC-12 Profile: static (lint rule)
Input data: lint over src/**/*.tsx — results_report.md row 46.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Run the lint rule | reports 0 findings |
| 2 | Allow-list applies to proper-noun acronyms only | allow-list respected |
Expected outcome: row 46.
Max execution time: 30s.
Expected result source: results_report.md row 46.
FT-P-24: i18n detector path used at first boot
Traces to: AC-13
Profile: fast — quarantined until the detector is added in Step 4
Input data: first boot in a clean profile — results_report.md row 47.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Mount the app with a profile providing Accept-Language: uk |
i18next initialises |
| 2 | Inspect i18next.language |
resolves from detector (not hardcoded 'en') |
| 3 | Static read src/i18n/i18n.ts |
no lng: 'en' hardcoded init present |
Expected outcome: row 47.
Max execution time: 3s.
Expected result source: results_report.md row 47.
FT-P-25: i18n persistence across reload
Traces to: AC-13
Profile: e2e — quarantined until the detector + persistence land
Input data: toggle language, reload — results_report.md row 48.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Click language toggle in <Header> to uk |
UI re-renders in Ukrainian |
| 2 | Hard reload | app boots |
| 3 | Inspect i18next.language |
equals uk |
Expected outcome: row 48.
Max execution time: 10s.
Expected result source: results_report.md row 48.
FT-P-26: Class-delete with confirmation — happy path
Traces to: AC-14, AC-30 Profile: fast
Input data: click Delete on a class entry in <AdminPage> and Confirm — results_report.md row 49.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Click Delete | <ConfirmDialog> mounts |
| 2 | Click Confirm | exactly one DELETE observed |
| 3 | Inspect URL | matches ^/api/admin/classes/[0-9]+$ |
Expected outcome: row 49 (confirm path).
Max execution time: 5s.
Expected result source: results_report.md row 49.
FT-P-27: Destructive policy — dialog before request for every destructive surface
Traces to: AC-14 Profile: fast
Input data: each destructive surface listed in _docs/ui_design/ (class delete, user deactivate, dataset bulk-overwrite, irreversible bulk) — results_report.md row 51.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | For each surface, trigger the destructive action | <ConfirmDialog> opens BEFORE any HTTP fires |
| 2 | Inspect request log up to dialog open | empty for the destructive route |
Expected outcome: row 51.
Max execution time: 10s.
Expected result source: results_report.md row 51.
FT-P-28: ConfirmDialog has dialog + modal a11y attributes
Traces to: AC-15 Profile: fast
Input data: render <ConfirmDialog open> — results_report.md row 52.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Open the dialog | root element rendered |
| 2 | Inspect root attributes | role="dialog" and aria-modal="true" |
Expected outcome: row 52.
Max execution time: 1s.
Expected result source: results_report.md row 52.
FT-P-29: ConfirmDialog focus trap (Tab cycles inside)
Traces to: AC-15 Profile: fast
Input data: open dialog and Tab through — results_report.md row 53.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Open dialog; focus on first focusable element | initial focus inside dialog |
| 2 | Tab repeatedly | focus stays inside the dialog subtree; first ↔ last cycles |
Expected outcome: row 53.
Max execution time: 3s.
Expected result source: results_report.md row 53.
FT-P-30: Header flight dropdown closed-state a11y
Traces to: AC-16 Profile: fast
Input data: render <Header> with dropdown closed — results_report.md row 55.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Mount Header | dropdown trigger rendered |
| 2 | Inspect attributes | role="combobox", aria-expanded="false", aria-haspopup="listbox" |
Expected outcome: row 55.
Max execution time: 1s.
Expected result source: results_report.md row 55.
FT-P-31: Header flight dropdown open-state a11y
Traces to: AC-16 Profile: fast
Input data: open the dropdown — results_report.md row 56.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Click trigger | dropdown opens |
| 2 | Inspect | aria-expanded="true"; outside-click handler attached (was NOT attached while closed — observable via DOM event listener count or via behavior under a synthetic outside-click while closed: no-op) |
Expected outcome: row 56.
Max execution time: 3s.
Expected result source: results_report.md row 56.
FT-P-32: ProtectedRoute spinner a11y
Traces to: AC-17 Profile: fast
Input data: render <ProtectedRoute> in loading state — results_report.md row 58.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Mount in loading state | spinner rendered |
| 2 | Inspect | role="status" + non-empty accessible label |
Expected outcome: row 58.
Max execution time: 1s.
Expected result source: results_report.md row 58.
FT-P-33: ProtectedRoute timeout fallback after 10 s
Traces to: AC-17 Profile: fast (with fake timers)
Input data: loading exceeds the timeout — results_report.md row 59.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Mount loading; advance fake time to 10 s | timer fires |
| 2 | Inspect DOM | fallback (retry CTA or error message) present; spinner unmounted |
Expected outcome: row 59.
Max execution time: 3s.
Expected result source: results_report.md row 59.
FT-P-34: Browser-support smoke (Chromium + Firefox)
Traces to: AC-18 Profile: e2e (manual smoke per AC — no enforcement today)
Input data: render /flights, /annotations, /dataset on each supported browser — results_report.md row 60.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Navigate to each route | page renders |
| 2 | Inspect console + DOM landmarks | no console errors; main landmark roles present |
Expected outcome: row 60.
Max execution time: 30s per browser.
Expected result source: results_report.md row 60.
FT-P-35: Mobile bottom-nav variant at 480 px
Traces to: AC-19 Profile: e2e
Input data: render at viewport 480×800 — results_report.md row 61.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Resize browser to 480×800 | layout reflows |
| 2 | Inspect DOM | bottom-nav variant present; top-bar variant absent |
Expected outcome: row 61.
Max execution time: 5s.
Expected result source: results_report.md row 61.
FT-P-36: Desktop top-bar variant at 1024 px
Traces to: AC-19 Profile: e2e
Input data: render at viewport 1024×768 — results_report.md row 62.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Resize to 1024×768 | reflow |
| 2 | Inspect DOM | top-bar present; bottom-nav absent |
Expected outcome: row 62.
Max execution time: 5s.
Expected result source: results_report.md row 62.
FT-P-37: Panel-width persistence — debounced PUT on resize end
Traces to: AC-21
Profile: fast — quarantined until useResizablePanel writes back (Step 4)
Input data: drag divider — results_report.md row 64.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Simulate drag-end on a useResizablePanel divider |
PUT observed within 1 s (debounce-aware) |
| 2 | Inspect URL + body | matches /api/annotations/settings/user; body contains panelWidths key |
Expected outcome: row 64.
Max execution time: 5s.
Expected result source: results_report.md row 64.
FT-P-38: Panel-width rehydration on reload
Traces to: AC-21
Profile: e2e — quarantined until Step 4
Input data: reload after resize — results_report.md row 65.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Resize panels; capture widths W | widths observable |
| 2 | Reload | app boots |
| 3 | Re-measure | widths equal W within ± 1 px |
Expected outcome: row 65.
Max execution time: 15s.
Expected result source: results_report.md row 65.
FT-P-39: Manual bounding-box draw on <CanvasEditor>
Traces to: AC-35 Profile: fast (synthetic pointer events)
Input data: mousedown(x1,y1) → mousemove(x2,y2) → mouseup with selectedClassNum = C, photoMode = P — results_report.md row 73.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Fire pointer sequence on the canvas | one new local detection appended |
| 2 | Inspect detection | classNum == C + P; x,y,w,h normalised within ± 1 px of the drawn rect |
Expected outcome: row 73.
Max execution time: 1s.
Expected result source: results_report.md row 73.
FT-P-40: 8-handle bbox resize
Traces to: AC-36 (a) Profile: fast
Input data: drag each of the 8 handles in turn — results_report.md row 74.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | For h ∈ {NW,N,NE,W,E,SW,S,SE}: mousedown on h, drag (dx,dy), mouseup | bbox edges adjacent to h move; opposite edges unchanged |
| 2 | Force the drag past zero size | resulting bbox has w>0 and h>0 (clamped) |
Expected outcome: row 74.
Max execution time: 5s.
Expected result source: results_report.md row 74.
FT-P-41: Ctrl+click multi-select on canvas
Traces to: AC-36 (b) Profile: fast
Input data: Ctrl+click an unselected bbox; second Ctrl+click on the same — results_report.md row 75.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Have one bbox already selected | initial selection = {A} |
| 2 | Ctrl+click bbox B | selection set = {A, B}; prior selection preserved |
| 3 | Ctrl+click bbox B again | selection set = {A} (toggle off) |
Expected outcome: row 75.
Max execution time: 1s.
Expected result source: results_report.md row 75.
FT-P-42: Ctrl+wheel zoom-around-cursor
Traces to: AC-36 (c) Profile: fast
Input data: Ctrl+wheel at (cx, cy) — results_report.md row 76.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Record the world coordinate at (cx, cy) before zoom | W₀ |
| 2 | Dispatch Ctrl+wheel | zoom level changes |
| 3 | Inverse-map (cx, cy) → world coordinate after zoom | equals W₀ within ± 1 viewport px |
Expected outcome: row 76.
Max execution time: 1s.
Expected result source: results_report.md row 76.
FT-P-43: Ctrl+drag pan on empty canvas
Traces to: AC-36 (d) Profile: fast
Input data: Ctrl+drag on empty area — results_report.md row 77.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Ctrl+drag from (x1,y1) by (dx,dy) | viewport origin translates by (-dx,-dy) within ± 1 px |
| 2 | Inspect bbox state | no bbox created or modified |
Expected outcome: row 77.
Max execution time: 1s.
Expected result source: results_report.md row 77.
FT-P-44: DetectionClasses loads from /api/annotations/classes
Traces to: AC-37 (load path) Profile: fast
Input data: mount with a successful response of N classes — results_report.md row 78.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Mount <DetectionClasses> |
GET observed |
| 2 | Inspect rendered entries | N entries; active-mode filter applied; no fallback indicator |
Expected outcome: row 78.
Max execution time: 3s.
Expected result source: results_report.md row 78.
FT-P-45: Class hotkey 1–9 selects classes[(key-1) + P]
Traces to: AC-37 (hotkey path)
Profile: fast (synthetic keydown on window)
Input data: keys '1'..'9', photoMode = P, classes mode-ordered — results_report.md row 79.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | For each key k ∈ {1..9}: dispatch keydown |
onSelect fires once |
| 2 | Inspect arg | class.id == classes[(k-1) + P].id |
| 3 | Inspect rendered list element | label index i+1 equals k |
Expected outcome: row 79.
Max execution time: 3s.
Expected result source: results_report.md row 79.
FT-P-46: Class click path
Traces to: AC-37 (click path) Profile: fast
Input data: click a class entry — results_report.md row 80.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Click entry for class C | onSelect fires once with C.id |
Expected outcome: row 80.
Max execution time: 1s.
Expected result source: results_report.md row 80.
FT-P-47: Fallback class list on API empty/5xx
Traces to: AC-37 (fallback) Profile: fast
Input data: GET /api/annotations/classes returns [] (or 5xx) — results_report.md row 81.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Stub empty response | fallback list rendered |
| 2 | Inspect rendered IDs | FALLBACK_CLASS_NAMES × 3 PhotoMode offsets (set equals [0..N-1, 20..20+N-1, 40..40+N-1]) |
Expected outcome: row 81.
Max execution time: 3s.
Expected result source: results_report.md row 81.
FT-P-48: PhotoMode switch — mode set + filter
Traces to: AC-38 (mode set + filter) Profile: fast
Input data: click Winter while photoMode = 0 — results_report.md row 82.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Click Winter | onPhotoModeChange(20) fires once |
| 2 | Inspect rendered class list | filtered to entries with photoMode == 20 |
Expected outcome: row 82.
Max execution time: 1s.
Expected result source: results_report.md row 82.
FT-P-49: PhotoMode auto-select when prior class no longer valid
Traces to: AC-38 (auto-select) Profile: fast
Input data: switch mode where previously selected class is not in the new filtered set — results_report.md row 83.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Pre-select a Regular class | initial state |
| 2 | Switch to Night (photoMode = 40) |
onSelect fires once with modeClasses[0].id |
Expected outcome: row 83.
Max execution time: 1s.
Expected result source: results_report.md row 83.
FT-P-50: yoloId on the wire — classNum == classId + photoModeOffset
Traces to: AC-38 (wire) Profile: fast
Input data: save annotation after drawing with C, P — results_report.md row 84.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Draw a bbox with selectedClassNum = C, photoMode = P |
local detection appended |
| 2 | Save | POST observed |
| 3 | Inspect new detection in body | classNum == C + P |
Expected outcome: row 84.
Max execution time: 3s.
Expected result source: results_report.md row 84.
FT-P-51: Tile-split endpoint contract
Traces to: AC-39 (endpoint)
Profile: fast — quarantined until the split action surfaces on the dataset page
Input data: click Split tile on a dataset item — results_report.md row 85.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Trigger Split tile | one POST observed |
| 2 | Inspect URL + status | ^/api/annotations/dataset/[0-9]+/split$; 200 JSON |
Expected outcome: row 85.
Max execution time: 5s.
Expected result source: results_report.md row 85.
FT-P-52: YOLO label parser — happy path
Traces to: AC-39 (parser happy) Profile: fast
Input data: isSplit: true, splitTile: "3 0.5 0.5 0.2 0.2" — results_report.md row 86.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Pass the item through the parser/render path | no exception |
| 2 | Inspect parsed result | {classNum:3, cx:0.5, cy:0.5, w:0.2, h:0.2} |
Expected outcome: row 86.
Max execution time: 1s.
Expected result source: results_report.md row 86.
FT-P-53: DatasetItem.isSplit is honored on the dataset list path
Traces to: AC-39 (dataset list) Profile: fast
Input data: GET /api/annotations/dataset response contains isSplit: true — results_report.md row 88.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Stub the response | render no crash |
| 2 | Inspect that the field is read into the item state (DOM marker present for split items) | field present |
Expected outcome: row 88.
Max execution time: 3s.
Expected result source: results_report.md row 88.
FT-P-54: Tile auto-zoom viewport matches tile rect
Traces to: AC-40 (viewport)
Profile: fast — quarantined (UX missing today)
Input data: open a splitTile-bearing annotation — results_report.md row 89.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Double-click the annotation in <AnnotationsSidebar> |
<CanvasEditor> opens to that annotation |
| 2 | Inspect viewport rect | equals tile rect within ± 1 px per edge |
Expected outcome: row 89.
Max execution time: 5s.
Expected result source: results_report.md row 89.
FT-P-55: Tile-zoom indicator visible while active
Traces to: AC-40 (indicator)
Profile: fast — quarantined
Input data: tile zoom active → cleared — results_report.md row 90.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Activate tile zoom | indicator icon/badge present in canvas chrome |
| 2 | Clear tile zoom | indicator removed |
Expected outcome: row 90.
Max execution time: 3s.
Expected result source: results_report.md row 90.
Negative Scenarios
FT-N-01: Overlay annotation below the lower bound is NOT rendered
Summary: At currentTime = T, an annotation at T - 60 ms (outside [-50, +150] ms) is excluded.
Traces to: AC-28
Profile: fast
Input data: currentTime = T, annotation at T - 60 ms — results_report.md row 30.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Render with the off-window annotation | overlay NOT rendered |
Expected outcome: row 30 (absent).
Max execution time: 1s.
Expected result source: results_report.md row 30.
FT-N-02: Overlay annotation above the upper bound is NOT rendered
Traces to: AC-28 Profile: fast
Input data: currentTime = T, annotation at T + 160 ms — results_report.md row 31.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Render | overlay NOT rendered |
Expected outcome: row 31.
Max execution time: 1s.
Expected result source: results_report.md row 31.
FT-N-03: Authenticated non-admin navigating to /admin is redirected to /flights
Traces to: AC-22
Profile: e2e — quarantined until role-gate is added (Step 4 / Step 8)
Input data: log in as op_alice (Operator) → navigate /admin — results_report.md row 08.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Authenticate as Operator | session active |
| 2 | Navigate to /admin |
redirect |
| 3 | Inspect final URL + tree | URL is /flights; <AdminPage> NOT mounted |
Expected outcome: row 08.
Max execution time: 10s.
Expected result source: results_report.md row 08.
FT-N-04: Unauthenticated user navigating to /admin is redirected to /login
Traces to: AC-22 Profile: e2e
Input data: no session → navigate /admin — results_report.md row 09.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Clear cookies and bearer | unauthenticated |
| 2 | Navigate to /admin |
redirect |
| 3 | Inspect final URL | /login |
Expected outcome: row 09.
Max execution time: 10s.
Expected result source: results_report.md row 09.
FT-N-05: Authenticated user without SETTINGS permission navigating to /settings
Traces to: AC-22
Profile: e2e — quarantined
Input data: op_bob (no SETTINGS) → /settings — results_report.md row 10.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Authenticate as op_bob | session active |
| 2 | Navigate to /settings |
redirect |
| 3 | Inspect final URL + tree | /flights; <SettingsPage> NOT mounted |
Expected outcome: row 10.
Max execution time: 10s.
Expected result source: results_report.md row 10.
FT-N-06: Upload of 501 MB file surfaces a user-visible 413 error
Traces to: AC-10 Profile: e2e
Input data: 501 MB synthetic file via dropzone — results_report.md row 39.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Drop 501 MB onto <MediaList> |
upload request issued |
| 2 | nginx returns 413 | rejected upstream |
| 3 | Inspect UI | user-visible error containing the i18n "file too large" string; NO alert() |
Expected outcome: row 39.
Max execution time: 30s.
Expected result source: results_report.md row 39.
FT-N-07: Class-delete Cancel path — NO DELETE request issued
Traces to: AC-14, AC-30 Profile: fast
Input data: click Delete → click Cancel — derived from results_report.md row 49.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Click Delete | <ConfirmDialog> mounts |
| 2 | Click Cancel | dialog unmounts |
| 3 | Inspect network log | zero DELETE requests fired |
Expected outcome: cancel branch of row 49.
Max execution time: 3s.
Expected result source: results_report.md row 49.
FT-N-08: Escape on <ConfirmDialog> cancels — no destructive request
Traces to: AC-15 Profile: fast
Input data: open dialog → press Escape — results_report.md row 54.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Open dialog | mounted |
| 2 | Dispatch keydown Escape |
dialog unmounts; cancel callback fires exactly once |
| 3 | Inspect network log | no destructive HTTP fired |
Expected outcome: row 54.
Max execution time: 1s.
Expected result source: results_report.md row 54.
FT-N-09: Header dropdown Escape — close + handler detached
Traces to: AC-16 Profile: fast
Input data: open dropdown → press Escape — results_report.md row 57.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Open dropdown | open |
| 2 | Dispatch Escape | dropdown closes |
| 3 | Inspect attributes | aria-expanded="false"; outside-click handler detached |
Expected outcome: row 57.
Max execution time: 1s.
Expected result source: results_report.md row 57.
FT-N-10: Malformed YOLO label surfaces a user-visible error (no silent swallow, no NaN render)
Traces to: AC-39 Profile: fast
Input data: isSplit: true, splitTile: "garbage" — results_report.md row 87.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Stub the item; render | parse attempted |
| 2 | Inspect UI | error toast / inline error present |
| 3 | Inspect rendered overlay | no NaN values, no tile region rendered |
Expected outcome: row 87.
Max execution time: 3s.
Expected result source: results_report.md row 87.
FT-N-11: Numeric field with empty input — no silent zero
Traces to: AC-26
Profile: fast — quarantined until form hygiene fix (Step 4)
Input data: clear a 09_settings numeric input and submit — results_report.md row 66.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Clear the field | form state observed |
| 2 | Click Save | form blocks save |
| 3 | Inspect network log | no PUT fires |
| 4 | Inspect DOM | submit disabled OR explicit validation error rendered |
Expected outcome: row 66.
Max execution time: 3s.
Expected result source: results_report.md row 66.
FT-N-12: Numeric field with non-numeric input — rejected
Traces to: AC-26
Profile: fast — quarantined
Input data: type "abc" and submit — results_report.md row 67.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Type non-numeric value | state updates |
| 2 | Click Save | rejected |
| 3 | Inspect | validation error rendered; no PUT |
Expected outcome: row 67.
Max execution time: 3s.
Expected result source: results_report.md row 67.
FT-N-13: Settings save with 500 response — saving flag reset; error surfaced
Traces to: AC-27
Profile: fast — quarantined until try/finally fix (Step 4)
Input data: upstream PUT returns 500 — results_report.md row 68.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Trigger save | request issued |
| 2 | Stub responds 500 within 2 s | failure handled |
| 3 | Inspect within ≤ 2 000 ms | toast / inline error present; saving === false; no navigation away |
Expected outcome: row 68.
Max execution time: 5s.
Expected result source: results_report.md row 68.
FT-N-14: Settings save with network failure — try/finally state reset
Traces to: AC-27
Profile: fast — quarantined
Input data: PUT throws (network drop) — results_report.md row 69.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Trigger save | request issued |
| 2 | Stub throws | rejection handled |
| 3 | Inspect | saving === false; user-visible error present |
Expected outcome: row 69.
Max execution time: 5s.
Expected result source: results_report.md row 69.
FT-N-15: MediaType magic-literal / magic-string hygiene
Traces to: AC-29 Profile: static
Input data: regex sweep of src/ for mediaType\s*[!=]==?\s*[0-9] and mediaType\s*[!=]==?\s*['"] — results_report.md rows 20, 21.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Run the two regexes | results captured |
| 2 | Inspect | match_count == 0 for both |
Expected outcome: rows 20, 21.
Max execution time: 5s.
Expected result source: results_report.md rows 20, 21.
FT-N-16: mission-planner getWeatherData fail-soft when VITE_OWM_API_KEY is unset
Traces to: AC-42 (AZ-499 AC-3) Profile: fast
Input data: build-time env with VITE_OWM_API_KEY="" (or undefined).
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Spy globalThis.fetch |
spy installed, no calls yet |
| 2 | Stub import.meta.env.VITE_OWM_API_KEY = "" and invoke getWeatherData(50, 30) |
resolves |
| 3 | Inspect return value | === null |
| 4 | Inspect fetch spy | mock.calls.length === 0 |
Pass criteria: function returns null AND no outbound HTTP request is made when the API key is unset. Mirrors the AZ-448 fail-soft contract on the main SPA.
Max execution time: 1s (env stub + sync inspection only).
Expected result source: AZ-499 AC-3 (no results_report.md row needed — behavioral test, no input data).
Cycle 2 Additions (Phase B Cycle 2 — Self-hosted satellite tiles + mission-planner OWM hardening)
The scenarios below were appended via /test-spec cycle-update mode after Phase B Cycle 2 completed (AZ-498 + AZ-499, batch_11). They use the same template shapes as the original spec. Cross-references: AC-41 (satellite tiles), AC-42 (mission-planner OWM env hardening) are the new global ACs added to traceability-matrix.md; the underlying task-spec ACs are AZ-498 AC-1..AC-7, AC-9 and AZ-499 AC-1..AC-6 (AZ-498 AC-8 was dropped with explicit user approval per _docs/03_implementation/batch_11_report.md; AZ-499 AC-7 is a manual deliverable, not a test).
FT-P-56: Self-hosted satellite tile URL is env-var resolved
Traces to: AC-41 (AZ-498 AC-1, AC-2) Profile: fast
Input data: build-time env with VITE_SATELLITE_TILE_URL set, unset, or set with a trailing slash.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Stub VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y} and call getTileUrl() |
returns the env value verbatim |
| 2 | Stub VITE_SATELLITE_TILE_URL="" and call getTileUrl() |
returns DEFAULT_SATELLITE_TILE_URL (http://localhost:5100/tiles/{z}/{x}/{y}) |
| 3 | Stub VITE_SATELLITE_TILE_URL=http://satellite-provider:5100/tiles/{z}/{x}/{y}/ (trailing slash) |
returns the value with the trailing slash stripped |
| 4 | Mount <FlightMap> with the env unset; inspect rendered <TileLayer> data-tile-url |
equals DEFAULT_SATELLITE_TILE_URL |
Pass criteria: all four assertions hold. Mirrors the established getOwmBaseUrl() / getApiBase() env-resolution pattern.
Max execution time: 2s (jsdom render + four stub variations).
Expected result source: AZ-498 AC-1, AC-2 (no results_report.md row needed — env-var plumbing, no input data fixture).
FT-P-57: <TileLayer crossOrigin="use-credentials"> enables cookie-auth on tile fetches
Traces to: AC-41 (AZ-498 AC-3); E1 (air-gap-friendly bundle); RID R-Reliability for tile auth Profile: fast
Input data: <FlightMap> and <MiniMap> mounted with the default tile URL.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Mount <FlightMap>; inspect rendered <TileLayer> data-cross-origin attribute |
=== "use-credentials" |
| 2 | Mount <MiniMap pointPosition={…}>; inspect rendered <TileLayer> data-cross-origin attribute |
=== "use-credentials" |
| 3 | (e2e — gated) Issue GET <VITE_SATELLITE_TILE_URL substituted with /tiles/1/0/0> from the rendered map; inspect outbound request |
request.credentials === "include" (browser attaches the same-origin auth cookie) |
Pass criteria: every <TileLayer> the SPA renders carries crossOrigin="use-credentials" so the browser sends the satellite-provider cookie on same-origin tile requests. Step 3 e2e is gated by the cross-workspace satellite-provider cookie-auth ticket landing (Step 16 deploy gate).
Max execution time: 2s for steps 1+2 (fast); e2e step is part of infrastructure.e2e.ts — bounded by suite-e2e timeout.
Expected result source: AZ-498 AC-3 (no results_report.md row — DOM-attribute observable).
FT-P-58: Classic/satellite map toggle, mapType state, and MiniMap.Props.mapType are removed
Traces to: AC-41 (AZ-498 AC-4) Profile: fast
Input data: <FlightMap> mounted with the default tile URL; <MiniMap> mounted with only pointPosition (no mapType prop).
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Mount <FlightMap>; query `screen.queryByRole('button', { name: /satellite |
classic/i })` |
| 2 | Mount <FlightMap>; query screen.getAllByTestId('tile-layer') |
length === 1 (no per-mode branching, single layer) |
| 3 | Compile-time check: instantiate <MiniMap pointPosition={…}> without mapType |
TypeScript tsc --noEmit -p tsconfig.test.json succeeds (STC-T1) |
| 4 | Compile-time check: source-tree grep for any remaining mapType reference under src/features/flights/ |
zero hits (compilation error if not — covered by STC-T1) |
Pass criteria: no toggle button, no mapType state, MiniMap.Props has no mapType. Removal is permanent; the flights.planner.satellite i18n key was removed from both en.json and ua.json in lockstep (i18n key parity preserved via STC-FP22).
Max execution time: 2s (jsdom render + grep).
Expected result source: AZ-498 AC-4.
FT-P-59: e2e harness exercises the new /tiles/{z}/{x}/{y} path
Traces to: AC-41 (AZ-498 AC-6); E1 (air-gap) Profile: e2e
Input data: suite-e2e compose stack up; tile-stub configured at http://tile-stub:8082/tiles/{z}/{x}/{y}.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | infrastructure.e2e.ts AC-2 — issue GET http://tile-stub:8082/tiles/1/0/0 from the playwright runner |
HTTP 200, response is a 256×256 image (JPEG) |
| 2 | Inspect response headers | Content-Type: image/jpeg, Cache-Control present, ETag present |
| 3 | Inspect outbound request from the SPA's <TileLayer> |
URL matches ^http://tile-stub:8082/tiles/\d+/\d+/\d+$ (NOT /{z}/{x}/{y}.png, NOT the legacy /sat/... Esri shape) |
| 4 | Inspect EXTERNAL_HOSTS route guard |
OSM and Esri hosts are NOT in the allow-list (removed during cycle 2 cleanup) |
Pass criteria: tile fetch shape matches the satellite-provider contract documented at _docs/02_document/contracts/satellite-provider/tiles.md. Note: the same-origin cookie-auth path (cookie attached on the actual fetch) is verified once the cross-workspace satellite-provider cookie-auth ticket lands; until then, the e2e profile uses the tile-stub which accepts requests without a cookie.
Max execution time: bounded by suite-e2e infrastructure-test timeout (per e2e/tests/infrastructure.e2e.ts).
Expected result source: contract at _docs/02_document/contracts/satellite-provider/tiles.md v1.0.0; AZ-498 AC-6.
FT-P-60: mission-planner getWeatherData uses env-resolved key + base URL
Traces to: AC-42 (AZ-499 AC-1, AC-2, AC-4) Profile: fast
Input data: build-time env with VITE_OWM_API_KEY set + VITE_OWM_BASE_URL either set, unset, or set with a trailing slash.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Spy globalThis.fetch returning a 200 OK with body { wind: { speed: 5, deg: 90 } } |
spy installed |
| 2 | Stub VITE_OWM_API_KEY=abc123 + VITE_OWM_BASE_URL=""; invoke getWeatherData(50, 30) |
outbound URL contains appid=abc123 AND units=metric |
| 3 | Stub VITE_OWM_API_KEY=abc123 + VITE_OWM_BASE_URL=https://example.test/data/2.5; invoke getWeatherData(50, 30) |
outbound URL starts with https://example.test/data/2.5/weather? |
| 4 | Stub VITE_OWM_API_KEY=abc123 + VITE_OWM_BASE_URL=https://example.test/data/2.5/ (trailing slash); invoke getWeatherData(50, 30) |
outbound URL starts with https://example.test/data/2.5/weather? (slash stripped) |
| 5 | Stub VITE_OWM_API_KEY=abc123 + VITE_OWM_BASE_URL=""; invoke getWeatherData(50, 30) |
outbound URL starts with https://api.openweathermap.org/data/2.5/weather? (default base) |
| 6 | Inspect return value on a successful fetch | === { windSpeed: 5, windAngle: 90 } (existing parsed-wind shape preserved) |
Pass criteria: every outbound URL is reconstructed from env vars; the public getWeatherData(lat, lon) signature and WeatherData return shape are unchanged. Pairs with the AZ-499 NFR-Compatibility constraint.
Max execution time: 2s (env stubs + fetch-spy assertions; no real network).
Expected result source: AZ-499 AC-1, AC-2, AC-4 (no results_report.md row — env-var plumbing).
FT-P-61: mission-planner geocodeAddress uses env-resolved Google API key
Traces to: AC-43 (AZ-501 AC-1) Profile: fast
Input data: build-time env with VITE_GOOGLE_GEOCODE_KEY set to a placeholder string.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Spy globalThis.fetch returning a 200 OK with body { status: 'OK', results: [{ geometry: { location: { lat, lng } } }] } |
spy installed |
| 2 | Stub VITE_GOOGLE_GEOCODE_KEY=env-key-xyz; invoke geocodeAddress('Kyiv, Ukraine') |
outbound URL contains key=env-key-xyz AND address=Kyiv%2C%20Ukraine |
| 3 | Inspect return value | === { lat, lng } from the mocked response |
Pass criteria: the outbound URL is reconstructed from the env var; no literal key remains in mission-planner/src/services/GeocodeService.ts (defense-in-depth confirmed by STC-SEC1D / NFT-SEC-09b).
Max execution time: 2s.
Expected result source: AZ-501 AC-1.
FT-N-17: mission-planner geocodeAddress fail-soft when VITE_GOOGLE_GEOCODE_KEY is unset
Traces to: AC-43 (AZ-501 AC-3) Profile: fast
Input data: build-time env with VITE_GOOGLE_GEOCODE_KEY empty / undefined.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Spy globalThis.fetch; spy console.warn |
spies installed |
| 2 | Stub VITE_GOOGLE_GEOCODE_KEY=''; invoke geocodeAddress('anywhere') |
returns null; fetch is NOT called; console.warn called exactly once with a message containing VITE_GOOGLE_GEOCODE_KEY |
| 3 | Stub VITE_GOOGLE_GEOCODE_KEY=env-key-xyz and force fetch to reject with Error('boom'); invoke geocodeAddress('anywhere') |
returns null; promise does NOT throw |
Pass criteria: missing-key path is silent-but-warned and never throws; network-error path is silent and never throws — preserves the LeftBoard address-box UX of "Enter does nothing if address is unresolvable". Max execution time: 2s. Expected result source: AZ-501 AC-3.
FT-P-62: AdminPage class edit — inline form + PATCH wire contract + refresh
Traces to: O9 (P12) — landed cycle 4 / 2026-05-13 by AZ-512. Profile: fast
Input data: an <AdminPage> mount with at least one detection class loaded via GET /api/annotations/classes; the user activates the row's edit (✎) affordance.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Inspect each rendered row | One edit (✎) button per class row (AC-1) |
| 2 | Click the edit (✎) on row N | Row N replaces its read-only cells with editable name / shortName / color / maxSizeM inputs seeded with the row's current values; Save + Cancel buttons appear; no other row is in edit mode (AC-2 single-row invariant) |
| 3 | Click edit (✎) on row M while row N is editing | Row N reverts to read-only; row M enters edit mode |
| 4 | Modify name and click Save (or press Enter inside the form) |
Exactly one PATCH /api/admin/classes/{N} is observed with body { name, shortName, color, maxSizeM } (full body per Risk-2 mitigation); on 200/2xx <AdminPage> re-fetches via GET /api/annotations/classes and row N re-renders read-only with the new values (AC-3) |
Pass criteria: zero PATCH calls before step 4; exactly one PATCH in step 4 with the complete editable shape; URL pattern ^/api/admin/classes/\d+$; success-path refresh observed via the existing GET /api/annotations/classes builder (no new endpoint introduced — endpoints.admin.class(id) reused per task constraint).
Max execution time: 5s.
Expected result source: _docs/02_tasks/done/AZ-512_admin_edit_detection_class.md AC-1..AC-3.
FT-N-18: AdminPage class edit — error paths (Cancel, validation, 5xx)
Traces to: O9 (P12), O10 (B4 anti-pattern: no alert()) — landed cycle 4 / 2026-05-13 by AZ-512.
Profile: fast
Input data: <AdminPage> mounted with at least one class loaded; the row's edit form is open.
Steps:
| Step | Consumer Action | Expected System Response |
|---|---|---|
| 1 | Modify any field; click Cancel (or press Escape in the form) | Zero PATCH observed; row reverts to original read-only values (AC-4) |
| 2 | Clear name; click Save |
Zero PATCH observed; inline role="alert" element renders admin.classes.nameRequired (en / ua localized) (AC-5) |
| 3 | Set maxSizeM ≤ 0 or NaN; click Save |
Zero PATCH observed; inline role="alert" renders admin.classes.maxSizeMustBePositive (AC-5) |
| 4 | Stub PATCH to return 500; click Save with valid fields | Exactly one PATCH observed (counterpart to FT-P-62 step 4); form stays open with the user's edits intact; inline role="alert" renders admin.classes.updateFailed; window.alert is NEVER called (AC-6 — Finding B4 anti-pattern enforced) |
Pass criteria: every error path produces exactly the documented network footprint and exactly the documented inline error key; window.alert is spied and asserted-zero across the entire scenario (the STC-SEC7 static check independently guards the no-alert() invariant in production source).
Max execution time: 10s.
Expected result source: _docs/02_tasks/done/AZ-512_admin_edit_detection_class.md AC-4 / AC-5 / AC-6.
Notes carried into Phase 3
- All tests tagged
quarantinedcorrespond to features either pending a Step 4 fix (e.g., AC-13 i18n detector, AC-21 panel persistence, AC-22 role-gate, AC-26/27 form hygiene, AC-39 split surface, AC-40 tile zoom) or pending Phase B implementation (AC-11 bundle gate, AC-24 SSE refresh, AC-25 async video, AC-40 tile zoom). The test is written so it activates the day the implementation lands; Phase 3 will surface them for downgrade or accept. - FT-P-13 / FT-P-14 / FT-P-37 / FT-P-38 / FT-P-51 / FT-P-54 / FT-P-55 / FT-N-03 / FT-N-05 / FT-N-11 / FT-N-12 / FT-N-13 / FT-N-14 are all on the quarantine list above.
- Tests that depend on the enum-spec numeric snapshot (
AC-04rows 15-17) are blocked on populatingenum_spec_snapshot.jsonfrom the suite spec — Phase 3 surfaces this as the blocking input. - AC-37 row 79 depends on backend ordering returning
[0..N-1, 20..20+N-1, 40..40+N-1]. If the seed reveals otherwise, this test fails — fix can land either side perdata_parameters.md.