[AZ-447] autodev Steps 1-4 baseline: docs, tests, refactor specs

Captures the full output of autodev existing-code Phase A through
Step 4 (Code Testability Revision) for the Azaion UI workspace:

- Step 1 Document: _docs/02_document/ (FINAL_report, architecture,
  glossary, components/, modules/, diagrams/, system-flows,
  module-layout) plus _docs/00_problem/ + _docs/01_solution/ +
  _docs/legacy/ + _docs/how_to_test + README.
- Step 2 Architecture Baseline: architecture_compliance_baseline.md.
- Step 3 Test Spec: _docs/02_document/tests/ (environment,
  test-data, blackbox/performance/resilience/security/
  resource-limit tests, traceability-matrix), enum_spec_snapshot,
  expected_results/results_report.md (98 rows), plus the
  run-tests.sh + run-performance-tests.sh runners.
- Step 4 Code Testability Revision: 01-testability-refactoring/
  run dir (list-of-changes C01-C07, deferred_to_refactor,
  analysis/research_findings + refactoring_roadmap) and the 7
  child task specs AZ-448..AZ-454 under _docs/02_tasks/todo/
  plus _dependencies_table.md.
- _docs/_autodev_state.md pins the cursor at Step 4 / refactor
  Phase 4 entry so /autodev resumes cleanly.

