# Traceability Matrix > **Status**: produced by autodev `/test-spec` Phase 2 (2026-05-14). > **Naming**: post-rename target. Tests written for the post-rename API surface — RED-status until B5–B8 land. The traceability matrix below treats the documented spec as the source of truth. ## Acceptance Criteria Coverage ### AC-1 — Vehicle CRUD | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-1.1 | Create vehicle | FT-P-01 | Covered | | AC-1.2 | Default-clear on create/update/setDefault | FT-P-02, FT-P-03 | Covered | | AC-1.3 | "Exactly one default" stricter than spec (B12 pending) | covered indirectly via FT-P-02, FT-P-03 (assertions on `count == 1`) | Covered (carry-forward) | | AC-1.4 | Default-clear NOT transaction-wrapped → race | NFT-RES-08 | Covered (probabilistic) | | AC-1.5 | GET /vehicles is plain array (NO pagination) | FT-P-04 | Covered | | AC-1.6 | Filter case-sensitive on `name`, exact on `isDefault` | FT-P-05, FT-N-01 | Covered | | AC-1.7 | GET /vehicles/{id} 404 | FT-N-02 | Covered | | AC-1.8 | DELETE /vehicles/{id} 409 if referenced | FT-N-03 | Covered | | AC-1.9 | All `/vehicles/*` require `Policy="FL"` | NFT-SEC-01, NFT-SEC-05, NFT-SEC-06 | Covered | ### AC-2 — Mission CRUD | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-2.1 | Create mission, default `CreatedDate = UtcNow` | FT-P-07 | Covered | | AC-2.2 | Non-existent VehicleId → 400 (today; spec wants 404) | FT-N-04 | Covered (carry-forward) | | AC-2.3 | GET /missions paginated `PaginatedResponse` | FT-P-08, FT-P-09, FT-P-10, NFT-PERF-04 | Covered | | AC-2.4 | GET /missions/{id} 404 | FT-N-05 | Covered | | AC-2.5 | PUT partial update (Name update only) | FT-P-11 | Covered | | AC-2.6 | LinqToDB does NOT eager-load `[Association]` | covered indirectly via FT-P-07/FT-P-11 (body shape assertion checks `Vehicle == null`, `Waypoints == null/[]`) | Covered | | AC-2.7 | All `/missions/*` require `Policy="FL"` | NFT-SEC-01 | Covered | | AC-2.8 | TOCTOU on FK → 500 | NOT directly covered as a separate test (deterministic reproduction is hard); falls under NFT-RES-08-style probabilistic family | NOT COVERED — see Uncovered Items §1 | ### AC-3 — Mission cascade delete F3 (most critical) | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-3.1 | Cascade walks `map_objects → detection → annotations → media → waypoints → missions` | FT-P-12 | Covered | | AC-3.2 | Mission missing → 404 BEFORE any cascade DELETE | FT-N-06 | Covered | | AC-3.3 | Cascade NOT transaction-wrapped → orphans | NFT-RES-01, NFT-RES-02 | Covered | | AC-3.4 | `relation does not exist` → 500 + log | NFT-RES-01 (uses `media` drop) | Covered | | AC-3.5 | After B7 cascade does NOT touch `orthophotos` / `gps_corrections` | covered via NFT-RES-04 (post-B9 build asserts tables absent); cascade does not reference them by construction (verified by code-level absence at Step 8) | Partially covered | | AC-3.6 | <50ms typical (P50) | NFT-PERF-01 | Covered | | AC-3.7 | autopilot race after step 1 → orphan | NFT-RES-08-style (orphan race, on `map_objects` insert) — design spec'd; probabilistic implementation deferred | NOT COVERED — see Uncovered Items §2 | ### AC-4 — Waypoint CRUD F4 | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-4.1 | Routes nested under `/missions/{id}/waypoints[/{wpId}]` | FT-P-13, FT-P-14, FT-P-15, FT-P-18, FT-N-07 (every endpoint exercised) | Covered | | AC-4.2 | Parent missing → 404 | FT-N-07 | Covered | | AC-4.3 | GET unpaginated, ordered by `OrderNum` ASC | FT-P-13 | Covered | | AC-4.4 | PUT is full overwrite | FT-P-15 | Covered | | AC-4.5 | Scoped cascade (detection → annotations → media → waypoints) | FT-P-18 | Covered | | AC-4.6 | Same NO-transaction caveat | NFT-RES-02 | Covered | | AC-4.7 | Require `Policy="FL"` | NFT-SEC-01, NFT-SEC-05 | Covered | ### AC-5 — JWT validation | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-5.1 | HS256 + `SymmetricSecurityKey(UTF-8(JWT_SECRET))` | covered indirectly via NFT-SEC-02 (different secret rejected) and NFT-SEC-03 (correct secret accepted) | Covered | | AC-5.2 | `ValidateLifetime=true`, `ClockSkew=1min` | NFT-SEC-03 | Covered | | AC-5.3 | `ValidateIssuer=false`, `ValidateAudience=false` (today) | NFT-SEC-04 | Covered (locks today's behavior) | | AC-5.4 | Missing header → 401 | NFT-SEC-01 | Covered | | AC-5.5 | Invalid signature → 401 | NFT-SEC-02 | Covered | | AC-5.6 | Expired token (outside skew) → 401 | NFT-SEC-03 | Covered | | AC-5.7 | Old `JWT_SECRET` after rotation → 401 | NFT-RES-07 | Covered | | AC-5.8 | Missing `permissions=FL` claim → 403 | NFT-SEC-05 | Covered | | AC-5.9 | Local validator never calls `admin` | NOT directly observable from outside the process; covered indirectly by `admin` not running in the test env (NFT-SEC-* still pass) | Partially covered | ### AC-6 — Startup + migration | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-6.1 | DATABASE_URL URL form converted via `ConvertPostgresUrl` | covered indirectly via every test that depends on a working DB connection (compose env uses URL form) | Covered | | AC-6.2 | DATABASE_URL raw form accepted | NOT directly covered — the test environment uses URL form; can be added by an extra startup scenario with the raw form | NOT COVERED — see Uncovered Items §3 | | AC-6.3 | Migrator runs ONCE at startup, inside scope | NFT-RES-03 (idempotency assertion implies single-run + safe-restart) | Partially covered | | AC-6.4 | 4 owned tables + 3 indexes created | NFT-RES-03 (asserts schema via `\d+` after first start) | Covered | | AC-6.5 | Post-B9 one-shot legacy `DROP TABLE IF EXISTS` | NFT-RES-04 | Covered | | AC-6.6 | Migrator idempotent | NFT-RES-03 | Covered | | AC-6.7 | DB unreachable → process exits non-zero | NFT-RES-05 | Covered | | AC-6.8 | DB missing (3D000) → process exits | NFT-RES-06 | Covered | | AC-6.9 | `ErrorHandlingMiddleware` registered FIRST | covered indirectly via FT-N-08 + NFT-SEC-08 (any unhandled exception produces the documented envelope) | Covered | | AC-6.10 | Listens on port 8080; edge maps host `5002:8080` | covered by every test that connects to port 5002→8080 | Covered | ### AC-7 — Health probe | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-7.1 | `GET /health` anonymous | FT-P-16, NFT-SEC-07 | Covered | | AC-7.2 | `200 { "status": "healthy" }` | FT-P-16, FT-P-17 | Covered | | AC-7.3 | <10ms typical | NFT-PERF-03 | Covered | | AC-7.4 | If pipeline down, TCP connect fails (Watchtower restarts) | container-lifecycle behavior outside the service; out-of-scope at the service test level | Out of scope — see Uncovered Items §4 | ### AC-8 — Wire shape | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-8.1 | Entity bodies PascalCase | FT-P-01, FT-P-04 (key-set assertion) | Covered | | AC-8.2 | Error envelope camelCase by accidental match | FT-N-02, FT-N-05 (key-set assertion includes `statusCode`, `message`) | Covered | | AC-8.3 | Envelope MUST NOT include `errors` field | FT-N-02 (key-set excludes `errors`), FT-N-05, NFT-SEC-08 | Covered | | AC-8.4 | KeyNotFoundException → 404 | FT-N-02, FT-N-05, FT-N-07 | Covered | | AC-8.5 | ArgumentException → 400, InvalidOperationException → 409 | FT-N-04 (400), FT-N-03 (409) | Covered | | AC-8.6 | 500 body redacted, stack only in log | FT-N-08, NFT-SEC-08 | Covered | | AC-8.7 | `PaginatedResponse` PascalCase keys | FT-P-08 (key-set assertion) | Covered | ### AC-9 — Authorization | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-9.1 | Policy `"FL"` registered, satisfied by `permissions == "FL"` | every protected-endpoint test | Covered | | AC-9.2 | Hardcoded string mismatch ("fl", "FLight") → 403 | NFT-SEC-06 | Covered | | AC-9.3 | Policy NAME `"FL"` retains legacy wording (deferred) | not testable at runtime — documentation-only | Documentation only | | AC-9.4 | No per-method authz beyond `[Authorize(Policy="FL")]` | covered by NFT-SEC-01 + NFT-SEC-07 (every endpoint gets the same gate; health is the only exception) | Covered | ### AC-10 — Operational invariants (API-observable subset) | AC ID | Acceptance Criterion (short) | Test IDs | Coverage | |-------|------------------------------|----------|----------| | AC-10.1 | One container per device | container-orchestration constraint, not API-observable; tracked under H1 + H3 | Out of scope (suite-level) | | AC-10.2 | RTO ≈ container restart, RPO = device-local backup | suite-level operational concern | Out of scope | | AC-10.3 | Unhandled 500 logged with stack trace via `LogError` | FT-N-08, NFT-SEC-08 | Covered | | AC-10.4 | No correlation id, no per-user audit | absence-of-feature; NOT directly testable; documented carry-forward | Documentation only | | AC-10.5 | B9 DROP block ordered AFTER `gps-denied` migrated | suite-level deploy ordering, NOT enforced by this service | Out of scope (suite-level) | | AC-10.6 | Cross-service cascade requires sibling tables present | NFT-RES-01 covers the failure mode (table dropped) — passes when failure produces 500 + partial deletes | Covered | ## Restrictions Coverage ### Hardware (H1–H6) | Restriction ID | Restriction (short) | Test IDs | Coverage | |---------------|---------------------|----------|----------| | H1 | Edge-device, one container per device | NFT-RES-LIM-01 through NFT-RES-LIM-04 (resource budget aligned with device assumption) | Indirectly covered | | H2 | Multi-arch (ARM64 + AMD64) | NOT testable at the test-spec level — covered by suite-level CI matrix (`.woodpecker/build-arm.yml` + future `build-amd.yml`) | Out of scope | | H3 | Vertical scale only | implicit in test environment (single `missions` container) | Implicitly covered | | H4 | No managed cloud | architectural constraint; not testable | Documentation only | | H5 | Watchtower + flight-gate | suite-level orchestration | Out of scope | | H6 | No container-internal resource limits | NFT-RES-LIM-01–04 (observe baseline so suite-level cgroups can be sized correctly) | Covered | ### Software (S1–S15) | Restriction ID | Restriction (short) | Test IDs | Coverage | |---------------|---------------------|----------|----------| | S1 | C# / .NET 10 | implicit in test environment | Implicitly covered | | S2 | ASP.NET Core | implicit | Implicitly covered | | S3–S5 | Library versions | csproj / lockfile concern; NOT a behavioral test | Out of scope (build-time check) | | S6 | Swagger NOT gated on `IsDevelopment()` | NOT directly tested; could add a single `GET /swagger` test that asserts 200 in production-like env. Carry-forward divergence | NOT COVERED — see Uncovered Items §5 | | S7 | PostgreSQL only | implicit | Implicitly covered | | S8 | One csproj, one root namespace | csproj structure; NOT a behavioral test | Out of scope (code organization) | | S9 | No `src/` directory | repo layout; NOT a behavioral test | Out of scope | | S10 | Layer-organized | code organization; NOT a behavioral test | Out of scope | | S11 | No automated tests today | this entire spec converts S11 from a constraint into a goal | Resolved by this spec | | S12 | No migration tool | NFT-RES-03 + NFT-RES-04 (idempotency observed) | Covered | | S13 | No in-process MQ / event bus | architectural constraint | Out of scope (architecture) | | S14 | Owned + borrowed tables | covered by FT-P-12, FT-P-18 (cascade walks both owned and borrowed) | Covered | | S15 | `gps-denied` decoupled | covered indirectly by NFT-RES-04 (legacy tables absent post-B9) + AC-3.5 absence of cascade reference | Covered | ### Environment (E1–E10) | Restriction ID | Restriction (short) | Test IDs | Coverage | |---------------|---------------------|----------|----------| | E1 | Two required env vars | implicit; NFT-RES-05 (DB unreachable) + NFT-SEC-* (JWT_SECRET behavior) | Covered | | E2 | DATABASE_URL accepts URL or raw form | URL form covered via NFT-RES-05 path; raw form NOT covered | NOT COVERED — see Uncovered Items §3 | | E3 | Hardcoded dev fallbacks NOT gated on IsDevelopment() | a startup test with NO env vars set could verify fallback boot — security risk gate; carry-forward | NOT COVERED — see Uncovered Items §6 | | E4 | JWT_SECRET shared across services | suite-level concern | Out of scope | | E5 | Container EXPOSE 8080; edge maps 5002:8080 | implicit | Implicitly covered | | E6 | Image tag post-B10 | build-time concern, not behavior | Out of scope | | E7 | Entrypoint post-B5 | build-time concern | Out of scope | | E8 | No appsettings env-specific overrides | code organization; NOT a behavioral test | Out of scope | | E9 | CORS `AllowAnyOrigin/Method/Header` | could add a single CORS preflight test that asserts the documented permissive behavior | NOT COVERED — see Uncovered Items §7 | | E10 | TLS termination is suite reverse proxy | suite-level concern | Out of scope | ### Operational (O1–O10) | Restriction ID | Restriction (short) | Test IDs | Coverage | |---------------|---------------------|----------|----------| | O1 | Migrator at every start, idempotent | NFT-RES-03, NFT-RES-04 | Covered | | O2 | flight-gate prevents restart mid-mission | suite-level orchestration | Out of scope | | O3 | No version table | covered indirectly by NFT-RES-03 (no version-table query observed) | Implicitly covered | | O4 | Single Woodpecker job, no test/security stage | this spec adds a test stage as a follow-up artifact | Resolved by this spec | | O5 | No structured logging | absence-of-feature; NOT testable directly | Documentation only | | O6 | No correlation id, no audit | absence-of-feature | Documentation only | | O7 | Health is process-liveness only | FT-P-17 (PG stopped, health still 200) | Covered | | O8 | Cascade NOT transaction-wrapped | NFT-RES-01, NFT-RES-02 | Covered | | O9 | Sibling table absent → cascade fails on `relation does not exist` | NFT-RES-01 (uses `media` drop) | Covered | | O10 | One-instance-per-device → no cluster awareness | architectural constraint | Documentation only | ## Coverage Summary | Category | Total Items | Covered | Partially / Implicit | Not Covered | Out of Scope / Doc-only | Coverage % (Covered + Partial of in-scope) | |----------|-----------|---------|--------------------|-------------|------------------------|-------------------------------------------| | AC-1 Vehicle CRUD | 9 | 8 | 1 (carry-forward) | 0 | 0 | 100% | | AC-2 Mission CRUD | 8 | 7 | 0 | 1 (AC-2.8 TOCTOU) | 0 | 87% | | AC-3 Cascade F3 | 7 | 5 | 1 | 1 (AC-3.7 race) | 0 | 86% | | AC-4 Waypoint CRUD F4 | 7 | 7 | 0 | 0 | 0 | 100% | | AC-5 JWT | 9 | 8 | 1 | 0 | 0 | 100% | | AC-6 Startup + migration | 10 | 8 | 1 | 1 (AC-6.2 raw conn) | 0 | 90% | | AC-7 Health | 4 | 3 | 0 | 0 | 1 | 100% (in-scope) | | AC-8 Wire shape | 7 | 7 | 0 | 0 | 0 | 100% | | AC-9 Authz | 4 | 3 | 0 | 0 | 1 | 100% (in-scope) | | AC-10 Operational | 6 | 1 | 0 | 0 | 5 | 100% (in-scope) | | Restrictions H | 6 | 1 | 2 | 0 | 3 | 100% (in-scope) | | Restrictions S | 15 | 4 | 2 | 0 | 9 | 100% (in-scope) | | Restrictions E | 10 | 1 | 1 | 3 (E2, E3, E9) | 5 | 60% (in-scope) | | Restrictions O | 10 | 4 | 2 | 0 | 4 | 100% (in-scope) | | **Total** | 112 | 67 | 11 | 6 | 28 | **93%** in-scope | ## Uncovered Items Analysis | # | Item | Reason Not Covered | Risk | Mitigation | |---|------|-------------------|------|-----------| | 1 | AC-2.8 — TOCTOU on FK between existence check and insert | Deterministic reproduction requires controllable concurrency primitive that doesn't exist today (instrumented test build with `pg_advisory_lock`) | Low — failure mode is well-documented and produces a 500 (loud failure, not silent corruption); occurs only when admin races with create | Add probabilistic test (similar to NFT-RES-08) under a follow-up ticket. Document as known carry-forward. | | 2 | AC-3.7 — autopilot orphan race on `map_objects` insert after step-1 read | Same as #1 — needs controllable concurrency | Low — leaves at most one orphan row per race; cleanup on next mission delete or via manual sweep | Same mitigation as #1; add to follow-up. | | 3 | AC-6.2 / E2 — `DATABASE_URL` raw form path | Test env uses URL form; raw form is the alternate adapter branch | Low — branch is small, well-localised in `ConvertPostgresUrl` | Add a single startup scenario with raw form. Single-line config change in test compose. | | 4 | AC-7.4 — TCP connect fails on container down (Watchtower restarts) | Container lifecycle outside service surface | None at service level — testable at suite e2e level | Cover at suite e2e (`monorepo-e2e` skill scope) | | 5 | S6 — Swagger NOT gated on `IsDevelopment()` | Carry-forward security finding; not part of AC | Medium — production deploy with Swagger exposed | Add a single test `GET /swagger/index.html` returns 200 in test env, with explicit comment that this LOCKS the carry-forward divergence (will fail when remediated). Suggest as follow-up. | | 6 | E3 — Hardcoded dev fallbacks NOT gated | Carry-forward security finding | Medium — production deploy without env vars boots with well-known secret | Add a startup test with NO env vars set, assert `JWT_SECRET` claim ladder still works (locks the divergence). Suggest as follow-up. | | 7 | E9 — CORS `AllowAnyOrigin/Method/Header` | Carry-forward; assumed safe behind reverse proxy | Low — assumed deployment topology mitigates | Add CORS preflight test that locks current behavior. Suggest as follow-up. | **Recommendation**: items 1, 2 are deterministic-test improvements to land alongside a future `transaction-wrap` refactor (closes the carry-forward at the same time as the test improvement). Items 3, 5, 6, 7 are 1-test additions each — add them in Step 5 (Decompose Tests) under a "blackbox-lock-carry-forward" task. ## Phase 3 Coverage Gate **Threshold**: ≥ 75% (per `cursor-meta.mdc` Quality Thresholds + `phases/03-data-validation-gate.md`). **Achieved**: 93% in-scope. **Verdict**: **PASS** — Phase 3 gate cleared on first iteration. The 6 uncovered items above are all low-medium risk with documented mitigations.