Files
ui/_docs/02_document/tests/blackbox-tests.md
T
Oleksandr Bezdieniezhnykh f7dd6c98d8
ci/woodpecker/push/build-arm Pipeline failed
[AZ-501] [AZ-502] Cycle 2 Step 14 security audit + inline fixes
Security audit (5 phases) → reports under _docs/05_security/.

AZ-501 (F-SAST-1, HIGH): Externalize hardcoded Google Geocode key
from mission-planner/src/config.ts to VITE_GOOGLE_GEOCODE_KEY via
new GeocodeService.ts; fail-soft warn when unset; STC-SEC1D static
deny-list gate; +5 unit tests in tests/mission_planner_geocode.test.ts.

AZ-502 (F-DEP-1, HIGH): Force vite>=6.4.2 and postcss>=8.5.10 via
package.json overrides in both roots; clean reinstall clears all
bun audit advisories.

Test-spec sync (Step 12) + Update Docs (Step 13) deltas: AC-43, AC-44,
NFT-SEC-09b, FT-P-61, FT-N-17, ripple log, batch_12 report.

Pending user actions: revoke Google + OWM keys (AC-6 / AZ-499 AC-7).

229 PASS / 13 SKIP / 0 FAIL on static + fast suites.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 05:31:11 +03:00

57 KiB
Raw Blame History

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 apiClientresults_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 /annotationsresults_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 /annotationsresults_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.Imageresults_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 msresults_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.jsonresults_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/**/*.tsxresults_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 = Presults_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 19 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 = 0results_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: trueresults_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 msresults_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 msresults_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 /adminresults_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 /adminresults_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) → /settingsresults_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).


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.


Notes carried into Phase 3

  • All tests tagged quarantined correspond 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-04 rows 15-17) are blocked on populating enum_spec_snapshot.json from 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 per data_parameters.md.