mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 11:51:10 +00:00
[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:
@@ -0,0 +1,166 @@
|
||||
# Data Parameters — Azaion UI
|
||||
|
||||
> Output of `/document` Step 6d. The Azaion UI is a **thin client over a typed
|
||||
> REST + SSE contract**; it carries no database. "Input data" therefore means
|
||||
> the data shapes the SPA consumes (REST response payloads, SSE event
|
||||
> payloads, env config). All claims trace to `_docs/02_document/data_model.md`,
|
||||
> `architecture.md` § 4–5, and per-component descriptions.
|
||||
|
||||
**Status**: synthesised-from-verified-docs (Step 6d — `/document`)
|
||||
**Date**: 2026-05-10
|
||||
|
||||
---
|
||||
|
||||
## Categories of input data
|
||||
|
||||
The SPA consumes four categories:
|
||||
|
||||
1. **Typed REST entities** — see `_docs/02_document/data_model.md` for the
|
||||
full ER map; key shapes summarised below.
|
||||
2. **SSE event payloads** — `live-gps`, `annotation-status`, planned
|
||||
`detect-stream`.
|
||||
3. **Configuration / environment variables** — runtime config injected at
|
||||
build time or via env.
|
||||
4. **Static assets** — translation bundles, icons, design tokens (compiled
|
||||
into the bundle).
|
||||
|
||||
---
|
||||
|
||||
## 1. Typed REST entities (defined in `src/types/index.ts`)
|
||||
|
||||
> Every entity below mirrors the suite's REST contract. Values listed here
|
||||
> match the **suite spec**, which is the source of truth per principle P9.
|
||||
> Where the UI's current TypeScript enum drifts from the spec, the row notes
|
||||
> the drift and the Step 4 fix.
|
||||
|
||||
### Auth
|
||||
|
||||
| Entity | Fields | Source |
|
||||
|--------|--------|--------|
|
||||
| `AuthUser` | `id`, `email`, `name`, `role`, `permissions: string[]`, `aircraftId?` | `02_auth`; `admin/` service |
|
||||
| `LoginRequest` | `{ email, password }` | `POST /api/admin/auth/login` body |
|
||||
| `LoginResponse` | `{ bearer, user: AuthUser }` (refresh cookie set server-side) | `POST /api/admin/auth/login` 200 |
|
||||
|
||||
### Flights
|
||||
|
||||
| Entity | Fields | Source |
|
||||
|--------|--------|--------|
|
||||
| `Flight` | `id`, `name`, `aircraftId`, `startDate?`, `endDate?`, `description?` | `flights/` service |
|
||||
| `Waypoint` | **Spec**: `{ Geopoint: { Lat, Lon, MGRS }, Source, Objective, OrderNum, Height }`. **UI today**: `{ name, latitude, longitude, order }` — drift, finding #20 / Step 4 fix | `05_flights`; `flights/` service |
|
||||
| `Aircraft` | `id`, `name`, `model`, `isDefault`, `serialNumber?` | `flights/` (read+write); `08_admin` mutation |
|
||||
| `LiveGpsEvent` (SSE) | `{ flightId, lat, lon, alt, heading, speed, ts }` | `createSSE('/api/flights/${id}/live-gps')`; F13 |
|
||||
|
||||
### Annotations + Media
|
||||
|
||||
| Entity | Fields | Source |
|
||||
|--------|--------|--------|
|
||||
| `Media` | `id`, `flightId`, `mediaType: MediaType`, `mediaStatus: MediaStatus`, `filename`, `waypointId?`, `videoTime?`, `thumbnail?` | `annotations/` service |
|
||||
| `MediaType` enum | **Spec**: `None=0`, `Image=1`, `Video=2`. **UI**: same. | `00_foundation`; P9 |
|
||||
| `MediaStatus` enum | **Spec**: must include `None`, `Confirmed`, `Error` plus the existing `New`, `AiProcessing`, `AiProcessed`, `ManualCreated`. **UI today**: only `New=0` / `AiProcessing=1` / `AiProcessed=2` / `ManualCreated=3` — drift, Step 4 fix | `00_foundation`; finding |
|
||||
| `AnnotationListItem` | `id`, `mediaId`, `videoTime`, `status: AnnotationStatus`, `source: AnnotationSource`, `detections: Detection[]`, `isSeed?: boolean` | `annotations/` |
|
||||
| `AnnotationStatus` enum | **Spec**: `None=0`, `Created=10`, `Edited=20`, `Validated=30`, `Deleted=40`. **UI today**: `Created=0`, `Edited=1`, `Validated=2` — drift, Step 4 fix per P9 | `00_foundation`; `04_verification_log.md` |
|
||||
| `AnnotationSource` enum | `AI=0`, `Manual=1` (matches spec) | `00_foundation` |
|
||||
| `Detection` | `{ classNum: number, x, y, w, h: number, affiliation: Affiliation, combatReadiness: CombatReadiness, confidence?: number }` (normalised pixel coords) | `06_annotations` |
|
||||
| `Affiliation` enum | **Spec**: must include `None` plus `Unknown`, `Friendly`, `Hostile`. **UI today**: `Unknown=0`, `Friendly=1`, `Hostile=2` — drift, Step 4 fix | finding |
|
||||
| `CombatReadiness` enum | **Spec**: must include `Unknown` plus `NotReady`, `Ready`. **UI today**: `NotReady=0`, `Ready=1` — drift, Step 4 fix | finding |
|
||||
| `DetectionClass` | `{ id, name, color, photoMode, maxSizeM }` | `08_admin` (write) + `annotations/` (read) |
|
||||
| Annotation save body | **Required** (per finding #32): `Source`, `WaypointId`, `videoTime`, plus `mediaId`, `detections`, `status`. **UI today**: missing `Source` and `WaypointId`; uses `time` instead of `videoTime` — Step 4 fix | `06_annotations/AnnotationsPage.tsx` |
|
||||
| `AnnotationStatusEvent` (SSE) | `{ annotationId, mediaId, oldStatus, newStatus, ts }` | `createSSE('/api/annotations/annotations/events')`; F14 |
|
||||
|
||||
### Dataset
|
||||
|
||||
| Entity | Fields | Source |
|
||||
|--------|--------|--------|
|
||||
| `DatasetItem` | `id`, `mediaId`, `classNum`, `status: AnnotationStatus`, `thumbnail`, `isSeed?: boolean`, `isSplit?: boolean` (parent-suite-doc fix applied for `isSplit`) | `07_dataset`; `annotations/` |
|
||||
| `ClassDistributionItem` | `{ classNum, label, color, count }` | `annotations/` |
|
||||
| Bulk-validate body | `{ ids: number[], targetStatus: AnnotationStatus.Validated }` | `POST /api/annotations/dataset/bulk-status` |
|
||||
|
||||
### Settings + Admin
|
||||
|
||||
| Entity | Fields | Source |
|
||||
|--------|--------|--------|
|
||||
| `SystemSettings` | as defined per `09_settings/SettingsPage.tsx` (settings keys per the suite spec) | `annotations/` (`/api/annotations/settings/system`) |
|
||||
| `DirectorySettings` | per `SettingsPage` directory tab | `annotations/` (`/api/annotations/settings/directories`) |
|
||||
| `CameraSettings` | per `SettingsPage` camera tab | `annotations/` |
|
||||
| `UserSettings` | `selectedFlightId?: number`, `panelWidths?: { ... }`, plus other per-user UI state | `annotations/` (`/api/annotations/settings/user`) |
|
||||
| `User` | `id`, `email`, `role`, `isActive`, `createdAt?` | `admin/` |
|
||||
|
||||
### Pagination
|
||||
|
||||
| Entity | Shape | Source |
|
||||
|--------|-------|--------|
|
||||
| `PaginatedResponse<T>` | `{ items: T[], totalCount: number, page: number, pageSize: number }` | shared envelope used by every list endpoint |
|
||||
|
||||
---
|
||||
|
||||
## 2. SSE event payloads
|
||||
|
||||
| Stream | URL | Payload shape | Where consumed |
|
||||
|--------|-----|---------------|----------------|
|
||||
| Live-GPS per flight (F13) | `GET /api/flights/${flightId}/live-gps?token=${bearer}` | `LiveGpsEvent` (see above) | `src/features/flights/FlightsPage.tsx:67` |
|
||||
| Annotation-status events (F14) | `GET /api/annotations/annotations/events?token=${bearer}` | `AnnotationStatusEvent` | `src/features/annotations/AnnotationsSidebar.tsx:25` |
|
||||
| Async detect progress (F7) | `GET /api/detect/stream/${jobId}?token=${bearer}` — **target-only, NOT wired today** | `{ jobId, progress: 0..1, detections?: Detection[], status, ts }` (anticipated) | not consumed today; planned per `04_verification_log.md` F7 |
|
||||
|
||||
Bearer goes in the **query string** (`?token=...`) per `ADR-008` — `EventSource`
|
||||
cannot send headers. Refresh-rotation breaks live SSE; reconnect is missing
|
||||
today (Step 8 hardening per `architecture.md` § Architecture Vision).
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuration / environment variables
|
||||
|
||||
| Variable | Where read | Type | Default | Source |
|
||||
|----------|-----------|------|---------|--------|
|
||||
| `VITE_OPENWEATHERMAP_API_KEY` | (target — Step 4 fix) `mission-planner/src/utils/flightPlanUtils.ts` | string (secret) | currently hardcoded `'335799082893fad97fa36118b131f919'` (must rotate) | P10 violation, Step 4 fix |
|
||||
| `VITE_SATELLITE_TILE_URL` | mission-planner Leaflet `TileLayer` | URL | none (unset breaks satellite imagery) | mission-planner only today |
|
||||
| `AZAION_REVISION` | stamped into the production image at build time | string (commit SHA) | `$CI_COMMIT_SHA` from CI | `Dockerfile`; `.woodpecker/build-arm.yml` |
|
||||
| `REGISTRY_HOST` | CI registry push | string | per pipeline secret | `.woodpecker/build-arm.yml` |
|
||||
| `i18next.lng` | `src/i18n/i18n.ts` | language code | hardcoded `'en'` (Step 4 fix — should resolve from detector) | `i18n.ts`; AC-13 |
|
||||
| nginx upstream hosts | `nginx.conf` | hostnames per service | docker-compose service names | `nginx.conf` |
|
||||
|
||||
The SPA bundle MUST NOT carry secrets at build time — except OpenWeatherMap
|
||||
once it is moved to `.env` (per P10 the proper long-term answer is to proxy
|
||||
the OWM call through `flights/` so no key reaches the browser; the `.env`
|
||||
move is the interim Step 4 testability fix).
|
||||
|
||||
---
|
||||
|
||||
## 4. Static assets
|
||||
|
||||
| Asset | Location | Notes |
|
||||
|-------|----------|-------|
|
||||
| Translation bundles | `src/i18n/en.json`, `src/i18n/ua.json` | English + Ukrainian; key parity is mandatory (AC-12) |
|
||||
| Design tokens | `src/index.css` (`az-bg`, `az-text`, `az-orange`, `az-success`, `az-danger`, `az-primary`, ...) | Tailwind 4; `ADR-005` |
|
||||
| Map icons | `src/features/flights/mapIcons.ts` | defaultIcon CDN URL pinned to `leaflet@1.7.1` (drift — finding) |
|
||||
| Aircraft / waypoint icons | bundled SVG / PNG under `src/features/flights/icons/*` (mission-planner port-source still has the larger set) | `05_flights` |
|
||||
| Detection-class colors | `src/features/annotations/classColors.ts` (logically owned by `11_class-colors`) | file-move pending (P11 / module-layout Verification Needed) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Data flow summary
|
||||
|
||||
1. **Plan flight** — UI fetches `aircrafts` from `flights/`; submits flight +
|
||||
waypoints; receives the persisted flight (today: delete-then-recreate
|
||||
waypoint cycle, finding #19; lossy POST shape, finding #20).
|
||||
2. **Capture media** — out-of-band via the loader / annotations services;
|
||||
the UI surfaces uploaded items via `MediaList` polling.
|
||||
3. **Annotate** — operator edits → `POST /api/annotations/annotations`;
|
||||
`F14` SSE pushes other-user status changes (admin-wide stream,
|
||||
client-side filtered).
|
||||
4. **AI Detect (sync image)** — `POST /api/detect/${id}` returns inline
|
||||
detections. **Used for both image and video today** (silent UX hazard
|
||||
for long videos — `F7` to ship in Phase B).
|
||||
5. **AI Detect (async video — target)** — `POST /api/detect/video/${id}`
|
||||
returns a job ID → SSE on `/api/detect/stream/${jobId}` streams progress.
|
||||
Long videos require `X-Refresh-Token` header per `_docs/10_auth.md`.
|
||||
6. **Curate dataset** — UI queries `annotations/` with status filters;
|
||||
bulk-validate transitions to `AnnotationStatus.Validated`; class-distribution
|
||||
chart loads from `/api/annotations/dataset/class-distribution`.
|
||||
7. **Settings** — system / directory / camera saves go to `annotations/`;
|
||||
aircraft default-toggle goes to `flights/` (cross-service mutation,
|
||||
accepted).
|
||||
8. **GPS-Denied Test Mode (target — F12)** — `.tlog` + video upload to
|
||||
`gps-denied-desktop/`; SITL drives `gps-denied-onboard/`; results render
|
||||
back through `flights/` GPS-Denied tab.
|
||||
|
||||
Full sequence diagrams: `_docs/02_document/system-flows.md`.
|
||||
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"$schema_note": "Pinned numeric values for the suite's wire-format enums per the suite spec. Tests FT-P-04, FT-P-05, FT-P-06 assert that src/types/index.ts matches these values exactly. Drift between the UI and this snapshot is a Step 4 fix candidate (see acceptance_criteria.md AC-04, restrictions.md O7).",
|
||||
"source_of_truth": [
|
||||
{"file": "../_docs/00_database_schema.md", "extracted_at": "2026-05-10T22:00:00+03:00", "note": "Authoritative — the DB schema pins the numeric values directly."},
|
||||
{"file": "../_docs/01_annotations.md", "note": "Wire-format declaration (line 26): all enum fields serialize as numeric integers."},
|
||||
{"file": "../_docs/09_dataset_explorer.md", "note": "JSON examples for Affiliation and CombatReadiness use stale sequential values (affiliation:2 // Hostile, combatReadiness:1 // Ready) and predate the schema's 0/10/20/30 scheme. Parent-suite doc fix pending — record in _docs/_process_leftovers/ when populated."}
|
||||
],
|
||||
"ui_drift_summary": {
|
||||
"AnnotationStatus": {"ui_values": {"Created": 0, "Edited": 1, "Validated": 2}, "spec_values": "see enums.AnnotationStatus", "fix_target": "src/types/index.ts (Step 4)"},
|
||||
"MediaStatus": {"ui_values": {"New": 0, "AiProcessing": 1, "AiProcessed": 2, "ManualCreated": 3}, "spec_values": "see enums.MediaStatus", "fix_target": "src/types/index.ts (Step 4) — UI must add None=0, Confirmed=5, Error=6 and renumber existing members"},
|
||||
"Affiliation": {"ui_values": {"Unknown": 0, "Friendly": 1, "Hostile": 2}, "spec_values": "see enums.Affiliation", "fix_target": "src/types/index.ts (Step 4) — UI must add None=0 and renumber existing members to spec values"},
|
||||
"CombatReadiness": {"ui_values": {"NotReady": 0, "Ready": 1}, "spec_values": "see enums.CombatReadiness", "fix_target": "src/types/index.ts (Step 4) — UI must add Unknown and confirm numeric values via .NET service inspection"},
|
||||
"MediaType": {"ui_values": {"None": 0, "Image": 1, "Video": 2}, "spec_values": "see enums.MediaType", "fix_target": "src/types/index.ts (Step 4) — spec schema order is None|Video|Image which implies Video=1, Image=2; UI has Image=1, Video=2. NEW DRIFT not previously called out in data_parameters.md."}
|
||||
},
|
||||
"enums": {
|
||||
"AnnotationStatus": {
|
||||
"source": "../_docs/00_database_schema.md line 79: enum AnnotationStatus \"None(0)|Created(10)|Edited(20)|Validated(30)|Deleted(40)\"",
|
||||
"values": {"None": 0, "Created": 10, "Edited": 20, "Validated": 30, "Deleted": 40},
|
||||
"verification_pending": false,
|
||||
"notes": "Authoritative. Wire format used by POST /annotations, PATCH /annotations/{id}/status, POST /dataset/bulk-status, and the F14 AnnotationStatusEvent SSE payload."
|
||||
},
|
||||
"MediaStatus": {
|
||||
"source": "../_docs/00_database_schema.md line 66: enum MediaStatus \"None(0)|New(1)|AIProcessing(2)|AIProcessed(3)|ManualCreated(4)|Confirmed(5)|Error(6)\"",
|
||||
"values": {"None": 0, "New": 1, "AIProcessing": 2, "AIProcessed": 3, "ManualCreated": 4, "Confirmed": 5, "Error": 6},
|
||||
"verification_pending": false,
|
||||
"case_note": "Schema uses 'AIProcessing' (uppercase AI); UI uses 'AiProcessing' (camelCase). The wire payload is numeric only, so the TypeScript identifier casing is internal. Recommend matching the spec casing on rename for consistency."
|
||||
},
|
||||
"Affiliation": {
|
||||
"source": "../_docs/00_database_schema.md line 94: enum Affiliation \"None(0)|Friendly(10)|Hostile(20)|Unknown(30)\"",
|
||||
"values": {"None": 0, "Friendly": 10, "Hostile": 20, "Unknown": 30},
|
||||
"verification_pending": false,
|
||||
"stale_example_note": "../_docs/01_annotations.md line 208 and ../_docs/09_dataset_explorer.md line 165 still show 'affiliation: 2 // Affiliation.Hostile' — STALE per the schema. Flag as parent-suite-doc fix leftover."
|
||||
},
|
||||
"CombatReadiness": {
|
||||
"source": "../_docs/00_database_schema.md line 95: enum CombatReadiness \"Ready|NotReady|Unknown\" — numeric values NOT pinned in the schema",
|
||||
"values": {"NotReady": 0, "Ready": 1, "Unknown": 2},
|
||||
"verification_pending": true,
|
||||
"verification_note": "Numeric values inferred as sequential per the spec's member-listing order. The 01_annotations.md JSON example shows 'combatReadiness: 1 // CombatReadiness.Ready' which is consistent with Ready=1. Step 4 .NET-service inspection must confirm or override. Alternative possibility: the spec lists Ready first by intent (Ready=0, NotReady=1, Unknown=2) — schema text 'Ready|NotReady|Unknown' is ambiguous on intent."
|
||||
},
|
||||
"MediaType": {
|
||||
"source": "../_docs/00_database_schema.md line 65: enum MediaType \"None|Video|Image\"",
|
||||
"values": {"None": 0, "Video": 1, "Image": 2},
|
||||
"verification_pending": true,
|
||||
"verification_note": "Numeric values inferred sequentially per schema member-order (None|Video|Image). This contradicts the UI's current src/types/index.ts which has Image=1, Video=2. Step 4 .NET-service inspection must confirm. If the .NET service in fact uses None=0, Image=1, Video=2 (the UI's current shape), then the schema text is misleading and the UI is correct; otherwise the UI is drifted and needs the fix."
|
||||
},
|
||||
"AnnotationSource": {
|
||||
"source": "../_docs/01_annotations.md lines 19-24 (table) + ../_docs/00_database_schema.md line 78 (enum Source \"AI|Manual\")",
|
||||
"values": {"AI": 0, "Manual": 1},
|
||||
"verification_pending": false,
|
||||
"notes": "Both files agree: AI=0, Manual=1. UI matches."
|
||||
},
|
||||
"WaypointSource": {
|
||||
"source": "../_docs/00_database_schema.md line 55: enum WaypointSource \"Auto|Manual\"",
|
||||
"values": {"Auto": 0, "Manual": 1},
|
||||
"verification_pending": true,
|
||||
"verification_note": "Inferred sequentially. Not asserted by any test in this round; recorded for completeness because data_parameters.md / acceptance_criteria.md flag Waypoint POST shape drift for Step 4."
|
||||
},
|
||||
"WaypointObjective": {
|
||||
"source": "../_docs/00_database_schema.md line 56: enum WaypointObjective \"Surveillance|Strike|Recon\"",
|
||||
"values": {"Surveillance": 0, "Strike": 1, "Recon": 2},
|
||||
"verification_pending": true,
|
||||
"verification_note": "Inferred sequentially. Same caveat as WaypointSource — not currently asserted by a UI test."
|
||||
}
|
||||
},
|
||||
"downstream_actions": {
|
||||
"step_4_fixes": [
|
||||
"src/types/index.ts AnnotationStatus → align to {None:0, Created:10, Edited:20, Validated:30, Deleted:40}",
|
||||
"src/types/index.ts MediaStatus → add None, Confirmed, Error; renumber per spec",
|
||||
"src/types/index.ts Affiliation → add None; renumber per spec",
|
||||
"src/types/index.ts CombatReadiness → add Unknown; confirm numerics via .NET inspection; renumber if needed",
|
||||
"src/types/index.ts MediaType → confirm numerics via .NET inspection; renumber if spec schema (Video=1, Image=2) wins"
|
||||
],
|
||||
"parent_suite_doc_fixes": [
|
||||
"../_docs/01_annotations.md line 208 — Affiliation example value (currently affiliation:2 // Hostile) must update to 20 per the schema",
|
||||
"../_docs/09_dataset_explorer.md line 165 — same fix",
|
||||
"Both files — surface CombatReadiness numeric pinning in the table-of-truth (currently only in member listing)"
|
||||
],
|
||||
"phase_3_disposition": "FT-P-04 (AnnotationStatus) gates today. FT-P-05 (MediaStatus + Affiliation) gates today. FT-P-06 partial (detection enum payload check) gates for Affiliation today; CombatReadiness assertion runs with the verification_pending: true caveat (Phase 4 runner script can downgrade it to documentary until Step 4 inspection lands)."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
# Expected Results — Azaion UI
|
||||
|
||||
> Maps every behavioral test trigger (REST request, SSE event, user action,
|
||||
> build/config artifact, static check) to a **quantifiable** expected result.
|
||||
> Sourced from `_docs/00_problem/acceptance_criteria.md` (34 ACs + 5
|
||||
> anti-criteria) and cross-checked against `restrictions.md`,
|
||||
> `data_parameters.md`, and `_docs/02_document/architecture.md`.
|
||||
>
|
||||
> **Project shape note.** The Azaion UI is a thin SPA over a typed REST + SSE
|
||||
> contract; it carries no database and consumes no sample-image / sample-video
|
||||
> input files of its own. "Input" here therefore means a trigger condition
|
||||
> (HTTP request, SSE message, click, build invocation, code search) and the
|
||||
> "expected result" is the observable behavior the test asserts. Almost every
|
||||
> row uses the **behavioral shape** defined in `test-spec/SKILL.md` ("trigger
|
||||
> + observable + quantifiable pass/fail"); a few rows that exchange concrete
|
||||
> JSON bodies use the **input/output shape**.
|
||||
>
|
||||
> No reference files are required at this stage — every observable in the
|
||||
> table below is small enough to inline. If `Phase 2` of `/test-spec` finds a
|
||||
> case that needs a multi-row expected payload (e.g. full
|
||||
> `traceability-matrix` output), a JSON / CSV file will be added in this
|
||||
> directory and referenced from the corresponding row.
|
||||
|
||||
**Status**: agent-drafted (autodev Step 3 — Test Spec, Phase 1 prereq)
|
||||
**Date**: 2026-05-10
|
||||
|
||||
---
|
||||
|
||||
## Result Format Legend
|
||||
|
||||
| Result Type | When to Use | Example |
|
||||
|-------------|-------------|---------|
|
||||
| Exact value | Output must match precisely | `status_code: 200`, `count: 0` |
|
||||
| Tolerance range | Numeric output with acceptable variance | `position ± 50ms`, `width ± 1px` |
|
||||
| Threshold | Output must exceed or stay below a limit | `latency ≤ 500ms`, `count == 0` |
|
||||
| Pattern match | Output must match a string/regex pattern | URL regex, header presence |
|
||||
| Set/count | Output must contain specific items or counts | `keys(en) == keys(ua)` |
|
||||
|
||||
## Comparison Methods
|
||||
|
||||
| Method | Description | Tolerance Syntax |
|
||||
|--------|-------------|------------------|
|
||||
| `exact` | Actual == Expected | N/A |
|
||||
| `numeric_tolerance` | abs(actual - expected) ≤ tolerance | `± <value>` |
|
||||
| `range` | min ≤ actual ≤ max | `[min, max]` |
|
||||
| `threshold_min` | actual ≥ threshold | `≥ <value>` |
|
||||
| `threshold_max` | actual ≤ threshold | `≤ <value>` |
|
||||
| `regex` | actual matches regex pattern | regex string |
|
||||
| `substring` | actual contains substring | substring |
|
||||
| `set_equals` | sets of items match exactly | set notation |
|
||||
| `set_contains` | actual set ⊇ expected | subset notation |
|
||||
| `present` / `absent` | observable exists / does not exist | N/A |
|
||||
|
||||
---
|
||||
|
||||
## Input → Expected Result Mapping
|
||||
|
||||
### Group 1 — Authentication & Token Handling (AC-01 / AC-02 / AC-03 / AC-22 / AC-23 / AC-24)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 01 | Any authenticated `fetch` issued via `apiClient` | Outbound API call that requires the bearer | RequestInit MUST include `credentials: 'include'` | exact | N/A | N/A |
|
||||
| 02 | Bootstrap refresh call on app mount (`AuthContext` init) | The refresh-on-load flow before any user interaction | RequestInit MUST include `credentials: 'include'`; cookie sent | exact | N/A | N/A |
|
||||
| 03 | First 401 from `/api/admin/...` while a session is active | Bearer expired mid-session | Sequence: `POST /api/admin/auth/refresh` (cookie-bound) → original request retried with new bearer → final response 200 | exact (sequence) | N/A | N/A |
|
||||
| 04 | Code-search across `src/` for `localStorage.|sessionStorage.` references that touch the bearer | Static check on token-storage policy | `match_count == 0` | exact | N/A | N/A |
|
||||
| 05 | Code-search across `src/` for `document.cookie` reads that target the refresh token | Static check on cookie-readability policy | `match_count == 0` (cookie is HttpOnly server-side; UI must not even attempt) | exact | N/A | N/A |
|
||||
| 06 | Programmatic read of `document.cookie` after login | Browser-level visibility test of the refresh cookie | Returned string MUST NOT contain the refresh-token value | absent | N/A | N/A |
|
||||
| 07 | Refresh-cookie response header from `/api/admin/auth/login` (test fixture) | Set-Cookie attribute audit | Header value matches regex `Secure;.*HttpOnly;.*SameSite=Strict` (order tolerant, case insensitive) | regex | case-insensitive, attribute-order-tolerant | N/A |
|
||||
| 08 | Authenticated user with `role != admin` navigating to `/admin` | RBAC route gate (admin) | Final URL is `/flights`; `<AdminPage>` MUST NOT mount | exact (URL), absent (component) | N/A | N/A |
|
||||
| 09 | Unauthenticated user navigating to `/admin` | RBAC route gate (login) | Final URL is `/login` | exact | N/A | N/A |
|
||||
| 10 | Authenticated user without settings permission navigating to `/settings` | RBAC route gate (settings) | Final URL is `/flights`; `<SettingsPage>` MUST NOT mount | exact, absent | N/A | N/A |
|
||||
| 11 | Auth refresh occurring while the user is on `/flights` (mid-session) | Refresh transparency | `<ProtectedRoute>` MUST NOT unmount its children during refresh; render-counter delta ≤ 1 across refresh | numeric_tolerance | `≤ 1` re-render | N/A |
|
||||
| 12 | Single `/api/admin/auth/refresh` invocation | Refresh round-trip count | Exactly 1 outbound network call observed during one refresh cycle | exact | N/A | N/A |
|
||||
| 13 | Bearer rotation while two SSE streams are open | SSE refresh-rotation handling | Both EventSource instances MUST close, reconnect with new token in query string, and resume within 5 s | exact (close+open count), threshold_max | `≤ 5000ms` | N/A |
|
||||
|
||||
### Group 2 — Wire Contract & Enum Compliance (AC-04 / AC-29)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 14 | Read `AnnotationStatus` enum from `src/types/index.ts` | Numeric values must match suite spec | `{None:0, Created:10, Edited:20, Validated:30, Deleted:40}` | exact (key+value map) | N/A | N/A |
|
||||
| 15 | Read `MediaStatus` enum | Numeric values must match suite spec | Members `{None, New, AiProcessing, AiProcessed, ManualCreated, Confirmed, Error}` present; numeric values match the spec map | set_contains, exact (per member) | N/A | N/A |
|
||||
| 16 | Read `Affiliation` enum | Numeric values must match suite spec | Members `{None, Unknown, Friendly, Hostile}` present; numeric values match the spec map | set_contains, exact | N/A | N/A |
|
||||
| 17 | Read `CombatReadiness` enum | Numeric values must match suite spec | Members `{Unknown, NotReady, Ready}` present; numeric values match the spec map | set_contains, exact | N/A | N/A |
|
||||
| 18 | Outbound payload of `POST /api/annotations/annotations` containing `status` field | Wire-format check | `body.status` is a number from the `AnnotationStatus` value set | set_contains | N/A | N/A |
|
||||
| 19 | Outbound payload containing a `Detection` array | Per-detection wire check | Every `detection.affiliation ∈ Affiliation values` and `detection.combatReadiness ∈ CombatReadiness values` | set_contains (per element) | N/A | N/A |
|
||||
| 20 | Code-search `src/` for `mediaType\s*[!=]==?\s*[0-9]` | Magic-literal hygiene for `MediaType` | `match_count == 0` | exact | N/A | N/A |
|
||||
| 21 | Code-search `src/` for `mediaType\s*[!=]==?\s*['"]` | Magic-string hygiene for `MediaType` | `match_count == 0` | exact | N/A | N/A |
|
||||
|
||||
### Group 3 — Annotations endpoints, payload, SSE, overlay window (AC-05 / AC-09 / AC-25 / AC-28)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 22 | Save action in `<AnnotationsPage>` | Annotation save endpoint | Exactly one `POST` to URL matching `^/api/annotations/annotations$` | exact (count + URL) | N/A | N/A |
|
||||
| 23 | Annotation save body | Required fields | Body is JSON containing keys `{Source, WaypointId, videoTime, mediaId, detections, status}` (no `time` key) | set_contains, absent (`time`) | N/A | N/A |
|
||||
| 24 | Mount of `06_annotations` page | Status-events SSE subscribe | Exactly one EventSource opened to URL matching `^/api/annotations/annotations/events(\?|$)` | exact (count + URL regex) | N/A | N/A |
|
||||
| 25 | Unmount of `06_annotations` page | Status-events SSE unsubscribe | EventSource readyState transitions to `CLOSED` (2) within 1 s | exact (state), threshold_max | `≤ 1000ms` | N/A |
|
||||
| 26 | Sync image detect trigger (`<AnnotationsSidebar>` Detect on a `MediaType.Image`) | Detect endpoint correctness | Exactly one `POST` to URL matching `^/api/detect/[0-9]+$` | exact, regex | N/A | N/A |
|
||||
| 27 | Async video detect trigger (Phase B; behind feature flag if pre-shipped) | Async detect endpoint | Exactly one `POST` to URL matching `^/api/detect/video/[0-9]+$`; response `{jobId: <int>}`; subsequent EventSource opened to `^/api/detect/stream/[0-9]+(\?|$)` | exact, regex (3 assertions) | N/A | N/A |
|
||||
| 28 | Long-video async detect | Header policy per `_docs/10_auth.md` | Outgoing request includes `X-Refresh-Token` header (non-empty) | present (header) | N/A | N/A |
|
||||
| 29 | Annotation overlay membership at `currentTime = T` | Asymmetric time window | Annotation with `videoTime` in `[T-50ms, T+150ms]` is rendered; outside this window NOT rendered | range, absent | exact bounds | N/A |
|
||||
| 30 | Overlay membership at `currentTime = T` with annotation at `T - 60ms` | Lower-bound exclusion | Annotation NOT rendered | absent | N/A | N/A |
|
||||
| 31 | Overlay membership at `currentTime = T` with annotation at `T + 160ms` | Upper-bound exclusion | Annotation NOT rendered | absent | N/A | N/A |
|
||||
|
||||
### Group 4 — Flight selection persistence + Live-GPS SSE (AC-06 / AC-08)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 32 | `FlightContext.selectFlight(flightId)` call | Selected-flight persistence path | Exactly one `PUT /api/annotations/settings/user` with body containing `{selectedFlightId: flightId}`; NO call to `/api/flights/select` (deprecated path must not exist) | exact (count + URL + body subset), absent | N/A | N/A |
|
||||
| 33 | Reload after a flight was selected | Selected-flight rehydration | On boot, `userSettings.selectedFlightId` is read and the flight is reselected without a user click | exact (selection state matches stored id) | N/A | N/A |
|
||||
| 34 | A flight is selected | Live-GPS SSE open | Exactly one EventSource to URL matching `^/api/flights/[0-9]+/live-gps(\?|$)`; readyState reaches `OPEN` (1) within 5 s | exact (count + URL regex), threshold_max | `≤ 5000ms` | N/A |
|
||||
| 35 | Flight is deselected | Live-GPS SSE close | All EventSources matching `^/api/flights/[0-9]+/live-gps` reach readyState `CLOSED` (2) within 1 s | exact (count → 0), threshold_max | `≤ 1000ms` | N/A |
|
||||
|
||||
### Group 5 — Dataset bulk-validate (AC-07)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 36 | Select N dataset items → click `Validate` | Bulk-validate endpoint + body | Exactly one `POST /api/annotations/dataset/bulk-status` with body `{ids: <length N int array>, targetStatus: 30}` (`AnnotationStatus.Validated`) | exact (URL + body) | N/A | N/A |
|
||||
| 37 | Successful 200 response from bulk-validate | UI status reflection | Each selected item's row status changes to `Validated` within 2 s of response | exact (per-row state), threshold_max | `≤ 2000ms` | N/A |
|
||||
|
||||
### Group 6 — Upload size cap (AC-10)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 38 | Read `client_max_body_size` from `nginx.conf` | nginx upload cap | Value equals `500M` | exact | N/A | N/A |
|
||||
| 39 | UI upload of a synthetic 501 MB file | 413 surfacing | API call resolves with HTTP 413; UI presents a user-visible error containing the i18n key for "file too large" (or its rendered string) — NO silent failure, NO `alert()` | exact (status), substring (rendered error), absent (`alert()`) | N/A | N/A |
|
||||
|
||||
### Group 7 — Build, bundle, and routing (AC-11 / AC-31 / AC-33 / AC-34)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 40 | `vite build` output `dist/` | Initial JS bundle budget (target — not yet enforced in CI) | Sum of gzipped initial-route JS chunks ≤ 2 MB | threshold_max | `≤ 2 MB gzipped` | N/A |
|
||||
| 41 | `vite build` output `dist/` | mission-planner exclusion | No file under `dist/` originates from `mission-planner/**`; static-import scan from `src/main.tsx` does not reach into `mission-planner/` | absent (file origin), absent (graph edge) | N/A | N/A |
|
||||
| 42 | `docker inspect azaion/ui:<tag>` | Production runtime image | Final image is based on `nginx:alpine`; no `node` binary present in the image filesystem | exact (base image), absent (binary) | N/A | N/A |
|
||||
| 43 | Read `nginx.conf` route blocks | Service routing | Exactly the 9 `location` blocks present: `/api/admin/`, `/api/flights/`, `/api/annotations/`, `/api/detect/`, `/api/loader/`, `/api/gps-denied-desktop/`, `/api/gps-denied-onboard/`, `/api/autopilot/`, `/api/resource/` | set_equals | N/A | N/A |
|
||||
| 44 | Each of the 9 `location` blocks | Prefix stripping | Each block rewrites/strips its `/api/<service>/` prefix before forwarding upstream (verified via `proxy_pass`/`rewrite` directive shape) | regex per block | N/A | N/A |
|
||||
|
||||
### Group 8 — Internationalization (AC-12 / AC-13)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 45 | Compare `src/i18n/en.json` vs `src/i18n/ua.json` | Key parity | `keys(en) == keys(ua)` (deep, sorted) | set_equals | N/A | N/A |
|
||||
| 46 | Lint sweep over `src/**/*.tsx` | No raw user-visible strings | Every JSX text node and every `aria-label` / `placeholder` / `title` string resolves through `t(...)` (or is a proper-noun acronym in the allow-list) | exact (lint findings == 0) | acronym allow-list | N/A |
|
||||
| 47 | First boot in a clean profile | i18n detector | `i18next.language` resolves from cookie or `Accept-Language`; not the literal `'en'` from a hardcoded init | exact (detector path used), absent (hardcoded `lng:'en'`) | N/A | N/A |
|
||||
| 48 | Toggle language in `<Header>` then reload | i18n persistence | After reload, `i18next.language` equals the previously selected language | exact | N/A | N/A |
|
||||
|
||||
### Group 9 — Destructive-action UX (AC-14 / AC-30)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 49 | Click `Delete class` in `<AdminPage>` | Class-delete confirmation | `<ConfirmDialog>` is rendered before any HTTP request fires; on Cancel NO `DELETE` request is made; on Confirm exactly one `DELETE` to URL matching `^/api/admin/classes/[0-9]+$` | exact (sequence), exact (count + URL regex) | N/A | N/A |
|
||||
| 50 | Code-search `src/` and `mission-planner/src/` for `\balert\(` | `alert()` ban | `match_count == 0` | exact | N/A | N/A |
|
||||
| 51 | Each destructive action surfaced in `_docs/ui_design/` (delete, validate-with-overwrite, irreversible bulk) | Confirm-before-fire policy | A `<ConfirmDialog>` opens before the destructive request fires (no direct submit path) | present (dialog), exact (sequence) | N/A | N/A |
|
||||
|
||||
### Group 10 — Accessibility (AC-15 / AC-16 / AC-17)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 52 | Render `<ConfirmDialog>` open | a11y attributes | Root element has `role="dialog"` AND `aria-modal="true"` | exact | N/A | N/A |
|
||||
| 53 | Open `<ConfirmDialog>` and Tab through | Focus trap | Focus stays inside the dialog (first ↔ last loop); never reaches an element outside | exact (focus stays in tree) | N/A | N/A |
|
||||
| 54 | Press `Escape` while `<ConfirmDialog>` is open | Cancel-on-Escape | Dialog unmounts; cancel callback invoked exactly once; no destructive HTTP request fires | exact (count) | N/A | N/A |
|
||||
| 55 | Render `<Header>` flight dropdown closed | Combobox a11y attrs | Trigger has `role="combobox"`, `aria-expanded="false"`, `aria-haspopup="listbox"` | exact | N/A | N/A |
|
||||
| 56 | Open the flight dropdown | Combobox a11y on open | `aria-expanded` switches to `"true"`; outside-click handler is now attached (was NOT attached while closed) | exact | N/A | N/A |
|
||||
| 57 | Press `Escape` while flight dropdown is open | Close-on-Escape | Dropdown closes; `aria-expanded` returns to `"false"`; outside-click handler is detached | exact | N/A | N/A |
|
||||
| 58 | `<ProtectedRoute>` shows the loading state | a11y on spinner | Spinner element has `role="status"` and an accessible label (non-empty `aria-label` or visually-hidden text) | exact, present (label) | N/A | N/A |
|
||||
| 59 | `<ProtectedRoute>` loading exceeds the timeout (10 s simulated) | Timeout fallback | A user-visible fallback (retry CTA or error message) is rendered; the indeterminate spinner is unmounted | present (fallback), absent (spinner) | timeout configurable | N/A |
|
||||
|
||||
### Group 11 — Browser support & responsive layout (AC-18 / AC-19)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 60 | Smoke render of `/flights`, `/annotations`, `/dataset` in headless Chromium and Firefox (latest 2 versions) | Browser support floor | No console error logged; main page region has rendered with expected landmark roles | exact (errors == 0), present (landmarks) | N/A | N/A |
|
||||
| 61 | Headless render at viewport width 480 px | Mobile bottom-nav variant | `<Header>` bottom-nav variant renders; top-bar variant is NOT in DOM | present, absent | N/A | N/A |
|
||||
| 62 | Headless render at viewport width 1024 px | Desktop variant | Top-bar `<Header>` renders; bottom-nav variant NOT in DOM | present, absent | N/A | N/A |
|
||||
|
||||
### Group 12 — Secrets (AC-20)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 63 | Code-search `src/` and `mission-planner/src/` for the literal OWM key value (and for any `appid=` / `api_key=` in source URLs) | Secrets-in-source check | `match_count == 0` for the literal key; the key is read only via `import.meta.env.VITE_OPENWEATHERMAP_API_KEY` (or proxied through the suite) | exact, exact (single read site) | N/A | N/A |
|
||||
|
||||
### Group 13 — User-settings persistence (AC-21)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 64 | Drag a resizable panel divider via `useResizablePanel` and release | Persist on resize-end | Within 1 s of release, exactly one `PUT /api/annotations/settings/user` fires with body containing `panelWidths: { ... }` reflecting the new sizes | exact (count + URL), substring (key), threshold_max (1 s) | debounce-aware | N/A |
|
||||
| 65 | Reload after a panel resize | Width rehydration | Restored panel widths equal the pre-reload widths within 1 px | numeric_tolerance | `± 1px` | N/A |
|
||||
|
||||
### Group 14 — Form hygiene (AC-26 / AC-27)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 66 | Submit a `09_settings` numeric field with empty value | No silent zero | Form does NOT save `0`; submit button disabled OR explicit validation error rendered; NO `PUT` request fires | absent (request), present (validation surface) | N/A | N/A |
|
||||
| 67 | Submit `09_settings` numeric field with non-numeric input | Reject non-numeric | Validation error rendered; NO `PUT` fires | absent (request), present (error) | N/A | N/A |
|
||||
| 68 | `09_settings` save action where the upstream PUT returns HTTP 500 | Error surfacing | A toast / inline error renders within 2 s; `saving` flag returns to `false` (i.e. button is re-enabled); NO route navigation occurs | present (error), exact (state), absent (navigation), threshold_max (2 s) | N/A | N/A |
|
||||
| 69 | `09_settings` save action where the PUT throws (network failure) | Error path via `try/finally` | `saving` flag returns to `false`; user-visible error rendered | exact (state), present (error) | N/A | N/A |
|
||||
|
||||
### Group 15 — CI / image / labels (AC-32)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 70 | Image push step in `.woodpecker/build-arm.yml` for branch `main` | Tag scheme | Pushed tag matches `^main-arm$` | regex | N/A | N/A |
|
||||
| 71 | Image push step | OCI labels | Pushed image carries non-empty labels `org.opencontainers.image.revision`, `org.opencontainers.image.created`, `org.opencontainers.image.source` | present (each), exact (count == 3) | N/A | N/A |
|
||||
| 72 | `org.opencontainers.image.revision` label value | Revision plumbing | Equals `$CI_COMMIT_SHA` from the pipeline run | exact | N/A | N/A |
|
||||
|
||||
### Group 16 — Manual annotation interactions on `CanvasEditor` + `DetectionClasses` (AC-35 / AC-36 / AC-37 / AC-38)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 73 | Synthetic `mousedown(x1,y1) → mousemove(x2,y2) → mouseup` over `CanvasEditor` with `selectedClassNum = C` and `photoMode = P` | Manual bbox draw (AC-35) | Exactly one new local detection appended with `classNum == C + P`, `x == min(x1,x2)/W`, `y == min(y1,y2)/H`, `w == |x2-x1|/W`, `h == |y2-y1|/H` (W,H = canvas pixel size) | numeric_tolerance (per coord), exact (count + classNum) | `± 1px / canvas px` | N/A |
|
||||
| 74 | `mousedown` on resize handle `h ∈ {NW, N, NE, W, E, SW, S, SE}` over a selected bbox, drag by `(dx, dy)`, mouseup | 8-handle resize (AC-36a) | Only the edges adjacent to `h` move; opposite edges unchanged; resulting bbox dimensions clamped to a minimum normalised size > 0 (no negative or zero w/h) | exact (per-edge invariance), threshold_min (w,h > 0) | N/A | N/A |
|
||||
| 75 | `Ctrl+click` on a bbox that is not currently selected | Canvas multi-select (AC-36b) | The bbox is added to the selection set; previous selection is preserved; clicking the same bbox a second time with Ctrl removes it from the set | exact (selection set delta), idempotent (toggle) | N/A | N/A |
|
||||
| 76 | `Ctrl+wheel` over the canvas at cursor position `(cx, cy)` | Canvas zoom (AC-36c) | Viewport zoom level changes; the world point at `(cx, cy)` before zoom maps back to `(cx, cy)` after zoom (zoom-around-cursor invariant) | numeric_tolerance | `± 1 viewport px` | N/A |
|
||||
| 77 | `Ctrl+drag` starting on empty canvas (no bbox under the pointer) | Canvas pan (AC-36d) | Viewport origin translates by exactly `(-dx, -dy)`; no bbox is created or modified | numeric_tolerance, absent (state delta) | `± 1 viewport px` | N/A |
|
||||
| 78 | Mount of `<DetectionClasses>` with a successful `GET /api/annotations/classes` response of N classes (mode-ordered) | Class list load (AC-37 / load path) | All N entries are rendered; the active-mode filter is applied; no fallback list is shown | exact (count rendered), absent (fallback) | N/A | N/A |
|
||||
| 79 | `keydown` on `window` with key `'1'..'9'` while `photoMode = P` and `classes` are mode-ordered per the contract | Class hotkey 1–9 (AC-37 / hotkey path) | `onSelect` fires exactly once with `class.id == classes[(key-1) + P].id`; the visible label index `i+1` on the rendered list element matches `key` | exact | N/A | N/A |
|
||||
| 80 | Click on a class entry in the strip | Class click path (AC-37 / click path) | `onSelect` fires once with that entry's `class.id` | exact (count + value) | N/A | N/A |
|
||||
| 81 | `GET /api/annotations/classes` returning empty or 5xx | Fallback list (AC-37 / fallback) | `FALLBACK_CLASS_NAMES` × 3 PhotoMode offsets is rendered (IDs in the contiguous `[0..N-1, 20..20+N-1, 40..40+N-1]` shape) | exact (set of IDs) | N/A | N/A |
|
||||
| 82 | Click the `Winter` PhotoMode button while `photoMode = 0` | PhotoMode switch — mode + filter (AC-38 / mode set + filter) | Outgoing `onPhotoModeChange(20)` fires once; rendered class list is filtered to entries whose `photoMode == 20` | exact (call + filter) | N/A | N/A |
|
||||
| 83 | PhotoMode switch where the previously-selected `classNum` is NOT in the new filtered set | PhotoMode auto-select (AC-38 / auto-select) | `onSelect` fires once with `modeClasses[0].id` (first class of the new mode) | exact (count + value) | N/A | N/A |
|
||||
| 84 | Save annotation after drawing bbox with `selectedClassNum = C` and `photoMode = P` | yoloId on wire (AC-38 / wire) | Outgoing `POST /api/annotations/annotations` body has `detections[i].classNum == C + P` for the newly drawn detection | exact | N/A | N/A |
|
||||
|
||||
### Group 17 — Tile splitting (AC-39 / AC-40)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 85 | Click `Split tile` action on a dataset item | Split endpoint contract (AC-39 / endpoint) | Exactly one `POST` to URL matching `^/api/annotations/dataset/[0-9]+/split$`; success response is HTTP 200 JSON | exact (count + URL regex + status) | N/A | N/A |
|
||||
| 86 | `AnnotationListItem` with `isSplit: true, splitTile: "3 0.5 0.5 0.2 0.2"` | YOLO label parse — valid (AC-39 / parser happy) | Parser yields `{ classNum: 3, cx: 0.5, cy: 0.5, w: 0.2, h: 0.2 }` without throwing | exact | N/A | N/A |
|
||||
| 87 | `AnnotationListItem` with `isSplit: true, splitTile: "garbage"` | YOLO label parse — malformed (AC-39 / parser sad) | User-visible error surfaced (toast or inline); NO silent swallow; NO render with NaN values | present (error), absent (NaN render) | N/A | N/A |
|
||||
| 88 | `DatasetItem` response containing `isSplit: true` from `GET /api/annotations/dataset` | DatasetItem.isSplit honored (AC-39 / dataset list) | UI reads `item.isSplit` without crash; downstream rendering uses the boolean (rendering policy is per item type, but presence is required) | present (field read) | N/A | N/A |
|
||||
| 89 | Double-click a `splitTile`-bearing annotation in `<AnnotationsSidebar>` | Tile auto-zoom viewport (AC-40 / viewport) | `CanvasEditor` viewport rect equals the tile rect encoded by `splitTile` (per AC-39 parse) within ±1 px per edge | numeric_tolerance | `± 1px per edge` | N/A |
|
||||
| 90 | While tile zoom is active | Tile-zoom indicator (AC-40 / indicator) | A visible tile-zoom indicator (icon or badge) is present in the canvas chrome; clearing the tile zoom removes it | present, absent (after clear) | N/A | N/A |
|
||||
|
||||
### Group 18 — Anti-criteria (AC-N1..AC-N5) — negative behavioral assertions
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 91 | Two browsers editing the same annotation simultaneously | No collaborative-edit semantics (AC-N1) | UI MUST NOT reconcile concurrent edits; last-write-wins on the server is the expected behavior (no merge UI, no presence indicators) | absent (merge UI) | N/A | N/A |
|
||||
| 92 | Static dependency scan of `package.json` and `mission-planner/package.json` | No in-browser ML (AC-N2) | No package matching the pattern `(onnxruntime|tensorflow|tflite|coreml|tfjs|@tensorflow/.*|@huggingface/.*|transformers\.js)` is declared | absent | N/A | N/A |
|
||||
| 93 | App boot in a network-disabled environment (offline) | No offline mode (AC-N3) | App enters an error / login-failed state; does NOT serve cached app data; no service worker registered | present (error), absent (sw) | N/A | N/A |
|
||||
| 94 | Static dependency scan | No response-signature library (AC-N4) | No package matching `(jsrsasign|tweetnacl|@noble/.*|jose)` is imported on the request-validation path | absent | N/A | N/A |
|
||||
| 95 | Code-search across `src/` and `mission-planner/` for symbols/components named `SoundDetections|DroneMaintenance` | Dropped legacy features (AC-N5) | `match_count == 0` | exact | N/A | N/A |
|
||||
|
||||
### Group 19 — Phase-3-added (Data Validation Gate)
|
||||
|
||||
| # | Input | Input Description | Expected Result | Comparison | Tolerance | Reference File |
|
||||
|----|-------|-------------------|-----------------|------------|-----------|---------------|
|
||||
| 96 | Click `Download` in `<AnnotationsPage>` on a canvas tainted by a cross-origin video frame (CORS-less video source) | Tainted-canvas fallback (NFT-RES-09; derived from `modules/src__features__annotations.md` finding on `handleDownload`) | A user-visible error (toast or inline message) is rendered; NO silent swallow; NO `alert()` invoked; no fabricated blob is offered for download | present (error), absent (silent swallow), absent (`alert()` invocation) | N/A | N/A |
|
||||
| 97 | Server force-closes an open `EventSource` (live-GPS or annotation-status) without rotation | SSE server disconnect indicator (NFT-RES-10; derived from AC-08 + AC-09 + AC-24) | UI either renders a connection-lost indicator (badge/icon) OR invokes a reconnect attempt within 10 s. Stale event data is NOT re-rendered as live; the most recent live timestamp is frozen + flagged stale | present (indicator OR reconnect), absent (stale data rendered as live), exact (reconnect_attempts ≤ 1 in the 10 s window) | dt ≤ 10 000 ms | N/A |
|
||||
| 98 | Warm-cache navigation to `/flights` on the post-login route in headless Chromium on the edge profile (2 vCPU / 4 GB RAM) | First Contentful Paint baseline (NFT-PERF-10; derived from AC-11 target + H2 edge deploy) | `performance.getEntriesByName('first-contentful-paint')[0].startTime` reported by the browser is below the threshold | threshold_max | `≤ 3 000 ms` | N/A |
|
||||
|
||||
---
|
||||
|
||||
## Coverage Summary
|
||||
|
||||
| AC ID | Mapped row(s) | Coverage |
|
||||
|--------|---------------|----------|
|
||||
| AC-01 | 01, 02, 03 | full (default + bootstrap + 401 retry) |
|
||||
| AC-02 | 04 | full |
|
||||
| AC-03 | 05, 06, 07 | full (JS-readable check, fixture cookie regex) |
|
||||
| AC-04 | 14, 15, 16, 17, 18, 19 | full (per enum + payload contract) |
|
||||
| AC-05 | 22, 23 | full (URL + required body fields) |
|
||||
| AC-06 | 32, 33 | full (write path + boot rehydration) |
|
||||
| AC-07 | 36, 37 | full (request + UI reflection) |
|
||||
| AC-08 | 34, 35 | full (open + close) |
|
||||
| AC-09 | 24, 25 | full (subscribe + unsubscribe) |
|
||||
| AC-10 | 38, 39 | full (server cap + UI surfacing) |
|
||||
| AC-11 | 40 | target (no CI gate today; row documents the threshold) |
|
||||
| AC-12 | 45, 46 | full |
|
||||
| AC-13 | 47, 48 | full |
|
||||
| AC-14 | 49, 50, 51 | full (dialog presence + alert ban + general policy) |
|
||||
| AC-15 | 52, 53, 54 | full |
|
||||
| AC-16 | 55, 56, 57 | full |
|
||||
| AC-17 | 58, 59 | full |
|
||||
| AC-18 | 60 | manual smoke today (test row exists; gate is target) |
|
||||
| AC-19 | 61, 62 | full (two breakpoint smokes) |
|
||||
| AC-20 | 63 | full |
|
||||
| AC-21 | 64, 65 | full |
|
||||
| AC-22 | 08, 09, 10 | full (admin + login + settings) |
|
||||
| AC-23 | 11, 12 | full |
|
||||
| AC-24 | 13 | target (Phase B / Step 8 hardening) |
|
||||
| AC-25 | 26, 27, 28 | full (sync image + async video + token header) |
|
||||
| AC-26 | 66, 67 | full |
|
||||
| AC-27 | 68, 69 | full |
|
||||
| AC-28 | 29, 30, 31 | full (in-window + below + above) |
|
||||
| AC-29 | 20, 21 | full |
|
||||
| AC-30 | 49 | overlapped with AC-14 row 49 |
|
||||
| AC-31 | 41 | full |
|
||||
| AC-32 | 70, 71, 72 | full |
|
||||
| AC-33 | 42 | full |
|
||||
| AC-34 | 43, 44 | full (route set + prefix strip) |
|
||||
| AC-35 | 73 | full (one-shot draw assertion) |
|
||||
| AC-36 | 74, 75, 76, 77 | full (resize + multi-select + zoom + pan) |
|
||||
| AC-37 | 78, 79, 80, 81 | full (load + hotkey + click + fallback) |
|
||||
| AC-38 | 82, 83, 84 | full (mode set + filter + auto-select + wire yoloId) |
|
||||
| AC-39 | 85, 86, 87, 88 | full (endpoint + parser happy + parser sad + DatasetItem flag) |
|
||||
| AC-40 | 89, 90 | target (UX missing today — finding #24; rows assert when implementation lands) |
|
||||
| AC-N1 | 91 | full |
|
||||
| AC-N2 | 92 | full |
|
||||
| AC-N3 | 93 | full |
|
||||
| AC-N4 | 94 | full |
|
||||
| AC-N5 | 95 | full |
|
||||
| (NFT-RES-09 anchor) | 96 | added Phase 3 — tainted-canvas fallback observable |
|
||||
| (NFT-RES-10 anchor) | 97 | added Phase 3 — SSE server-disconnect observable |
|
||||
| (NFT-PERF-10 anchor) | 98 | added Phase 3 — FCP baseline threshold |
|
||||
|
||||
Every AC + anti-criterion has at least one row. Every row is quantifiable.
|
||||
|
||||
## Open Items For Phase 1 Validation
|
||||
|
||||
- **AC-04 enum maps**: rows 14–17 reference "the spec map" for `MediaStatus`,
|
||||
`Affiliation`, `CombatReadiness`. The exact numeric values must be harvested
|
||||
from the suite spec at Phase 1 time and inlined; they are deliberately left
|
||||
symbolic here because the UI today drifts (per `restrictions.md` O7 + the
|
||||
data-parameters drift notes) and we want the test to assert against the
|
||||
spec, not against the current `src/types/index.ts`.
|
||||
- **AC-11 / AC-18 / AC-24 / AC-25 (async) / AC-40** are flagged "Phase B /
|
||||
target" or "Step 4 fix" in `acceptance_criteria.md`. The rows above produce
|
||||
verifiable assertions when those features ship; until then `/test-spec`
|
||||
Phase 3 may downgrade them from blocking to documentary. AC-40 in particular
|
||||
has zero consumer of `splitTile` in the UI today (finding #24 in
|
||||
`modules/src__features__annotations.md`); its rows are written so the test
|
||||
exists the day the tile-zoom UX ships.
|
||||
- **AC-37 backend ordering**: the class-hotkey contract depends on the
|
||||
`annotations/` service returning classes in the contiguous
|
||||
`[0..N-1, 20..20+N-1, 40..40+N-1]` shape. This was flagged Step 4
|
||||
verification in `data_model.md:158`. If the backend ordering does not match,
|
||||
AC-37 row 79 will fail at integration time and we may need a server-side
|
||||
fix or a client-side resort.
|
||||
- **Reference files**: none required at this stage. If `/test-spec` Phase 2
|
||||
produces a per-route JSON expectation that doesn't fit a single row,
|
||||
reference files will be added in this folder following the
|
||||
`<input_name>_expected.<ext>` convention.
|
||||
Reference in New Issue
Block a user