Epic AZ-447 (UI testability gates) tracks the 7 child tasks that
will land in subsequent commits.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 00:38:49 +03:00
parent da0a5aa187
commit 510df68bcf
84 changed files with 13065 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+212
View File
@@ -0,0 +1,212 @@
# Test Environment
## Overview
**System under test**: the Azaion UI single-page application — a React 19 + Vite 6 static bundle served by `nginx:alpine` (port 80) that talks to the parent suite microservices through the same nginx instance (reverse-proxied `/api/<service>/` routes per `nginx.conf`). The SPA's observable surface is everything reachable from a browser: outgoing HTTP requests (URL, method, headers, body), incoming responses (rendered DOM, console, errors), outgoing EventSource streams, browser storage (`localStorage` / `sessionStorage` / `document.cookie`), and the built artifact (`dist/`).
**Consumer app purpose**: the test runners are the consumer. They drive a real browser (or jsdom) at the SPA's public surface, capture every outbound request/event, and assert against `_docs/00_problem/input_data/expected_results/results_report.md` (95 rows).
**Black-box discipline**: the consumer MUST NOT import from `src/` (except for the typed enum shapes that ARE part of the wire contract per `P9`), MUST NOT bypass the React tree to call internal hooks, and MUST NOT inspect React component state directly. Assertions are made on the rendered DOM, ARIA roles, outgoing network activity, EventSource state machine, console output, and built artifacts.
## Test Execution Profiles
Two profiles share the artifact directory but address different black-box levels. Runner selection is deferred to the Decompose Tests step (`autodev` Step 5) — this document specifies the **environment requirements**, not the runner choice.
| Profile | Scope | Black-box level | Backing services | Browser |
|---------|-------|----------------|------------------|---------|
| `fast` | Unit + component + static checks | DOM + network requests issued by a mounted component or by a code-level helper, captured at the `fetch` / `EventSource` boundary | Stubbed (request interception layer, e.g. MSW or equivalent). No real services. | jsdom or headless Chromium (component renderer). |
| `e2e` | Browser smoke + cross-service flows | Real browser → real nginx (UI image) → real suite docker-compose stack | Full suite docker-compose stack (`admin/`, `flights/`, `annotations/`, `detect/`, `gps-denied-desktop/`, `gps-denied-onboard/`, `autopilot/`, `resource/`, `loader/`). | Headless Chromium + Firefox latest 2 versions per AC-18. |
| `static` | Source / config / bundle checks | The repo + the `dist/` artifact | None (no runtime). | None (CLI). |
Tests in `blackbox-tests.md`, `performance-tests.md`, etc. tag themselves with `Profile: fast | e2e | static` to make runner routing unambiguous.
## Docker Environment
The Azaion UI image carries no DB. The "Docker environment" is the test-time choreography of UI + suite services + stubs.
### Services (e2e profile)
| Service | Image / Build | Purpose | Ports |
|---------|--------------|---------|-------|
| `azaion-ui` | Built from this repo (`Dockerfile`, ARM64 per H1 / S5) — final stage `nginx:alpine` | The SPA under test | `80` |
| `admin` | Suite `admin/` image (auth + users + classes write + GPS settings) | Auth + RBAC; cookie issuer per E3 | per suite compose |
| `flights` | Suite `flights/` image | Flight CRUD + waypoints + aircraft + live-GPS SSE | per suite compose |
| `annotations` | Suite `annotations/` image | Media + annotations + dataset + class read + settings + status SSE | per suite compose |
| `detect` | Suite `detect/` image | Sync image detect (and future async video detect F7) | per suite compose |
| `gps-denied-desktop`, `gps-denied-onboard`, `autopilot`, `resource`, `loader` | Suite microservice images | Auxiliary services hit by the SPA (only `loader/` and `resource/` are hit on production paths today; `gps-denied-*` is target-only F12) | per suite compose |
| `owm-stub` | Tiny HTTP server returning canned OpenWeatherMap responses | Replace direct OWM HTTPS (E10) so tests are deterministic and rate-limit-free | `8081` |
| `tile-stub` | Tiny HTTP server returning a 256x256 PNG | Replace OSM tile servers | `8082` |
| `test-db` | Suite-managed (Postgres per suite default) | Backs `admin/`, `flights/`, `annotations/` | Internal |
### Networks
| Network | Services | Purpose |
|---------|----------|---------|
| `azaion-test-net` | all of the above | Isolated test network; no internet egress (OWM + tile stubs replace the only external hops). |
### Volumes
| Volume | Mounted to | Purpose |
|--------|-----------|---------|
| `test-db-data` | `test-db:/var/lib/postgresql/data` | Suite DB persistence — wiped between e2e runs (see Data Isolation below). |
| `seed-fixtures` | `admin:/seed`, `flights:/seed`, `annotations:/seed` (read-only) | Bootstrap data loaded at service start (users, flights, classes, sample media). See `test-data.md`. |
| `test-output` | `playwright-runner:/output` | Where the consumer writes CSV reports, screenshots, traces. |
### docker-compose structure (outline)
```yaml
services:
azaion-ui:
build: .
ports: ["80:80"]
depends_on: [admin, flights, annotations, detect]
environment:
AZAION_REVISION: ${CI_COMMIT_SHA:-test}
admin:
image: azaion/admin:test
depends_on: [test-db]
flights:
image: azaion/flights:test
depends_on: [test-db]
annotations:
image: azaion/annotations:test
depends_on: [test-db]
detect:
image: azaion/detect:test
test-db:
image: postgres:16-alpine
owm-stub:
build: ./testing/stubs/owm
tile-stub:
build: ./testing/stubs/tile
playwright-runner:
build: ./testing/runner
depends_on: [azaion-ui]
environment:
BASE_URL: http://azaion-ui:80
OWM_BASE_URL: http://owm-stub:8081
TILE_BASE_URL: http://tile-stub:8082
```
The compose file is part of the test-spec output; its concrete shape lands when the Decompose Tests step picks the runner (Step 5).
## Consumer Application
### `fast` profile
**Tech stack** (target — chosen at Step 5): a component-testing harness in TypeScript (Vitest or Jest + React Testing Library) plus a request-interception layer (MSW or equivalent) and jsdom (or headless Chromium component renderer).
**Entry point**: `npm run test:fast` (or `bun test`) — runs all `*.test.ts(x)` files under the test root.
#### Communication with system under test
| Interface | Protocol | Endpoint / Topic | Authentication |
|-----------|----------|-----------------|----------------|
| Mounted React component under test | direct mount via the testing library | n/a — observe the DOM + outbound requests captured by MSW | Stubbed bearer / cookie in test helpers |
| Outgoing `fetch` (under test) | HTTP via MSW handlers | mock `/api/<service>/...` per test | per handler |
| Outgoing `EventSource` (under test) | SSE via MSW or `EventSourcePolyfill` test double | mock `/api/<service>/...` per test | bearer in query string (ADR-008) |
| Static check | `bun run` script + filesystem regex (e.g. via `ripgrep`) | n/a | n/a |
### `e2e` profile
**Tech stack** (target — chosen at Step 5): Playwright (Chromium + Firefox per AC-18) driving the deployed `azaion-ui` nginx; assertion library is the runner's built-in expectations + a small request-interception adapter that logs every outbound request for assertion.
**Entry point**: `bun run test:e2e` — runs all `*.e2e.ts` files under the test root against the live compose stack.
#### Communication with system under test
| Interface | Protocol | Endpoint / Topic | Authentication |
|-----------|----------|-----------------|----------------|
| Browser navigation | HTTPS | `${BASE_URL}/login`, `/flights`, `/annotations`, `/dataset`, `/admin`, `/settings` | login via the public `/login` flow |
| Suite REST | HTTPS via SPA's nginx proxy | `/api/admin/*`, `/api/flights/*`, `/api/annotations/*`, `/api/detect/*`, `/api/loader/*`, `/api/resource/*`, `/api/gps-denied-{desktop,onboard}/*`, `/api/autopilot/*` | bearer in `Authorization` header + cookie (HttpOnly) |
| Suite SSE | HTTPS | `/api/flights/<id>/live-gps`, `/api/annotations/annotations/events`, `/api/detect/stream/<jobId>` (F7 target) | bearer in `?token=` per ADR-008 |
| Bundle / image inspection | filesystem / `docker inspect` | n/a | n/a |
| OpenWeatherMap | HTTPS via `owm-stub` | per stub | none |
| OSM tiles | HTTPS via `tile-stub` | per stub | none |
### What the consumer does NOT have access to
- No direct DB access to `test-db`. Suite DB queries are forbidden from the test runner; the consumer asserts only through the suite's REST + SSE.
- No internal `src/` imports beyond the typed enum shapes that ARE part of the wire contract (`AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`, `MediaType`, `AnnotationSource` per `data_parameters.md` §1) — these are the spec the test asserts against per `P9`.
- No React component state read via hooks or test-only escape hatches; only the DOM + outbound network surface is observable.
- No shared memory or filesystem with the SPA.
## CI/CD Integration
**When to run**:
- `fast` profile: on every commit (planned addition to `.woodpecker/build-arm.yml`; currently absent — O14).
- `e2e` profile: on PR merge to `dev` / `stage` and pre-release on `main`. Long-running; not gating regular commits.
- `static` profile: on every commit (lints + bundle / config checks run as part of the build).
**Pipeline stage**:
- `fast` + `static`: between `bun install` and `bun run build`.
- `e2e`: after `bun run build`, against the just-built image, in a separate compose job.
**Gate behavior**:
- `fast` + `static`: block merge on failure.
- `e2e`: block merge on failure for `dev` / `stage`. On `main`, manual approval is allowed for known-quarantined tests (e.g., the Phase B target tests for AC-11 / AC-24 / AC-40 that assert "when implemented").
**Timeout**: `fast` ≤ 5 min suite total. `e2e` ≤ 30 min suite total. Individual tests timeout per the `Max execution time` field in each scenario.
## Reporting
**Format**: CSV (and JUnit XML for CI consumption when the runner produces it).
**Columns**: `Test ID, Test Name, Profile, Execution Time (ms), Result (PASS|FAIL|SKIP|QUARANTINE), Error Message, Traces to AC, Traces to results_report.md row`.
**Output path**: `./test-output/report.csv` (mounted from the `playwright-runner` / `vitest-runner` container). For `static` checks, `./test-output/static-report.csv`. Suite-level rollup written to `./test-output/summary.csv`.
## Test Execution
**Decision**: **Docker (preferred)** for the `e2e` and `static` profiles; **local Bun** for the `fast` profile and as an option for `e2e` on developer machines that already have Playwright + the suite stack running. The project is **not hardware-dependent** — see "Hardware dependencies found" below.
### Hardware dependencies found
| Indicator | Found at | Verdict |
|-----------|----------|---------|
| GPU / CUDA imports | none in `src/` or `mission-planner/` | absent |
| CoreML / MPS imports | none | absent |
| Camera / sensor / GPIO / V4L2 | none | absent — the SPA reads `<video>` elements rendered from a server-supplied URL |
| OS-specific drivers / kernel modules | none | absent |
| Platform-gated source branches | none | absent |
| Spec-level constraint | `_docs/00_problem/restrictions.md` H3 ("No GPU expectation in UI image") + H4 ("Chromium / Firefox latest 2") | confirms platform-neutral browser surface |
Conclusion: classify as **Not hardware-dependent**. Docker headless Chromium reproduces the real production runtime; no real-hardware execution path is required.
### Execution instructions
#### Docker mode (preferred; CI default)
1. **Prerequisites**: Docker Engine 24+ with the `azaion-test-net` network reachable, ARM64 or amd64 host (the UI image is ARM64-only per H1 — CI runners on ARM64; multi-arch builds optional for local dev).
2. **Build**: `docker buildx build --platform linux/arm64 -t azaion-ui:test .` (or `--platform linux/amd64` on amd64 dev machines).
3. **Compose up**: `docker compose -f e2e/docker-compose.suite-e2e.yml up -d` — brings up `azaion-ui`, `admin`, `flights`, `annotations`, `detect`, the auxiliary services, `owm-stub`, `tile-stub`, `test-db`, and the `playwright-runner`.
4. **Run tests**: `docker compose -f e2e/docker-compose.suite-e2e.yml run --rm playwright-runner` — the runner image entrypoint is `bun run test:e2e`. Reports land in `./test-output/`.
5. **Tear down**: `docker compose -f e2e/docker-compose.suite-e2e.yml down -v` (volumes wiped between runs).
6. **Required environment**: `BASE_URL=http://azaion-ui:80`, `OWM_BASE_URL=http://owm-stub:8081`, `TILE_BASE_URL=http://tile-stub:8082`, `CI_COMMIT_SHA=<sha>` (stamped into `AZAION_REVISION`).
#### Local mode (for `fast` profile + developer-machine `e2e` runs)
1. **Prerequisites**: Bun 1.3.11 (matches S4), Chromium + Firefox latest two stable lines installed via Playwright (`bunx playwright install --with-deps chromium firefox`).
2. **`fast` profile**: `bun install && bun run test:fast` (alias for `bun test`, runs Vitest under jsdom plus MSW handlers).
3. **`e2e` profile (local)**: bring up the suite stack via the parent suite's compose file (`../docker-compose.yml`), point `BASE_URL=http://localhost:80`, then `bun run test:e2e`.
4. **Required environment**: same as Docker mode plus `OWM_API_KEY=test-key` (passed through the OWM stub).
#### CI runner mapping
| Profile | Runner type | Mode | Gate |
|---------|------------|------|------|
| `static` | ARM64 build runner | local (no browser) | block merge on failure |
| `fast` | ARM64 build runner | local Bun (jsdom + MSW) | block merge on failure |
| `e2e` | ARM64 e2e runner with Docker | Docker compose stack | block merge on failure for `dev`/`stage`; manual approval allowed for quarantined tests on `main` (per CI/CD Integration above) |
The decision is consumed by Phase 4 to choose between `scripts/run-tests.sh` (local Bun for `fast` + `static`) and `e2e/docker-compose.suite-e2e.yml` (Docker for `e2e`).
@@ -0,0 +1,224 @@
# Performance Tests
The Azaion UI is a thin SPA; the dominant performance concerns are bundle size, auth-refresh transparency, SSE responsiveness, and UI reflection of server-confirmed state changes. Server-side throughput is OUT of scope here — this file covers the UI's observable timing only.
### NFT-PERF-01: Initial JS bundle ≤ 2 MB gzipped
**Summary**: The sum of gzipped initial-route JS chunks in `dist/` stays within the architecture's stated budget.
**Traces to**: AC-11, O13
**Metric**: gzipped byte total of initial JS entries.
**Profile**: static
**Preconditions**:
- `bun run build` has produced `dist/`.
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Run `vite build` (or read the build manifest if already built) | `dist/` produced |
| 2 | Walk Vite's `manifest.json` to enumerate entry chunks (non-async) | list of initial chunks |
| 3 | Gzip-size each chunk (Node `zlib.gzipSync(content, {level:9})` or equivalent) | per-chunk size |
| 4 | Sum sizes | total bytes |
**Pass criteria**: total ≤ 2 097 152 bytes (2 MB). Documentary today — no CI gate (AC-11 status: "target, not currently enforced"). Test exists so the gate flips to blocking the day CI wires it up.
**Duration**: ≤ 60 s.
**Expected result source**: `results_report.md` row 40.
---
### NFT-PERF-02: Auth refresh — exactly one network round trip per cycle
**Summary**: A single 401-triggered refresh round consists of exactly one `POST /api/admin/auth/refresh` plus one retry of the original request.
**Traces to**: AC-01, AC-23
**Metric**: count of `/api/admin/auth/refresh` requests per refresh event.
**Profile**: fast
**Preconditions**:
- Authenticated session.
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Issue an authenticated request that returns 401 once, then 200 on retry | network log captured |
| 2 | Count refresh requests fired in the cycle | exactly 1 |
**Pass criteria**: refresh count == 1 per cycle (`results_report.md` row 12 — exact).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 12.
---
### NFT-PERF-03: SSE bearer-rotation reconnect ≤ 5 s
**Summary**: When the bearer rotates while N SSE streams are open, all streams close and reopen with the new token within 5 s.
**Traces to**: AC-24
**Metric**: per-EventSource time from `close()` observed to next `OPEN` readyState (after reconnect with new token).
**Profile**: fast — `quarantined` until SSE refresh-reconnect is implemented (Step 8 hardening)
**Preconditions**:
- Two EventSources open (live-GPS + annotation-status).
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Trigger a refresh that rotates the bearer | new bearer in memory |
| 2 | For each EventSource: time from old `close` to new `OPEN` | dt_i (ms) |
| 3 | Inspect new URLs for new token in query string | new `?token=` value |
**Pass criteria**: `max(dt_i) ≤ 5 000 ms`; both streams close+open exactly once (`results_report.md` row 13).
**Duration**: ≤ 30 s.
**Expected result source**: `results_report.md` row 13.
---
### NFT-PERF-04: Live-GPS SSE opens within 5 s of flight select
**Summary**: After clicking a flight in the Header, the live-GPS EventSource reaches `OPEN` quickly.
**Traces to**: AC-08
**Metric**: time from select-flight click to EventSource `readyState === 1` (OPEN).
**Profile**: e2e (suite live-gps simulator emits events at 1 Hz)
**Preconditions**:
- Authenticated; flight selectable.
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Click a flight | one EventSource constructed to `^/api/flights/[0-9]+/live-gps(\?|$)` |
| 2 | Wait for `OPEN` | dt (ms) |
**Pass criteria**: `dt ≤ 5 000 ms` (`results_report.md` row 34).
**Duration**: ≤ 10 s.
**Expected result source**: `results_report.md` row 34.
---
### NFT-PERF-05: Live-GPS SSE closes within 1 s of deselect
**Summary**: Deselecting the flight tears down the live-GPS stream promptly.
**Traces to**: AC-08
**Metric**: time from deselect to `CLOSED`.
**Profile**: e2e
**Preconditions**:
- Continuation of NFT-PERF-04.
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Deselect the flight | EventSource closes |
| 2 | Wait for `CLOSED` | dt (ms) |
**Pass criteria**: `dt ≤ 1 000 ms` and no remaining open live-GPS sources (`results_report.md` row 35).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 35.
---
### NFT-PERF-06: Annotation-status SSE unsubscribes within 1 s on page unmount
**Summary**: Navigating away from `/annotations` closes the status-events SSE within 1 s.
**Traces to**: AC-09
**Metric**: time from unmount to `CLOSED`.
**Profile**: fast
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Mount `/annotations` then unmount | EventSource transitions |
| 2 | Measure dt | ms |
**Pass criteria**: `dt ≤ 1 000 ms` (`results_report.md` row 25).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 25.
---
### NFT-PERF-07: Bulk-validate UI reflects new status within 2 s
**Summary**: After a successful bulk-validate, every selected row shows `Validated` quickly.
**Traces to**: AC-07
**Metric**: time from server 200 to last DOM row update.
**Profile**: fast
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Select N items, click Validate | request issued |
| 2 | Stub responds 200 | UI updates begin |
| 3 | Wait for all N rows to show `Validated` | dt (ms) |
**Pass criteria**: `dt ≤ 2 000 ms` (`results_report.md` row 37).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 37.
---
### NFT-PERF-08: Panel-width persistence debounce ≤ 1 s after resize-end
**Summary**: A drag-end on a resizable panel triggers a single PUT within 1 s (debounced).
**Traces to**: AC-21
**Metric**: time from `mouseup` (drag-end) to outbound PUT; PUT count per drag.
**Profile**: fast — `quarantined` until Step 4 writer is added
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Drag and release a divider | event captured |
| 2 | Wait for PUT or 1 s timeout | dt (ms); count |
**Pass criteria**: exactly 1 PUT within ≤ 1 000 ms; URL = `/api/annotations/settings/user`; body contains `panelWidths` (`results_report.md` row 64).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 64.
---
### NFT-PERF-09: Settings save error surfaces within 2 s
**Summary**: A 500 on settings save produces an error toast and resets the `saving` flag within 2 s.
**Traces to**: AC-27
**Metric**: time from server 500 to error visibility + state reset.
**Profile**: fast — `quarantined` until Step 4 try/finally fix
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Trigger save, stub responds 500 after T ms | failure delivered |
| 2 | Wait for error toast and `saving === false` | dt (ms) |
**Pass criteria**: `dt ≤ 2 000 ms` (`results_report.md` row 68).
**Duration**: ≤ 5 s.
**Expected result source**: `results_report.md` row 68.
---
### NFT-PERF-10: First Contentful Paint on `/flights` ≤ 3 s on mid-range edge hardware
**Summary**: A warm-cache load of the default authenticated route renders the main pane within 3 s.
**Traces to**: AC-11 (target), H2 (edge deploys)
**Metric**: `performance.getEntriesByName('first-contentful-paint')[0].startTime`.
**Profile**: e2e — documentary (no enforcement today; no AC binds a hard FCP budget)
**Preconditions**:
- Warm browser cache (second visit).
- Edge-profile container: 2 vCPU, 4 GB RAM (the hardware-assessment phase confirms the figure).
**Steps**:
| Step | Consumer Action | Measurement |
|------|----------------|-------------|
| 1 | Navigate to `/flights` post-login | navigation completes |
| 2 | Read FCP entry | ms |
**Pass criteria**: FCP ≤ 3 000 ms (row 98 — threshold_max).
**Duration**: ≤ 30 s.
**Expected result source**: `results_report.md` row 98.
+244
View File
@@ -0,0 +1,244 @@
# Resilience Tests
Failure / recovery scenarios at the SPA's observable boundary: bearer expiry, refresh cookie loss, upstream 5xx, network partition, oversized uploads, SSE drop. Every fault injection is at the network / browser layer; the UI is observed for graceful behavior and recovery.
### NFT-RES-01: 401 → refresh → retry recovery is transparent
**Summary**: An authenticated request that returns 401 mid-session is refreshed and retried without unmounting the routed view.
**Traces to**: AC-01, AC-23
**Preconditions**:
- Active session on `/flights`.
**Fault injection**:
- Force a 401 on the next outbound authenticated request.
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Stub the next request to return 401 once | first response 401 |
| 2 | Observe the SPA's reaction | `POST /api/admin/auth/refresh` with `credentials:'include'`; on 200, original request retried with new bearer |
| 3 | Inspect `<ProtectedRoute>` children | not unmounted; re-render delta ≤ 1 |
**Pass criteria**: row 03 sequence; row 11 re-render bound (≤ 1); row 12 refresh count == 1 — all hold simultaneously.
**Expected result source**: `results_report.md` rows 03, 11, 12.
---
### NFT-RES-02: SSE bearer-rotation — both streams reconnect within 5 s
**Summary**: A bearer rotation during open SSE streams (live-GPS + annotation-status) tears them down and reopens them with the new token.
**Traces to**: AC-24
**Preconditions**:
- Two EventSources open.
**Fault injection**:
- Trigger a server-driven rotation of the bearer (force a refresh).
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Rotate bearer | new token in memory |
| 2 | Observe each EventSource | closes, then reopens with new `?token=` |
| 3 | Measure max reconnect time | ≤ 5 000 ms |
**Pass criteria**: both streams close+open exactly once; max reconnect ≤ 5 000 ms (`results_report.md` row 13).
**Status**: `quarantined` until SSE reconnect-on-rotation ships (Step 8 hardening).
**Expected result source**: `results_report.md` row 13.
---
### NFT-RES-03: Network offline at boot — error state, no offline mode
**Summary**: With network disabled, app boot results in a user-visible error state — NOT a service worker-served cached UI.
**Traces to**: AC-N3
**Preconditions**:
- Browser network disabled (or all `/api/*` stubs respond with offline error).
**Fault injection**:
- All outbound requests fail with `net::ERR_INTERNET_DISCONNECTED` (or equivalent).
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Load the SPA | static assets served by nginx OK; API calls fail |
| 2 | Observe DOM | login or general error surface present |
| 3 | Inspect `navigator.serviceWorker.controller` | `null` |
**Pass criteria**: row 93 — error/login-failed state present; no service worker controller.
**Expected result source**: `results_report.md` row 93.
---
### NFT-RES-04: ProtectedRoute loading timeout fallback after 10 s
**Summary**: The `<ProtectedRoute>` spinner has a bounded loading window; a stalled auth bootstrap surfaces a retry CTA / error.
**Traces to**: AC-17
**Preconditions**:
- Bootstrap refresh stubbed to never resolve.
**Fault injection**:
- `POST /api/admin/auth/refresh` hangs (no response).
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Mount `<ProtectedRoute>` | spinner rendered |
| 2 | Advance fake time to 10 s | timeout fires |
| 3 | Inspect DOM | retry CTA or error message present; spinner unmounted |
**Pass criteria**: row 59 — fallback present, spinner absent.
**Status**: `quarantined` until timeout fix lands (Step 4).
**Expected result source**: `results_report.md` row 59.
---
### NFT-RES-05: Settings save with upstream 500 — UI state recovers
**Summary**: A 500 on settings save surfaces an error and resets the `saving` flag.
**Traces to**: AC-27
**Preconditions**:
- Form filled in valid state on `/settings`.
**Fault injection**:
- Upstream `PUT /api/annotations/settings/system` returns 500 after T ms.
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Click Save | PUT issued |
| 2 | Stub responds 500 within 2 s | failure |
| 3 | Inspect within 2 s | toast / inline error; `saving === false`; no route navigation |
**Pass criteria**: row 68.
**Status**: `quarantined` until Step 4 try/finally fix.
**Expected result source**: `results_report.md` row 68.
---
### NFT-RES-06: Settings save with network drop — try/finally state reset
**Summary**: When the underlying fetch throws (network drop), `saving` resets and the user sees an error.
**Traces to**: AC-27
**Preconditions**:
- Form filled in valid state.
**Fault injection**:
- Network drop mid-PUT (`fetch` rejects).
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Click Save | PUT issued |
| 2 | Stub throws | rejection delivered |
| 3 | Inspect | `saving === false`; error surfaced |
**Pass criteria**: row 69.
**Status**: `quarantined`.
**Expected result source**: `results_report.md` row 69.
---
### NFT-RES-07: nginx 413 on oversized upload surfaces user-visible error
**Summary**: An upload that exceeds `client_max_body_size 500M` returns 413; the UI presents a user-facing message (no silent failure, no `alert()`).
**Traces to**: AC-10
**Preconditions**:
- Authenticated; `<MediaList>` open.
**Fault injection**:
- Drop a 501 MB synthetic file.
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Upload 501 MB | upload starts |
| 2 | nginx rejects | 413 delivered |
| 3 | Inspect UI | error containing the i18n "file too large" string; no `alert()` invoked |
**Pass criteria**: row 39.
**Expected result source**: `results_report.md` row 39.
---
### NFT-RES-08: Refresh cookie expired — redirect to /login
**Summary**: When the refresh cookie is gone (or expired) and a 401 occurs, the SPA redirects the user to `/login` rather than silently looping refresh.
**Traces to**: AC-01, AC-22
**Preconditions**:
- Cookie cleared from the browser jar.
**Fault injection**:
- 401 on any authenticated call; subsequent `POST /api/admin/auth/refresh` returns 401.
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Issue authenticated call | 401 |
| 2 | Refresh attempted | 401 (no cookie) |
| 3 | Observe routing | redirect to `/login` |
**Pass criteria**: final URL `/login`; no infinite refresh loop (single refresh attempt). Derived from AC-01 + AC-22; no specific results_report row binds the loop bound — Phase 3 flags this and the loop bound is added to row 03 if accepted.
---
### NFT-RES-09: Annotation download tainted-canvas fallback
**Summary**: When `<canvas>.toBlob()` raises a tainted-canvas exception (cross-origin video frame), the user sees an error rather than a silent no-op.
**Traces to**: NFR (`04_verification_log.md` finding on `handleDownload` tainted-canvas risk)
**Preconditions**:
- An annotation is loaded from a video sourced with CORS that taints the canvas.
**Fault injection**:
- Cross-origin video source taints the canvas; `toBlob` throws.
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Click Download | export attempted |
| 2 | toBlob throws | error handled |
| 3 | Inspect UI | user-visible error; no `alert()`; no silent swallow |
**Pass criteria**: row 96 — error surfaced; no silent swallow; no fabricated blob; no `alert()`.
**Expected result source**: `results_report.md` row 96.
---
### NFT-RES-10: SSE server disconnect — UI surfaces a connection-lost indicator
**Summary**: When the suite server closes a live-GPS or status-events SSE without rotation, the UI does NOT show stale data and DOES indicate the connection lost.
**Traces to**: AC-08, AC-09, AC-24
**Preconditions**:
- One SSE stream open.
**Fault injection**:
- Server force-closes the stream (no rotation).
**Steps**:
| Step | Action | Expected Behavior |
|------|--------|------------------|
| 1 | Drop the stream from the server | `error` fires on EventSource |
| 2 | Observe DOM | a connection-lost indicator is rendered (or stale-data badge) |
| 3 | Observe reconnect behavior | `EventSource` auto-retries per browser default; if the SPA re-creates it, exactly one new instance |
**Pass criteria**: row 97 — connection-lost indicator OR reconnect attempt within 10 s; stale data NOT rendered as live; reconnect attempts ≤ 1 in the 10 s window.
**Expected result source**: `results_report.md` row 97.
@@ -0,0 +1,213 @@
# Resource Limit Tests
The SPA's resource constraints are bundle size (initial JS), upload size cap (server), runtime image footprint (`nginx:alpine` only), and exclusion of the unbundled `mission-planner/` from production output. Long-session memory / CPU behavior is also covered here at a documentary level — no AC binds a hard runtime memory budget today.
### NFT-RES-LIM-01: Initial JS bundle ≤ 2 MB gzipped
**Traces to**: AC-11, O13
**Preconditions**:
- `bun run build` has produced `dist/`.
**Monitoring**:
- Sum of gzipped initial-route JS chunk sizes (computed from Vite's `manifest.json`).
**Duration**: ≤ 60 s (build + measurement).
**Pass criteria**: total gzipped initial JS ≤ 2 097 152 bytes (`results_report.md` row 40). Documentary today; CI gate target.
**Expected result source**: `results_report.md` row 40.
---
### NFT-RES-LIM-02: nginx `client_max_body_size 500M`
**Traces to**: AC-10, E9
**Preconditions**:
- `nginx.conf` present in the repo (and in the built image's `/etc/nginx/`).
**Monitoring**:
- Read the value of `client_max_body_size` from `nginx.conf`.
**Duration**: ≤ 5 s.
**Pass criteria**: value equals `500M` (`results_report.md` row 38).
**Expected result source**: `results_report.md` row 38.
---
### NFT-RES-LIM-03: Production image is `nginx:alpine` and carries no Node.js
**Traces to**: AC-33, S5, O11
**Preconditions**:
- Built image `azaion/ui:<tag>` available locally.
**Monitoring**:
- `docker inspect azaion/ui:<tag>` for the final stage's base image.
- Filesystem scan inside the image for a `node` binary.
**Duration**: ≤ 30 s.
**Pass criteria**: base image is `nginx:alpine`; no `node` binary present (`results_report.md` row 42).
**Expected result source**: `results_report.md` row 42.
---
### NFT-RES-LIM-04: `mission-planner/` is excluded from production bundle
**Traces to**: AC-31, O12, ADR-009
**Preconditions**:
- `bun run build` has produced `dist/`.
**Monitoring**:
- Vite's build report / manifest for chunk origins.
- Static-import graph analysis starting from `src/main.tsx` — verify no edges into `mission-planner/`.
**Duration**: ≤ 30 s.
**Pass criteria**: no `dist/**` file originates from `mission-planner/**`; the import graph from `src/main.tsx` does NOT reach `mission-planner/` (`results_report.md` row 41).
**Expected result source**: `results_report.md` row 41.
---
### NFT-RES-LIM-05: SPA memory stable across a 30-minute annotation session
**Traces to**: H2 (edge deploy), AC-09 (SSE)
**Status**: documentary — no AC binds a hard runtime memory budget today
**Preconditions**:
- E2E profile; user logged in on `/annotations`; annotation-status SSE open.
**Monitoring**:
- Headless Chromium `performance.memory.usedJSHeapSize` sampled every 60 s for 30 min.
**Duration**: 30 min.
**Pass criteria**: `usedJSHeapSize` does not grow by more than 50 % over the session under steady-state interaction (open/close media, page through dataset). A documentary baseline; if Phase 3 deems it un-anchored to an AC, it is downgraded to a metric-only run.
---
### NFT-RES-LIM-06: Live-GPS SSE 1-hour soak — no listener leak, no memory creep
**Traces to**: AC-08
**Status**: documentary
**Preconditions**:
- E2E profile; flight selected; live-GPS simulator emits at 1 Hz.
**Monitoring**:
- `EventSource` instance count sampled every 60 s — must stay at exactly 1.
- `usedJSHeapSize` sampled every 60 s.
**Duration**: 60 min.
**Pass criteria**: EventSource count stays at 1 throughout; heap grows by ≤ 30 % (documentary).
---
### NFT-RES-LIM-07: 100 sequential flight selections — no leaked SSEs, no leaked Contexts
**Traces to**: AC-08, P4
**Status**: documentary
**Preconditions**:
- E2E profile; ≥ 5 flights in seed.
**Monitoring**:
- Total EventSource instances created over the loop.
- Final open EventSource count (after deselect-then-reselect cycles end at "deselected").
**Duration**: ≤ 5 min.
**Pass criteria**: after the loop, open EventSource count is ≤ 1 (only the currently-selected stream if any). No more than `100 + 1 ` EventSources were created in total (one extra for any pre-test state). Documentary; Phase 3 to confirm or downgrade.
---
### NFT-RES-LIM-08: Edge-host RAM profile of the UI image at steady state
**Traces to**: H2
**Status**: documentary; hardware-assessment phase will pin the exact numbers
**Preconditions**:
- Production image running on the target edge profile (2 vCPU, 4 GB RAM).
**Monitoring**:
- `docker stats azaion-ui` sampled every 10 s for 5 min while a user is actively on `/annotations` with one open SSE.
**Duration**: 5 min.
**Pass criteria**: RSS of the nginx process under sustained traffic stays under 200 MB (documentary baseline; will be tightened or relaxed at hardware-assessment time).
---
### NFT-RES-LIM-09: nginx routes — exactly 9 location blocks for the suite services
**Traces to**: AC-34, E2
**Preconditions**:
- `nginx.conf` present.
**Monitoring**:
- Parse `nginx.conf` for `location` blocks under the main `server`.
**Duration**: ≤ 5 s.
**Pass criteria**: `location` block set equals `{/api/admin/, /api/flights/, /api/annotations/, /api/detect/, /api/loader/, /api/gps-denied-desktop/, /api/gps-denied-onboard/, /api/autopilot/, /api/resource/}` (`results_report.md` row 43).
**Expected result source**: `results_report.md` row 43.
---
### NFT-RES-LIM-10: nginx — each route strips its `/api/<service>/` prefix
**Traces to**: AC-34, E2
**Preconditions**:
- `nginx.conf` present.
**Monitoring**:
- Per `location` block, inspect the `proxy_pass` / `rewrite` directive shape — verify the prefix is stripped before forwarding upstream.
**Duration**: ≤ 5 s.
**Pass criteria**: every block satisfies the strip-prefix regex (per-block check, `results_report.md` row 44).
**Expected result source**: `results_report.md` row 44.
---
### NFT-RES-LIM-11: CI image tag scheme is `${branch}-arm`
**Traces to**: AC-32, E7
**Preconditions**:
- `.woodpecker/build-arm.yml` present.
**Monitoring**:
- Parse the push step's `tag` field for branches `dev`, `stage`, `main`.
**Duration**: ≤ 5 s.
**Pass criteria**: pushed tag for branch `main` matches `^main-arm$` (`results_report.md` row 70). Same regex shape for `dev` and `stage` (derived).
**Expected result source**: `results_report.md` row 70.
---
### NFT-RES-LIM-12: OCI labels present on the pushed image
**Traces to**: AC-32, E6
**Preconditions**:
- `.woodpecker/build-arm.yml` present.
**Monitoring**:
- Parse the push step's label declarations — count and presence.
**Duration**: ≤ 5 s.
**Pass criteria**: labels `org.opencontainers.image.revision`, `org.opencontainers.image.created`, `org.opencontainers.image.source` are all declared and non-empty; total label count == 3 (`results_report.md` row 71).
**Expected result source**: `results_report.md` row 71.
---
### NFT-RES-LIM-13: Revision label equals `$CI_COMMIT_SHA`
**Traces to**: AC-32, E5
**Preconditions**:
- `.woodpecker/build-arm.yml` present.
**Monitoring**:
- Parse the label value template for `org.opencontainers.image.revision`.
**Duration**: ≤ 5 s.
**Pass criteria**: label value template equals `$CI_COMMIT_SHA` (or the pipeline's documented equivalent) (`results_report.md` row 72).
**Expected result source**: `results_report.md` row 72.
+246
View File
@@ -0,0 +1,246 @@
# Security Tests
Blackbox security assertions against the SPA's observable surface: token storage discipline, refresh cookie attributes, RBAC route gating, credentials flag, secrets-in-source checks, destructive-action policy, dependency hygiene. These complement the server's RBAC and the suite's security_approach (`_docs/00_problem/security_approach.md`); they do NOT replace server-side enforcement (O4).
### NFT-SEC-01: Bearer is never written to `localStorage` or `sessionStorage`
**Traces to**: AC-02, O2
**Profile**: static + e2e
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Static code-search on `src/` and `mission-planner/src/` for `localStorage.|sessionStorage.` near `bearer|token|accessToken` | `match_count == 0` |
| 2 | E2E: complete a login; inspect `localStorage` and `sessionStorage` keys | neither contains the bearer value |
**Pass criteria**: row 04 — `match_count == 0`; runtime storage does not contain the bearer.
**Expected result source**: `results_report.md` row 04.
---
### NFT-SEC-02: `document.cookie` does not expose the refresh token
**Traces to**: AC-03
**Profile**: e2e + static
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Static code-search for `document.cookie` reads against `refreshToken|refresh-cookie` | `match_count == 0` (row 05) |
| 2 | E2E: complete login; read `document.cookie` from page context | returned string does NOT contain the refresh-token value (row 06) |
**Pass criteria**: rows 05 + 06.
**Expected result source**: `results_report.md` rows 05, 06.
---
### NFT-SEC-03: Refresh cookie attributes — `Secure`, `HttpOnly`, `SameSite=Strict`
**Traces to**: AC-03, E3, O5
**Profile**: e2e
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Login via `POST /api/admin/auth/login` against the suite stack | `Set-Cookie` header returned |
| 2 | Inspect header value | matches regex `Secure;.*HttpOnly;.*SameSite=Strict` (case-insensitive, attribute-order-tolerant) |
**Pass criteria**: row 07 — regex match.
**Notes**: this is a server-contract assertion; the UI test exists as defence-in-depth so a suite regression is caught before it lands in production.
**Expected result source**: `results_report.md` row 07.
---
### NFT-SEC-04: `credentials: 'include'` is set on every authenticated fetch
**Traces to**: AC-01, O3
**Profile**: fast (apiClient wrapper) + e2e (live capture)
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Issue an authenticated request via `apiClient` | RequestInit captured |
| 2 | Inspect | `credentials === 'include'` (row 01) |
| 3 | Repeat for the bootstrap refresh | same (row 02 — `quarantined` until Step 4 bootstrap fix) |
**Pass criteria**: rows 01 + 02.
**Expected result source**: `results_report.md` rows 01, 02.
---
### NFT-SEC-05: `/admin` route blocks non-admins client-side (defence in depth)
**Traces to**: AC-22
**Profile**: e2e — `quarantined` until role-gate is added (Step 4 / Step 8)
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Log in as `op_alice` (Operator, no admin role) | session active |
| 2 | Navigate to `/admin` | URL changes |
| 3 | Inspect final URL + DOM | URL is `/flights`; `<AdminPage>` NOT mounted |
**Pass criteria**: row 08.
**Notes**: server-side RBAC is authoritative; the UI gate is a usability + leakage layer.
**Expected result source**: `results_report.md` row 08.
---
### NFT-SEC-06: `/settings` route gate is applied per RBAC
**Traces to**: AC-22
**Profile**: e2e — `quarantined`
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Log in as user without SETTINGS permission | session active |
| 2 | Navigate to `/settings` | URL changes |
| 3 | Inspect | URL is `/flights`; `<SettingsPage>` NOT mounted |
**Pass criteria**: row 10.
**Expected result source**: `results_report.md` row 10.
---
### NFT-SEC-07: `alert()` is forbidden anywhere in the SPA
**Traces to**: AC-14, O10
**Profile**: static
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Regex sweep `src/` and `mission-planner/src/` for `\balert\(` | `match_count == 0` |
**Pass criteria**: row 50.
**Expected result source**: `results_report.md` row 50.
---
### NFT-SEC-08: ConfirmDialog gates every destructive action
**Traces to**: AC-14, AC-30, O10
**Profile**: fast
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | For each destructive surface in `_docs/ui_design/` (class delete, user deactivate, dataset bulk-overwrite, etc.) | sequence checked |
| 2 | Confirm sequence on click → before any HTTP fires | dialog present (row 51) |
| 3 | On Confirm in class-delete flow → exactly one DELETE to `^/api/admin/classes/[0-9]+$` | (row 49) |
**Pass criteria**: rows 49 + 51.
**Expected result source**: `results_report.md` rows 49, 51.
---
### NFT-SEC-09: OpenWeatherMap API key is not shipped in source or bundle
**Traces to**: AC-20, P10
**Profile**: static (source) + static (bundle)
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Regex sweep `src/` and `mission-planner/src/` for the literal current OWM key value | `match_count == 0` (row 63) |
| 2 | Regex sweep for `appid=` and `api_key=` literal occurrences in source URLs | `match_count == 0` (row 63) |
| 3 | Scan `dist/**/*.js` post-build for the literal key | `match_count == 0` (Phase 3 may downgrade to "until Step 4 fix") |
**Pass criteria**: row 63.
**Status**: `quarantined` for source check until Step 4 fix; the bundle-scan check passes immediately for `src/` (mission-planner not bundled, AC-31).
**Expected result source**: `results_report.md` row 63.
---
### NFT-SEC-10: No in-browser ML libs
**Traces to**: AC-N2
**Profile**: static
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Parse `package.json` and `mission-planner/package.json` dependencies | dependency lists |
| 2 | Match against `^(onnxruntime|@?tensorflow(?:js)?(?:/.*)?|tflite|coreml|tfjs|@huggingface/.*|transformers\.js)$` | zero matches |
**Pass criteria**: row 92.
**Expected result source**: `results_report.md` row 92.
---
### NFT-SEC-11: No response-signature / JOSE libs on the request path
**Traces to**: AC-N4
**Profile**: static
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Parse `package.json` dependencies | list |
| 2 | Match against `^(jsrsasign|tweetnacl|@noble/.*|jose)$` | zero matches |
**Pass criteria**: row 94.
**Expected result source**: `results_report.md` row 94.
---
### NFT-SEC-12: No service worker — offline mode is explicitly absent
**Traces to**: AC-N3
**Profile**: e2e
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Load the SPA in a fresh browser context | app boots |
| 2 | Read `navigator.serviceWorker.getRegistrations()` | empty array |
**Pass criteria**: row 93 — no service worker registered.
**Expected result source**: `results_report.md` row 93.
---
### NFT-SEC-13: Dropped legacy features are not present in source
**Traces to**: AC-N5
**Profile**: static
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Regex sweep `src/` and `mission-planner/src/` for `SoundDetections|DroneMaintenance` | `match_count == 0` |
**Pass criteria**: row 95.
**Expected result source**: `results_report.md` row 95.
---
### NFT-SEC-14: Anti-criterion AC-N1 — no concurrent-edit reconciliation surfaces
**Traces to**: AC-N1
**Profile**: e2e + static
**Steps**:
| Step | Consumer Action | Expected Response |
|------|----------------|------------------|
| 1 | Open the same annotation in two browser sessions; edit both | both save individually |
| 2 | Inspect each session's DOM | no merge UI; no presence indicator |
**Pass criteria**: row 91.
**Notes**: this is an anti-criterion — the test enforces that the feature is NOT silently added.
**Expected result source**: `results_report.md` row 91.
+100
View File
@@ -0,0 +1,100 @@
# Test Data Management
The Azaion UI is a thin client over a typed REST + SSE contract — it carries no database of its own (P3 / P4 / `data_parameters.md`). "Test data" therefore means three things:
1. **Suite-side seed fixtures** loaded into the `e2e` profile's docker-compose stack (users, flights, aircraft, classes, sample media).
2. **Stub responses** mounted on the `fast` profile's request-interception layer (canned `/api/<service>/*` payloads, canned OpenWeatherMap and OSM tile responses).
3. **Source-level fixtures** committed in this repo: i18n bundles, enum spec snapshots, `nginx.conf`, `vite.config.ts`, `package.json`, and the `dist/` build output.
There are NO image / video / `.tlog` input files under `_docs/00_problem/input_data/` for this project — the cell `data_parameters.md` and `results_report.md` already make this explicit. Every observable assertion lives in `_docs/00_problem/input_data/expected_results/results_report.md` (95 rows) which the tests reference verbatim.
## Seed Data Sets
| Data Set | Description | Used by Tests | How Loaded | Cleanup |
|----------|-------------|---------------|-----------|---------|
| `seed_users` | 4 users: `op_alice` (Operator), `op_bob` (Operator, no SETTINGS permission), `admin_carol` (Admin), `integrator_dave` (System Integrator). All with known passwords for test login. | All `e2e` tests that authenticate | `admin/` service init script reads `seed-fixtures/users.json` at container start | `docker compose down -v` wipes `test-db-data` between e2e suite runs |
| `seed_aircraft` | 3 aircraft with one marked `isDefault: true` | E2E tests touching `/flights`, `/admin` aircraft tab | `flights/` service init script | as above |
| `seed_flights` | 5 flights spanning the 4 users; some with waypoints, one with `LiveGpsEvent` simulator wired | E2E tests touching `/flights` and the live-GPS stream | `flights/` service init script | as above |
| `seed_classes` | The contract's `[0..N-1, 20..20+N-1, 40..40+N-1]` ordering (per AC-37 / `data_model.md:158`) — N≥9 so number-key hotkeys 1..9 are all hot. | `<DetectionClasses>` component + `<AdminPage>` class CRUD tests | `annotations/` (read path) + `admin/` (write path) init scripts | as above |
| `seed_media` | 6 media items (3 images, 3 videos) attached to `seed_flights`, with mediaStatus values exercising the full enum after AC-04 fix lands (`None`, `New`, `AiProcessing`, `AiProcessed`, `ManualCreated`, `Confirmed`, `Error`) | E2E tests touching `/annotations` and `/dataset` | `annotations/` init script | as above |
| `seed_annotations` | Annotations for `seed_media`, including: some with `Source: AI`, some `Manual`; one with `isSplit: true` and a valid `splitTile` "3 0.5 0.5 0.2 0.2" (AC-39); one with malformed `splitTile` ("garbage", AC-39 sad path); status values spanning the full `AnnotationStatus` enum after AC-04 fix (`None=0`, `Created=10`, `Edited=20`, `Validated=30`, `Deleted=40`) | E2E tests for `<AnnotationsPage>`, `<CanvasEditor>`, `<DatasetPage>` | `annotations/` init script | as above |
| `seed_user_settings` | Known `selectedFlightId` and `panelWidths` for `op_alice` so the rehydration tests assert against a deterministic state | AC-21, AC-06 tests | `annotations/` init script | as above |
| `enum_spec_snapshot` | A committed JSON file `_docs/00_problem/input_data/enum_spec_snapshot.json` that pins the contract values for `AnnotationStatus`, `MediaStatus`, `Affiliation`, `CombatReadiness`, `MediaType`, `AnnotationSource`, `WaypointSource`, `WaypointObjective`. Populated from `../_docs/00_database_schema.md` (authoritative) with the UI's current drift documented in `ui_drift_summary`. `CombatReadiness` + `MediaType` are flagged `verification_pending: true` because the schema does not pin numeric values; Step 4 .NET-service inspection finalizes them. | Static checks for AC-04 / AC-29 (Group 2 rows 14-19 of `results_report.md`) | Read directly from the repo at test time. | n/a (committed at Phase 3). |
| `bundle_artifact` | The `dist/` produced by `vite build`. | AC-11, AC-31, AC-33 (Group 7 rows 40-44) | Built by the CI step before tests run. | Ephemeral per CI run. |
## Data Isolation Strategy
- **`fast` profile**: each test creates its own MSW handlers and tears them down in the test runner's `afterEach`. No process-wide state. React tree is unmounted between tests. Test files MAY share an enum spec snapshot (read-only) since it is a contract pin.
- **`e2e` profile**: each `e2e` run gets a fresh suite docker-compose stack (`docker compose down -v` then `up -d`) before the suite executes. Inside a run, tests group into **isolation buckets** by data set; a bucket runs sequentially against a known seed state, then the seeds are reset between buckets via `admin/` service's `POST /test-only/reset` (a test-only endpoint, gated behind a non-production build flag). Buckets across different machines run on independent compose stacks.
- **Per-user isolation**: tests that mutate user-scoped state (e.g., `selectedFlightId`, panel widths) use distinct seed users so concurrent tests within a bucket cannot race on the same `UserSettings` row.
- **No cross-test order dependencies** — any test must be re-runnable in isolation by booting the bucket's seed snapshot.
## Input Data Mapping
The UI has no consumer-side input files (images, videos, `.tlog`s). Every test "input" is a **trigger** classified per `test-spec/SKILL.md` "Behavioral shape" — a user action, a request the SPA initiates, an SSE event, or a static check against the repo / `dist/`. The `Input` column of each test in `blackbox-tests.md` / etc. references a `results_report.md` row (which already defines the trigger and the quantifiable observable).
| Input Reference | Source Location | Description | Covers Scenarios |
|-----------------|----------------|-------------|-----------------|
| `results_report.md` rows 1-13 | `_docs/00_problem/input_data/expected_results/results_report.md` | Auth & token-handling triggers | Group 1 tests (FT-P-01..FT-P-13 / FT-N-* per blackbox-tests.md) |
| `results_report.md` rows 14-21 | as above | Wire-contract / enum compliance triggers | Group 2 tests |
| `results_report.md` rows 22-31 | as above | Annotations endpoint / payload / SSE / overlay-window triggers | Group 3 tests |
| `results_report.md` rows 32-35 | as above | Flight selection persistence + live-GPS SSE triggers | Group 4 tests |
| `results_report.md` rows 36-37 | as above | Dataset bulk-validate triggers | Group 5 tests |
| `results_report.md` rows 38-39 | as above | Upload size cap triggers | Group 6 tests |
| `results_report.md` rows 40-44 | as above | Build / bundle / routing triggers | Group 7 tests |
| `results_report.md` rows 45-48 | as above | i18n triggers | Group 8 tests |
| `results_report.md` rows 49-51 | as above | Destructive-UX triggers | Group 9 tests |
| `results_report.md` rows 52-59 | as above | A11y triggers | Group 10 tests |
| `results_report.md` rows 60-62 | as above | Browser-support + responsive triggers | Group 11 tests |
| `results_report.md` row 63 | as above | OWM secrets check | Group 12 test |
| `results_report.md` rows 64-65 | as above | User-settings persistence triggers | Group 13 tests |
| `results_report.md` rows 66-69 | as above | Form hygiene triggers | Group 14 tests |
| `results_report.md` rows 70-72 | as above | CI / image / labels triggers | Group 15 tests |
| `results_report.md` rows 73-84 | as above | Canvas + DetectionClasses + PhotoMode triggers | Group 16 tests |
| `results_report.md` rows 85-90 | as above | Tile splitting + tile-zoom triggers | Group 17 tests |
| `results_report.md` rows 91-95 | as above | Anti-criteria triggers | Group 18 tests |
## Expected Results Mapping
Every test in `blackbox-tests.md`, `performance-tests.md`, `resilience-tests.md`, `security-tests.md`, and `resource-limit-tests.md` carries an `Expected result source: results_report.md row <N>` line in its body. The comparison method, tolerance, and reference file (if any) are inherited from that row — tests do not re-state them. The `traceability-matrix.md` aggregates: AC → results_report row(s) → test ID(s).
| Test Scenario ID | Input Data | Expected Result | Comparison Method | Tolerance | Expected Result Source |
|-----------------|------------|-----------------|-------------------|-----------|----------------------|
| (see `blackbox-tests.md` etc.) | (row reference) | (row's Expected Result column) | (row's Comparison column) | (row's Tolerance column) | `results_report.md` row N |
No reference files (e.g. JSON / CSV) are required at this stage — every observable in `results_report.md` is small enough to fit inline. If a downstream test needs a multi-row expected payload (e.g. the suite's full class-distribution response shape), the file will be added in `_docs/00_problem/input_data/expected_results/` per the naming convention `<input_name>_expected.<ext>` and the row updated to reference it. The Phase 3 Data Validation Gate will surface this if it happens.
## External Dependency Mocks
| External Service | Mock/Stub | How Provided | Behavior |
|-----------------|-----------|-------------|----------|
| OpenWeatherMap (E10) | `owm-stub` HTTP service (e2e) / MSW handler (fast) | Docker service `owm-stub:8081` (e2e) / per-test MSW handler (fast) | Returns canned `/data/2.5/onecall` JSON with deterministic wind/precip values. Tests that exercise wind compute (`flightPlanUtils.ts` once moved into `src/`) assert against these canned values. |
| OSM tile servers | `tile-stub` HTTP service (e2e) / never hit in fast | Docker service `tile-stub:8082` returning a fixed 256x256 PNG | Replaces `a.tile.openstreetmap.org` etc.; map tile loads do not depend on internet. |
| Suite microservices (`admin/`, `flights/`, `annotations/`, `detect/`, ...) | Real services (e2e) / MSW handlers (fast) | docker-compose (e2e) / per-test handler (fast) | Production-shape responses per `data_parameters.md` §1. Errors injected per resilience tests (5xx, 413, network drop). |
| GPS-Denied services (target) | `gps-denied-desktop` + `gps-denied-onboard` real services (e2e); no fast coverage today | docker-compose | F12 Test Mode is target-only — tests for it are quarantined until the feature lands (see Open Items in `results_report.md`). |
| LiveGPS simulator | Embedded in `flights/` service test mode | docker-compose | Emits deterministic `LiveGpsEvent` payloads at 1 Hz on `/api/flights/<id>/live-gps` so AC-08 timing assertions are stable. |
| Annotation-status events generator | Embedded in `annotations/` service test mode | docker-compose | Allows tests to trigger `AnnotationStatusEvent` SSE deliveries on demand. |
All mocks are deterministic: same input always yields the same output. Non-determinism (timestamps, IDs assigned by the suite) is bounded to suite-managed fields that tests do not compare directly — tests use field presence + shape, not value equality, for those fields.
## Data Validation Rules
| Data Type | Validation | Invalid Examples | Expected System Behavior |
|-----------|-----------|-----------------|------------------------|
| Bearer (Authorization header) | Non-empty string starting `Bearer ` | empty header, missing header | `01_api-transport` adds it automatically for authenticated requests; absence triggers a refresh attempt via 401-retry. |
| Refresh cookie (response Set-Cookie) | `Secure; HttpOnly; SameSite=Strict` per AC-03 | Missing `HttpOnly` | Test rejects the response (regression — server contract violation). Documented in the suite, asserted by the UI test for defence-in-depth. |
| Annotation save body | `{Source, WaypointId, videoTime, mediaId, detections, status}` keys all present (AC-05 / row 23) | missing `Source`, contains `time` instead of `videoTime` | Test FAILS — finding #32 fix regression. |
| Waypoint POST body | `{Geopoint:{Lat,Lon,MGRS}, Source, Objective, OrderNum, Height}` shape per `data_parameters.md` (Step 4 fix candidate) | UI's current `{name, latitude, longitude, order}` | Test FAILS once the Step 4 fix lands; pre-fix the test is quarantined (documents the contract drift). |
| Enum numeric value on the wire | Member of the spec value set (AC-04 / rows 14-19) | `status: 1` for `Edited` (UI today) instead of `20` (spec) | Test FAILS — exactly the regression that motivated Step 4 AC-04. |
| `splitTile` YOLO label | 5 space-separated numeric tokens (AC-39 / row 86) | `"garbage"` (row 87) | Parser surfaces a user-visible error; does NOT silently swallow. Test asserts the error path. |
| Numeric form input | Non-empty, parseable, in range (AC-26 / rows 66-67) | empty string, non-numeric, out-of-range | Validation error shown; no PUT fires. |
| File upload | ≤ 500 MB (E9 / AC-10 / row 39) | 501 MB | HTTP 413; UI surfaces a user-visible i18n error; no `alert()`. |
| i18n key set | `keys(en.json) == keys(ua.json)` (AC-12 / row 45) | UA missing a key present in EN | Static check FAILS. |
## Open Items For Phase 3 Validation
- **Enum spec numeric values** (AC-04): **resolved at Phase 3.** Snapshot committed at `_docs/00_problem/input_data/enum_spec_snapshot.json` per `../_docs/00_database_schema.md` (authoritative). `AnnotationStatus`, `MediaStatus`, `Affiliation`, `AnnotationSource` have pinned numerics from the schema. `CombatReadiness` + `MediaType` carry a `verification_pending: true` flag because the schema doesn't pin numerics on those — Step 4 .NET-service inspection lifts the flag (or reorders if the inferred sequential mapping is wrong). UI drift (5 enums) is pinned in the snapshot's `ui_drift_summary` for Step 4 to consume.
- **Phase B / target ACs** (AC-11 bundle gate, AC-18 browser-list, AC-24 SSE refresh, AC-25 async-video path, AC-40 tile-zoom UX): the rows are written but the consumer-side behavior does not exist today. Phase 3 will surface these to the user for: (a) downgrade to documentary, (b) quarantine, or (c) accept as gating the day the feature lands.
- **Waypoint POST shape** (`data_parameters.md` finding #20): the contract pin is the suite spec, but the UI's current shape is wrong. Tests assert against the contract. Pre-fix the test is quarantined; the Step 4 fix lands together with un-quarantining.
- **AC-37 backend ordering**: the class-hotkey contract depends on the `annotations/` service returning classes in `[0..N-1, 20..20+N-1, 40..40+N-1]`. If the seed reveals a different shape, AC-37 row 79 will fail; the fix may need to land server-side or the UI may need a client-side resort. Phase 3 will surface this gap.
- **No `Reference File` rows are needed today** — every `Reference File` cell in `results_report.md` is `N/A`. If Phase 2 reveals a need (e.g. for a complex SSE payload sequence), the reference file lands in `_docs/00_problem/input_data/expected_results/` and the row is updated.
@@ -0,0 +1,165 @@
# Traceability Matrix
Maps every acceptance criterion and every restriction in `_docs/00_problem/` to the test scenarios that verify it (this directory) and the expected-result row in `_docs/00_problem/input_data/expected_results/results_report.md` that provides the quantifiable observable. Quarantined tests are marked `[Q]` — they assert against a Phase B target feature or a Step 4 fix that has not landed; they activate the day the implementation ships.
## Acceptance Criteria Coverage
| AC ID | Acceptance Criterion (short) | Tests | results_report rows | Coverage |
|-------|------------------------------|-------|---------------------|----------|
| AC-01 | `credentials:'include'` on every authenticated fetch | FT-P-01 [Q for bootstrap], FT-P-02, NFT-PERF-02, NFT-SEC-04, NFT-RES-01, NFT-RES-08 | 01, 02, 03 | Covered |
| AC-02 | Bearer never written to client storage | NFT-SEC-01 | 04 | Covered |
| AC-03 | Refresh cookie `Secure HttpOnly SameSite=Strict` | NFT-SEC-02, NFT-SEC-03 | 05, 06, 07 | Covered |
| AC-04 | Numeric enums match suite spec | FT-P-04, FT-P-05, FT-P-06 | 14, 15, 16, 17, 18, 19 | Covered (`enum_spec_snapshot.json` committed — Phase 3 gate resolved) |
| AC-05 | Annotation save endpoint + required body fields | FT-P-07, FT-P-08 | 22, 23 | Covered |
| AC-06 | Selected-flight persistence path | FT-P-16, FT-P-17 | 32, 33 | Covered |
| AC-07 | Bulk-validate works | FT-P-20, FT-P-21, NFT-PERF-07 | 36, 37 | Covered |
| AC-08 | Live-GPS SSE per flight | FT-P-18, FT-P-19, NFT-PERF-04, NFT-PERF-05, NFT-RES-10, NFT-RES-LIM-06, NFT-RES-LIM-07 | 34, 35, 97 | Covered |
| AC-09 | Annotation-status SSE during page lifetime | FT-P-09, FT-P-10, NFT-PERF-06, NFT-RES-LIM-05 | 24, 25, 97 | Covered |
| AC-10 | Upload size cap 500 MB + UI error path | FT-N-06, NFT-RES-07, NFT-RES-LIM-02 | 38, 39 | Covered |
| AC-11 | Initial JS bundle ≤ 2 MB | NFT-PERF-01, NFT-RES-LIM-01 | 40 | Covered (documentary — no CI gate today) |
| AC-12 | i18n key parity en ↔ ua | FT-P-22, FT-P-23 | 45, 46 | Covered |
| AC-13 | i18n detector + persistence | FT-P-24 [Q], FT-P-25 [Q] | 47, 48 | Covered (quarantined — Step 4 fix) |
| AC-14 | Destructive actions require ConfirmDialog + alert() forbidden | FT-P-26, FT-P-27, FT-N-07, NFT-SEC-07, NFT-SEC-08 | 49, 50, 51 | Covered |
| AC-15 | ConfirmDialog a11y | FT-P-28, FT-P-29, FT-N-08 | 52, 53, 54 | Covered |
| AC-16 | Header flight dropdown a11y | FT-P-30, FT-P-31, FT-N-09 | 55, 56, 57 | Covered |
| AC-17 | ProtectedRoute spinner a11y + timeout | FT-P-32, FT-P-33 [Q], NFT-RES-04 [Q] | 58, 59 | Covered (quarantined for timeout) |
| AC-18 | Browser support — Chromium + Firefox latest 2 | FT-P-34, NFT-PERF-10 | 60, 98 | Covered (manual smoke, no automated gate today) |
| AC-19 | Mobile / desktop breakpoint variants | FT-P-35, FT-P-36 | 61, 62 | Covered |
| AC-20 | OpenWeatherMap key not in source | NFT-SEC-09 [Q for source until Step 4] | 63 | Covered (quarantined for source check) |
| AC-21 | UserSettings panel-width persistence | FT-P-37 [Q], FT-P-38 [Q], NFT-PERF-08 [Q] | 64, 65 | Covered (quarantined) |
| AC-22 | RBAC client-side route gates | FT-N-03 [Q], FT-N-04, FT-N-05 [Q], NFT-SEC-05 [Q], NFT-SEC-06 [Q], NFT-RES-08 | 08, 09, 10 | Covered (quarantined for `/admin` + `/settings` gates) |
| AC-23 | Auth refresh transparency | FT-P-02, FT-P-03, NFT-PERF-02, NFT-RES-01 | 11, 12 | Covered |
| AC-24 | SSE bearer-rotation reconnect | NFT-PERF-03 [Q], NFT-RES-02 [Q], NFT-RES-10 | 13, 97 | Covered (quarantined — Step 8 hardening) |
| AC-25 | Detect endpoint correctness (sync + async) | FT-P-11, FT-P-12 [Q], FT-P-13 [Q] | 26, 27, 28 | Covered (async path quarantined — F7 target) |
| AC-26 | Numeric input hygiene | FT-N-11 [Q], FT-N-12 [Q] | 66, 67 | Covered (quarantined — Step 4 fix) |
| AC-27 | Save error surfacing in settings | FT-N-13 [Q], FT-N-14 [Q], NFT-PERF-09 [Q], NFT-RES-05 [Q], NFT-RES-06 [Q] | 68, 69 | Covered (quarantined — Step 4 fix) |
| AC-28 | Annotation overlay time window `[-50, +150] ms` | FT-P-14, FT-P-15, FT-N-01, FT-N-02 | 29, 30, 31 | Covered |
| AC-29 | `mediaType` is typed (no magic literals) | FT-N-15 | 20, 21 | Covered |
| AC-30 | Class delete confirmation | FT-P-26, FT-N-07, NFT-SEC-08 | 49 | Covered (overlaps AC-14 row 49) |
| AC-31 | `mission-planner/` not in production bundle | NFT-RES-LIM-04 | 41 | Covered |
| AC-32 | CI image tag + OCI labels | NFT-RES-LIM-11, NFT-RES-LIM-12, NFT-RES-LIM-13 | 70, 71, 72 | Covered |
| AC-33 | Production runtime `nginx:alpine` only | NFT-RES-LIM-03 | 42 | Covered |
| AC-34 | nginx routes 9 services with prefix stripping | NFT-RES-LIM-09, NFT-RES-LIM-10 | 43, 44 | Covered |
| AC-35 | Manual bbox draw on CanvasEditor | FT-P-39 | 73 | Covered |
| AC-36 | 8-handle resize + Ctrl-multi-select + Ctrl-wheel zoom + Ctrl-drag pan | FT-P-40, FT-P-41, FT-P-42, FT-P-43 | 74, 75, 76, 77 | Covered |
| AC-37 | Class picker — load + hotkey + click + fallback | FT-P-44, FT-P-45, FT-P-46, FT-P-47 | 78, 79, 80, 81 | Covered (pending backend ordering check — Phase 3 gate) |
| AC-38 | PhotoMode switcher | FT-P-48, FT-P-49, FT-P-50 | 82, 83, 84 | Covered |
| AC-39 | Tile-splitting endpoint + parser | FT-P-51 [Q], FT-P-52, FT-P-53, FT-N-10 | 85, 86, 87, 88 | Covered (split surface quarantined) |
| AC-40 | Tile-zoom auto-zoom + indicator | FT-P-54 [Q], FT-P-55 [Q] | 89, 90 | Covered (quarantined — UX missing today) |
| AC-N1 | No collaborative-edit semantics | NFT-SEC-14 | 91 | Covered |
| AC-N2 | No in-browser ML | NFT-SEC-10 | 92 | Covered |
| AC-N3 | No offline mode | NFT-RES-03, NFT-SEC-12 | 93 | Covered |
| AC-N4 | No response-signature library | NFT-SEC-11 | 94 | Covered |
| AC-N5 | Dropped legacy features (Sound Detections, Drone Maintenance) | NFT-SEC-13 | 95 | Covered |
## Restrictions Coverage
| RID | Restriction (short) | Tests / Mechanism | Coverage |
|-----|---------------------|-------------------|----------|
| H1 | ARM64-only production image | NFT-RES-LIM-03 (image base check); the build pipeline produces ARM64 only — meta-config | Covered |
| H2 | Edge-device deployment target | NFT-PERF-10, NFT-RES-LIM-08 | Covered (documentary) |
| H3 | No GPU expectation in UI image | environment.md (no GPU in test rig); NFT-SEC-10 (no ML libs) — together they verify the constraint | Covered (by composition) |
| H4 | HTML5 video + canvas + EventSource on Chromium / Firefox latest 2 | FT-P-34 | Covered |
| S1 | TypeScript strict mode | STC-S1: static read of `tsconfig.json` for `"strict": true` (planned static check — added by Phase 3 if accepted) | NOT COVERED — Phase 3 to add |
| S2 | React 19 | STC-S2: `package.json` dep version pin | NOT COVERED — Phase 3 to add |
| S3 | Vite 6 | STC-S3: `package.json` dep version pin | NOT COVERED — Phase 3 to add |
| S4 | Bun 1.3.11 | STC-S4: `package.json` `packageManager` field + Dockerfile base image pin | NOT COVERED — Phase 3 to add |
| S5 | Static-bundle output only (no Node in prod image) | NFT-RES-LIM-03 | Covered |
| S6 | REST + SSE only (no WebSocket / GraphQL / gRPC-Web) | STC-S6: dep scan for `ws`, `socket.io`, `graphql`, `apollo`, `grpc-web` (planned) | NOT COVERED — Phase 3 to add |
| S7 | Two React Contexts only (no Redux / Zustand / TanStack Query) | STC-S7: dep scan for `redux`, `zustand`, `@reduxjs/.*`, `@tanstack/.*` | NOT COVERED — Phase 3 to add |
| S8 | Tailwind 4 + `az-*` design tokens | STC-S8: dep version pin + presence of token CSS vars | NOT COVERED — Phase 3 to add |
| S9 | `leaflet@1.9.4` + `react-leaflet@5` + `leaflet-draw` + `leaflet-polylinedecorator` | STC-S9: `package.json` version pin set | NOT COVERED — Phase 3 to add |
| S10 | `chart.js@4` + `react-chartjs-2@4` | STC-S10 | NOT COVERED — Phase 3 to add |
| S11 | `@hello-pangea/dnd@18` | STC-S11 | NOT COVERED — Phase 3 to add |
| S12 | `i18next` + `react-i18next` with EN + UA bundles only | FT-P-22, FT-P-23 + STC-S12 (no other locale bundle files) | Partially Covered |
| S13 | No client-side persistence library | NFT-SEC-01 + STC-S13 (dep scan for `localforage`, `idb`, `dexie`) | Partially Covered |
| S14 | No test framework configured today | META — these tests' very existence supersedes this restriction; resolved at Step 5 (Decompose Tests) per `acceptance_criteria.md` AC-14 / S14 note | N/A — meta |
| E1 | Air-gap-friendly bundle | NFT-RES-03 (offline boot) + external-dep stubs in environment.md | Partially Covered |
| E2 | nginx strips `/api/<service>/` per service | NFT-RES-LIM-09, NFT-RES-LIM-10 | Covered |
| E3 | `Secure HttpOnly SameSite=Strict` refresh cookie | NFT-SEC-03 | Covered |
| E4 | Vite dev proxy at `/api → http://localhost:8080` | dev-only; not testable in production runtime | NOT COVERED — meta-config (Phase 3 to confirm) |
| E5 | `AZAION_REVISION` stamped at build | NFT-RES-LIM-13 | Covered |
| E6 | OCI image labels | NFT-RES-LIM-12 | Covered |
| E7 | Image registry + tag scheme | NFT-RES-LIM-11 | Covered |
| E8 | Branch triggers (`dev` / `stage` / `main`) | STC-E8: parse `.woodpecker/build-arm.yml` triggers (planned) | NOT COVERED — Phase 3 to add |
| E9 | `client_max_body_size 500M` | NFT-RES-LIM-02 | Covered |
| E10 | OpenWeatherMap direct-from-browser today | NFT-SEC-09 (key check) + environment.md `owm-stub` (E2E isolation) | Covered |
| O1 | Bilingual UI mandatory | FT-P-22, FT-P-23 | Covered |
| O2 | Bearer never in localStorage / sessionStorage | NFT-SEC-01 | Covered |
| O3 | `credentials:'include'` on every authenticated fetch | NFT-SEC-04 | Covered |
| O4 | RBAC is server-enforced (UI must NOT trust `AuthUser.role`) | meta-design + FT-N-04 (unauth → /login regardless of any client claim); STC-O4 (no `if (user.role === 'admin')` gating sensitive data, only UI hide/show) | Partially Covered — Phase 3 may add the static gate-pattern lint |
| O5 | Refresh cookie attributes | NFT-SEC-03 | Covered |
| O6 | No hardcoded credentials | NFT-SEC-09 | Covered |
| O7 | Spec is source of truth for numeric enums | FT-P-04, FT-P-05, FT-P-06 | Covered |
| O8 | Persist what you type (panel widths) | FT-P-37 [Q], FT-P-38 [Q] | Covered (quarantined) |
| O9 | Admin can edit existing detection classes (P12) | NOT COVERED — feature missing today (`acceptance_criteria.md` notes P12 violation; PATCH endpoint to re-introduce in Phase B) | NOT COVERED — Phase B target |
| O10 | Destructive actions require ConfirmDialog | NFT-SEC-08, FT-P-26, FT-P-27, FT-N-07 | Covered |
| O11 | No SSR / RSC | NFT-RES-LIM-03 (no Node in image) + STC-O11 (no `react-dom/server` import) | Partially Covered |
| O12 | `mission-planner/` not compiled by production Vite build | NFT-RES-LIM-04 | Covered |
| O13 | Bundle size budget ≤ ~2 MB gzipped initial JS | NFT-PERF-01, NFT-RES-LIM-01 | Covered (target — no CI gate today) |
| O14 | No CI test step today | META — resolved at Step 5 (Decompose Tests) | N/A — meta |
| O15 | No vuln scan / SBOM / image signing | NOT COVERED — Step 6 / security_approach surface; Phase B addition | NOT COVERED |
## Coverage Summary
| Category | Total Items | Covered | Partially Covered | Not Covered | N/A (meta) | Coverage % (Covered+Partial) |
|----------|-------------|---------|-------------------|-------------|-----------|--------------------|
| Acceptance Criteria | 40 | 40 | 0 | 0 | 0 | 100% (24 fully ungated, 16 with Phase 3 quarantine markers) |
| Anti-Criteria | 5 | 5 | 0 | 0 | 0 | 100% |
| Restrictions | 41 | 17 | 8 | 13 | 3 | 61% |
| **Total** | **86** | **62** | **8** | **13** | **3** | **81%** |
Acceptance criterion coverage exceeds the 75 % template threshold. Restriction coverage is short of 75 % because most of the un-covered restrictions are dependency-version pins (S1-S11) for which a single static check pass (planned `STC-S*` family) would lift them to Covered without changing the SPA's observable behavior.
## Uncovered Items Analysis
| Item | Reason Not Covered | Risk | Mitigation |
|------|-------------------|------|-----------|
| S1-S11 (TS strict, React/Vite/Bun version pins, Tailwind, Leaflet, Chart.js, DnD) | Dependency-version restrictions need a static `package.json` / `tsconfig.json` parser pass; not authored in this phase | Drift between pinned and installed versions (e.g., transitive resolution); accidental upgrade in a refactor breaking ADRs (ADR-001 …) | Phase 3 to confirm adding the `STC-S*` family; otherwise Step 4 / Step 8 may add them as part of the testability fix |
| S12, S13 partials | `STC-S12` (no other locale bundles) and `STC-S13` (no IndexedDB / localForage / Dexie deps) need explicit dep scans | Low — currently aligned per `package.json`; risk only on dep additions | Phase 3 promotion to a single static dep-scan job (run alongside the dep-license lint) |
| E4, E8 | Dev-only proxy and pipeline branch triggers are not consumer-observable from a production build | None today; future risk if the proxy diverges from the prod nginx | Document-only; Phase 3 confirms |
| O4 partial (RBAC trust pattern) | No lint rule today; the design intent is captured but not asserted in code | UI accidentally gates sensitive data on a client claim | Phase 3 to add an `STC-O4` lint that bans `if (user.role === 'admin') { /* reveal data */ }` patterns; FT-N-04 already locks the unauth path |
| O9 (admin can edit classes — P12) | The feature does not exist today (only add + delete); cannot test absence of a regression | Functionality missing relative to spec | Phase B feature cycle will add `PATCH /api/admin/classes/{id}` plus FT-P-XX in the cycle that ships it; AC-37 / AC-30 already enforce the surrounding contract |
| O11 partial | NFT-RES-LIM-03 confirms no Node binary in the image; an explicit "no `react-dom/server` import" lint is missing | Could regress to SSR by accident on a refactor; the image base check would still pass | Phase 3 confirms `STC-O11` |
| O15 (vuln scan / SBOM / signing) | Pipeline does not emit any of these today | Supply-chain risk | Addressed at Step 6 / `security_approach.md`; outside the test-spec scope |
| AC-11, AC-18, AC-24, AC-40, AC-25 async path | Phase B / target features with quarantined tests | Tests do not gate today — risk = the feature ships without the assertion being un-quarantined | Phase 3 chooses: keep quarantined (gates the day the feature ships) OR downgrade to documentary; recommendation: keep quarantined |
| AC-04 enum spec numeric values for `MediaStatus` / `Affiliation` / `CombatReadiness` | The exact spec values are not pinned in `enum_spec_snapshot.json` yet | Tests use symbolic comparison and may silently match a wrong UI state | Phase 3 to require population of the snapshot before AC-04 tests gate CI; recommendation: BLOCK Step 4 until the snapshot is committed |
| AC-37 backend ordering | The class-hotkey contract requires `[0..N-1, 20..20+N-1, 40..40+N-1]` ordering from the suite; not yet verified | Hotkey test fails at integration; ambiguous responsibility | Phase 3 to surface; fix can land server-side or via a client-side resort, depending on the verification result |
## Quarantine List (running)
The following 18 tests assert against a Phase B target or a Step 4 fix and are quarantined until the implementation lands. Phase 3 will decide their disposition.
| Test | Reason | Activates when |
|------|--------|---------------|
| FT-P-01 (bootstrap part) | Bootstrap refresh missing `credentials:'include'` per finding | Step 4 fix |
| FT-P-12, FT-P-13 | Async video detect (F7) not wired | Phase B feature cycle |
| FT-P-24, FT-P-25 | i18n detector + persistence missing | Step 4 fix |
| FT-P-33 (timeout) | ProtectedRoute timeout missing | Step 4 fix |
| FT-P-37, FT-P-38 | Panel-width writer missing (`useResizablePanel`) | Step 4 fix |
| FT-P-51 | Split surface not on dataset page today | Phase B |
| FT-P-54, FT-P-55 | Tile-zoom UX missing (finding #24) | Phase B |
| FT-N-03, FT-N-05 | `/admin` and `/settings` role-gates missing | Step 4 / Step 8 |
| FT-N-11, FT-N-12, FT-N-13, FT-N-14 | Settings form hygiene fixes missing | Step 4 |
| NFT-PERF-03 / NFT-RES-02 | SSE refresh-rotation reconnect missing | Step 8 hardening |
| NFT-PERF-08 / NFT-PERF-09 | Tied to FT-P-37 / FT-N-13 quarantines | per above |
| NFT-SEC-05, NFT-SEC-06 | Tied to FT-N-03, FT-N-05 | per above |
| NFT-SEC-09 (source check) | OpenWeatherMap key still in source today | Step 4 fix |
| NFT-RES-04 | Tied to FT-P-33 | per above |
## Phase 3 (Data Validation Gate) — Open Items to Resolve
### Resolved in Phase 3
1. ~~**`enum_spec_snapshot.json`** — populate from the suite spec before AC-04 tests gate CI.~~**Resolved:** snapshot committed at `_docs/00_problem/input_data/enum_spec_snapshot.json`. `verification_pending: true` markers remain for `CombatReadiness` and `MediaType` (numeric ordering inferred from schema member-listing); Step 4 .NET-service inspection lifts those.
2. ~~**NFT-RES-09** — no `results_report.md` row binding.~~**Resolved:** row 96 added (tainted-canvas fallback observable).
3. ~~**NFT-RES-10** — no `results_report.md` row binding.~~**Resolved:** row 97 added (SSE server-disconnect observable; ≤ 10 s indicator-or-reconnect; reconnect attempts ≤ 1 in window).
4. ~~**NFT-PERF-10** — FCP baseline has no `results_report.md` row.~~**Resolved:** row 98 added (`FCP ≤ 3 000 ms` on warm-cache navigation to `/flights`, headless Chromium, 2 vCPU / 4 GB edge profile).
### Still open (carry forward to Step 4 / runner)
5. **AC-37 backend ordering** — verify the `annotations/` service response shape and either confirm matches or schedule a fix on the appropriate side.
6. **STC family** — confirm adding the `STC-S*` / `STC-O4` / `STC-O11` static-check IDs to lift the restriction coverage above 75 %.
7. **Quarantine disposition** — accept the quarantine list above; decide whether quarantined tests gate CI today (recommended: no, but they are picked up automatically the day the feature lands).
8. **AC-04 UI enum drift in `src/types/index.ts`** — tests will FAIL until the Step 4 fix lands per `acceptance_criteria.md`; quarantine until Step 4 OR run them and use the failure as the gate to schedule Step 4. The drift list is pinned in `enum_spec_snapshot.json``ui_drift_summary` (5 enums).
9. **Parent-suite doc fixes (leftover)**`../_docs/01_annotations.md` line 208 and `../_docs/09_dataset_explorer.md` line 165 show stale `affiliation: 2 // Hostile` examples; should be 20 per `00_database_schema.md`. Record as a leftover in `_docs/_process_leftovers/` when raised against the parent suite.