mirror of
https://github.com/azaion/satellite-provider.git
synced 2026-06-21 13:41:15 +00:00
ec0eb909a1
Phase 12 of autodev existing-code flow — cycle-update mode of the
test-spec skill. Append cycle-8 coverage to the documentation suite
without rewriting any pre-cycle-8 content.
blackbox-tests.md
- Add 4 new BT entries (BT-28..BT-31) — one per cycle-8 endpoint:
- BT-28: Region request endpoint strict validation + OSM rename
(AZ-808 + AZ-812; 11 sub-cases through the new `RegionRequest
Validator` + the AZ-795 deserializer infra; sub-case `pos` proves
the new `lat`/`lon` names accepted, sub-case `9` proves the old
`latitude`/`longitude` rejected by `UnmappedMemberHandling.Disallow`).
- BT-29: Create route endpoint nested + cross-field validation
(AZ-809; 15 sub-cases covering nested per-point validators,
geofence cross-field invariants, and the `createTilesZip` /
`requestMaps` cross-field rule; advisory ACs 9 + 10 explicitly
NOT tested per spec).
- BT-30: UAV upload metadata multipart validation (AZ-810; 14
sub-cases across the three-layer composition: deserializer,
FluentValidation, envelope cross-field; documents the unique
`errors["metadata"]` vs `errors["metadata.items[i].field"]` key
convention for multipart endpoints).
- BT-31: GET tiles/latlon query-param validation + unknown-param
rejection (AZ-811; 8 sub-cases; sub-cases 4b + 4c prove the
novel `UnknownQueryParameterEndpointFilter` rejects both
legacy and hostile unknown query keys).
traceability-matrix.md
- Append 41 AC rows (AZ-808 AC-1..AC-8, AZ-809 AC-1..AC-10,
AZ-810 AC-1..AC-9, AZ-811 AC-1..AC-9, AZ-812 AC-1..AC-6).
- Update Coverage Summary: cycle-8 row added; Total moves from
126 tests / 75 ACs to 167 tests / 116 ACs.
- Add "Coverage shape notes (Cycle 8 ...)" section explaining the
multipart enforcement shape (AZ-810), the new query-param filter
(AZ-811), the AZ-808 + AZ-812 same-cycle coordination, and the
AZ-810 AC-9 process annotation (false-PASS by source tracing →
bound to green full-suite re-run after the test-data coord-clamp
fix in commit b763da3).
- AZ-809 AC-9 + AC-10 marked as `◐ advisory (not tested)` —
naming-consistency concerns surfaced for parent-suite team
decision.
State
- Advance autodev to Step 13 (Update Docs), sub_step phase 0
awaiting-invocation.
No production code change; no contract change; no test code change.
Co-authored-by: Cursor <cursoragent@cursor.com>
63 KiB
63 KiB
Traceability Matrix
Acceptance Criteria → Test Mapping
| AC | Description | Tests | Coverage |
|---|---|---|---|
| T1 | Tiles cached, not re-downloaded | BT-02 | ✓ |
| T2 | Concurrent download limit | RS-05, RL-03 | ✓ |
| T3 | Tile stored with correct path | BT-01 | ✓ |
| T4 | Tile metadata persisted | BT-01 | ✓ |
| R1 | Region state transitions | BT-03, BT-04, BT-05 | ✓ |
| R2 | CSV manifest generated | BT-03, BT-04, BT-05 | ✓ |
| R3 | Summary file generated | BT-03, BT-04, BT-05 | ✓ |
| R4 | Stitched image when requested | BT-05 | ✓ |
| R5 | Stitched image valid content | BT-05 | ✓ |
| R6 | Region processing bounded | RL-04 | ✓ |
| RT1 | Points interpolated at ~200m | BT-06 | ✓ |
| RT2 | Point types correctly assigned | BT-06 | ✓ |
| RT3 | Total distance calculated | BT-06 | ✓ |
| RT4 | Geofence filtering applied | BT-11 | ✓ |
| RT5 | ZIP ≤ 50 MB | BT-09, RL-01 | ✓ |
| RT6 | Route map stitched | BT-08, BT-10, BT-12 | ✓ |
| A1 | Region request returns immediately | BT-03 | ✓ |
| A2 | Status endpoint reflects state | BT-03, BT-07 | ✓ |
| A3 | Route returns computed metadata | BT-06 | ✓ |
| S1 | Migrations run on startup | RS-02 | ✓ |
| S2 | Queue rejects when full | RS-04, RL-02 | ✓ |
| S3 | Failed regions marked failed | RS-03 | ✓ |
| AZ-484 AC-1 | Schema accepts source + captured_at; multi-source rows coexist under 5-col unique index | MultiSourceInsertCoexistsUnderNewIndex_AZ484_AC1, NewUniqueConstraintIncludesSourceColumn_AZ484_AC1 (integration) |
✓ |
| AZ-484 AC-2 | Read returns most-recent across sources | MostRecentAcrossSourcesSelection_AZ484_AC2 (integration) |
✓ |
| AZ-484 AC-3 | Same-source UPSERT collapses to one row with refreshed captured_at | SameSourceUpsertReplacesPreviousRow_AZ484_AC3 (integration) |
✓ |
| AZ-484 AC-4 | Migration 013 backfill leaves no orphans (count preserved, source='google_maps', captured_at=created_at) | BackfillUpdateAssignsGoogleMapsAndCapturedAt_AZ484_AC4 (integration) |
✓ |
| AZ-484 AC-5 | Google Maps download path stamps Source='google_maps' (wire) + CapturedAt UTC | BuildTileEntity_SetsGoogleMapsSourceAndUtcCapturedAt_AZ484_AC5 (unit) |
✓ |
| AZ-484 AC-6 | Existing region/route flows unchanged post-T1 (200 unit + smoke baseline preserved) | Full unit suite (213 tests) + integration smoke scenarios BT-01..BT-12 | ✓ |
| AZ-484 AC-7 | Vision + contract docs amended (architecture.md, glossary.md, module-layout.md, tile-storage.md frozen v1.0.0) | doc-state AC; verified by monorepo-document reviews |
✓ |
| AZ-487 AC-1 | Anonymous request returns 401 from every authenticated endpoint | SEC-05 (blackbox); JwtIntegrationTests.AnonymousRequest_*_Returns401 (integration) |
✓ |
| AZ-487 AC-2 | Expired token returns 401; no internal leak in body | SEC-06 (blackbox); JwtIntegrationTests.ExpiredToken_Returns401 (integration) |
✓ |
| AZ-487 AC-3 | Tampered signature returns 401 | SEC-07 (blackbox); JwtIntegrationTests.InvalidSignature_Returns401 (integration) |
✓ |
| AZ-487 AC-4 | Valid token reaches handler with identical response | SEC-09, BT-18 (blackbox); JwtIntegrationTests.ValidToken_Returns200_OnHealthyEndpoint (integration) |
✓ |
| AZ-487 AC-5 | Startup fails on missing / short JWT_SECRET |
SEC-08 (behavioral); AuthenticationServiceCollectionExtensionsTests.AddSatelliteJwt_Throws* (unit) |
✓ |
| AZ-487 AC-6 | HttpContext.User exposes claims (sub, permissions, …) |
JwtTokenFactoryTests.Create_WithExtraClaims_PropagatesClaimsThroughValidation (unit) + indirect via AZ-488 AC-6 (live permission check) |
✓ |
| AZ-487 AC-7 | Swagger UI Authorize button works | JwtIntegrationTests.SwaggerDocument_AdvertisesBearerSecurityScheme (integration; programmatic equivalent of UI flow) |
◐ doc-verified |
| AZ-487 AC-8 | All existing tests pass with attached test token | Full scripts/run-tests.sh --full run (cycle 2 Step 11 — passed) |
✓ |
| AZ-488 AC-1 | Happy-path 1-item batch persists with source='uav' |
BT-13 (blackbox); UavUploadTests.HappyPathSingleItem_PersistsRow (integration) |
✓ |
| AZ-488 AC-2 | 3-item mixed batch returns per-item results | BT-14 (blackbox); UavUploadTests.MixedBatch_ReturnsPerItemResults (integration) |
✓ |
| AZ-488 AC-3 | UAV upload coexists with pre-seeded google_maps row |
BT-15 (blackbox); UavUploadTests.MultiSourceCoexistence_AZ484_Cycle2 (integration); reuses AZ-484 AC-1 + AC-2 invariants |
✓ |
| AZ-488 AC-4 | Same-source UPSERT keeps one source='uav' row |
BT-16 (blackbox); UavUploadTests.SameSourceUpsert_AZ484_Cycle2 (integration); reuses AZ-484 AC-3 invariant |
✓ |
| AZ-488 AC-5 | Unauthenticated upload returns 401 (covered by AZ-487) | UavUploadTests.NoToken_Returns401 (integration); AZ-487 AC-1 row covers contract |
✓ |
| AZ-488 AC-6 | Authenticated request without GPS permission returns 403 |
SEC-10 (blackbox); UavUploadTests.ValidTokenWithoutGpsPermission_Returns403 (integration); PermissionsRequirementTests (unit) |
✓ |
| AZ-488 AC-7a | INVALID_FORMAT reject reason on wrong content-type or magic bytes |
UavTileQualityGateTests.Validate_NonJpegContentType_* and Validate_WrongMagicBytes_* (unit) |
✓ |
| AZ-488 AC-7b | SIZE_OUT_OF_BAND reject reason on bytes outside [MinBytes, MaxBytes] |
RL-06 (resource-limit); UavTileQualityGateTests.Validate_BytesBelowMin_* and Validate_BytesAboveMax_* (unit) |
✓ |
| AZ-488 AC-7c | WRONG_DIMENSIONS reject reason on non-256×256 images |
BT-14, BT-17 (blackbox); UavTileQualityGateTests.Validate_WrongDimensions_* (unit) |
✓ |
| AZ-488 AC-7d | CAPTURED_AT_FUTURE / CAPTURED_AT_TOO_OLD reject reasons |
UavTileQualityGateTests.Validate_CapturedAtFuture_* and Validate_CapturedAtTooOld_* (unit) |
✓ |
| AZ-488 AC-7e | IMAGE_TOO_UNIFORM reject reason on uniform / low-variance JPEGs |
UavTileQualityGateTests.Validate_UniformGreyImage_RejectsImageTooUniform (unit) |
✓ |
| AZ-488 AC-7 (ordering) | First-applicable rule wins (e.g. format-fail beats dimensions-fail) | BT-17 (blackbox); UavTileQualityGateTests.Validate_MultipleViolations_* (unit) |
✓ |
| AZ-488 AC-8 | Oversized batch (> MaxBatchSize) returns 400 envelope error |
RL-05 (resource-limit); UavUploadTests.OversizedBatch_Returns400 (integration) |
✓ |
| AZ-488 AC-9 | Contract uav-tile-upload.md v1.0.0 frozen and matches implementation |
doc-state AC; verified by Step 13 (Update Docs) review | ✓ |
| AZ-488 AC-10 | All existing tests + new AZ-487/AZ-488 tests pass; no AZ-484 regression | Full scripts/run-tests.sh --full run (cycle 2 Step 11 — passed) |
✓ |
| AZ-494 AC-1 | Wrong iss token returns 401 |
SEC-12 (blackbox); JwtIntegrationTests.WrongIssuer_Returns401 (integration) |
✓ |
| AZ-494 AC-2 | Wrong aud token returns 401 |
SEC-13 (blackbox); JwtIntegrationTests.WrongAudience_Returns401 (integration) |
✓ |
| AZ-494 AC-3 | Matching iss + aud accepted | JwtIntegrationTests.ValidToken_Returns200_OnHealthyEndpoint (integration; updated to mint via env iss/aud) |
✓ |
| AZ-494 AC-4 | Missing config fails fast | AuthenticationServiceCollectionExtensionsTests.AddSatelliteJwt_ThrowsOnMissingIssuer + _ThrowsOnEmptyIssuer + _ThrowsOnMissingAudience + _ThrowsOnEmptyAudience (unit) |
✓ |
| AZ-494 AC-5 | Existing tests pass with matched fixtures | Full integration suite reruns at Step 16 with JwtTestHelpers.MintAuthenticated (auto-fills iss/aud from env) |
✓ (gate verified at Step 16) |
| AZ-494 AC-6 | Security artifacts updated (F-AUTH-2 → Resolved) | _docs/05_security/security_report.md + owasp_review.md updated this batch |
✓ |
| AZ-494 AC-7 | Suite contract reflects reality | suite/_docs/10_auth.md lives outside this workspace; this cycle's deploy report documents that satellite-provider validates iss/aud locally and the prod values are admin-team-confirmed at deploy time |
◐ deferred (cross-repo write) |
| AZ-491 AC-1 | Single source of truth — only one JwtTokenFactory exists in source |
Structural: repo-wide grep returns exactly SatelliteProvider.TestSupport/JwtTokenFactory.cs; the legacy SatelliteProvider.Tests/TestUtilities/JwtTokenFactory.cs was deleted in batch 02 |
✓ |
| AZ-491 AC-2 | Existing integration tests pass unchanged | Full integration suite at Step 11 (./scripts/run-tests.sh --full) — all green |
✓ |
| AZ-491 AC-3 | Existing unit tests pass unchanged | Unit suite at Step 11 (Step 1 of run-tests.sh) — all green |
✓ |
| AZ-491 AC-4 | Runner-side concerns preserved in JwtTestHelpers (env reads, HttpClient mutation stay in IntegrationTests) |
Structural: JwtTokenFactory (pure) in TestSupport; JwtTestHelpers (side-effectful) in IntegrationTests — documented in module-layout.md |
✓ |
| AZ-491 AC-5 | Cycle-2 fixes remain effective (AZ-487/AZ-488 token-validation invariants preserved) | Integration scenarios JwtIntegrationTests.AnonymousRequest_*, _ExpiredToken_Returns401, _InvalidSignature_Returns401, _ValidToken_Returns200_OnHealthyEndpoint, UavUploadTests.* — all migrated to MintAuthenticated and still PASS at Step 11 |
✓ |
| AZ-491 AC-6 | Code-review rule lands to prevent re-duplication | .cursor/skills/code-review/SKILL.md Phase 6 rule added in batch 02 (Cycle-3 review SKILL update) |
✓ |
| AZ-493 AC-1 | Empty-state on startup — no leftover rows from previous run | IntegrationTestDatabaseReset.ResetAsync invoked at runner start; uniqueness assumptions in UavUploadTests (source='uav' rows per coordinate) hold without the wall-clock workaround |
✓ |
| AZ-493 AC-2 | Wallclock workaround no longer needed | Structural: UavUploadTests no longer offsets coordinates by DateTime.UtcNow.Ticks % … to dodge stale rows; coordinates are now deterministic per scenario |
✓ |
| AZ-493 AC-3 | Opt-out preserves state (--keep-state flag skips reset) |
scripts/run-tests.sh parses --keep-state, sets INTEGRATION_TEST_DB_RESET=skip, and Program.cs honours that env var |
✓ |
| AZ-493 AC-4 | Reset only fires in test environment (two-guard model) | Unit: IntegrationTestResetGuardTests (env sentinel + Host allowlist postgres/localhost/127.0.0.1; production-shape hostnames rejected) |
✓ |
| AZ-493 AC-5 | Documentation reflects new convention | doc-state AC — _docs/02_document/module-layout.md + _docs/02_document/modules/tests_integration.md updated in batch 03 |
✓ |
| AZ-493 AC-6 | Existing tests pass unchanged | Full integration suite at Step 11 — all green | ✓ |
| AZ-495 AC-1..AC-N | Doc folder convention formalized | doc-state AC — .cursor/skills/new-task/SKILL.md updated in batch 01; _docs/02_document/module-layout.md carries the convention |
✓ |
| AZ-496 AC-1 | Microsoft.AspNetCore.Authentication.JwtBearer bumped 8.0.21 → 8.0.25 in SatelliteProvider.Api.csproj |
Structural: csproj diff visible in batch 01 commit; transitive update propagates to Tests.csproj via ProjectReference |
✓ |
| AZ-496 AC-2..AC-N | Suite still green at the new version | Full unit + integration suite at Step 11 — all green; SEC-05..SEC-11 + AZ-494 AC-1/AC-2 (which depend on JwtBearer) all PASS |
✓ |
| AZ-500 AC-1 | Every csproj targets net10.0 |
Structural: grep -r "<TargetFramework>" --include="*.csproj" returns 9/9 net10.0, 0 net8.0 (verified at cycle 4 Step 11) |
✓ |
| AZ-500 AC-2 | global.json sdk.version=10.0.0, rollForward=latestMinor |
Structural: file contents asserted; SDK roll-forward exercised by host running .NET 10.0.103 | ✓ |
| AZ-500 AC-3 | All Docker base images + CI images on :10.0 |
Structural: grep -rE "mcr.microsoft.com/dotnet/" --include="*Dockerfile" --include="*.yml" --include="*.sh" → 7/7 on :10.0 |
✓ |
| AZ-500 AC-4 | Microsoft.AspNetCore.* + Microsoft.Extensions.* on 10.0.7; Serilog.AspNetCore documented fallback 8.0.3 |
Structural: csproj diff (19 references on 10.0.7); Serilog.AspNetCore fallback rationale recorded in AGENTS.md:244 per Risk #4 |
✓ |
| AZ-500 AC-5 | Perf-script bootstrap step succeeds (no exit 3) — closes cycle-3 SDK-mismatch leftover | PERF_REPEAT_COUNT=2 PERF_UAV_BATCH_SIZE=2 ./scripts/run-performance-tests.sh exit 1 (NOT 3 — bootstrap clean, build OK, JWT mint OK, PT-01..PT-07 PASS); leftover _docs/_process_leftovers/2026-05-12_perf-cycle3-harness-execution.md updated with new (non-SDK) PT-08 grep-pipefail finding; full perf gate runs at Step 15 of cycle 4 |
✓ |
| AZ-500 AC-6 | All unit + integration tests pass on the migrated build | Full ./scripts/run-tests.sh --full at cycle 4 Step 11 — 271/271 unit + integration suite green |
✓ |
| AZ-500 AC-7 | docker-compose build succeeds with no downgrade / framework / missing-image warnings |
run-tests.sh Step 2 build path + docker compose up -d --build both succeeded; only warnings emitted are CS8604 nullable + ASPDEPR002 deprecation (neither category gated) |
✓ |
| AZ-500 AC-8 | Documentation reflects .NET 10 | _docs/02_document/architecture.md lines 5 + 67 (Tech Stack table) updated; AGENTS.md lines 9 + 240–244 updated incl. Serilog fallback note |
✓ |
| AZ-503 AC-1 | UUIDv5 reference vectors match Python (≥10 cases) | Uuidv5Tests.Create_MatchesPythonReferenceVectors_AC1 (unit) + version/variant bit assertions |
✓ |
| AZ-503 AC-2 | Insert is idempotent on identical inputs (id stable, created_at preserved) | BT-20 (blackbox); UavTileUploadHandlerTests.HandleAsync_IdenticalUpload_ProducesIdenticalIdAndDeterministicContentSha (unit) |
✓ |
| AZ-503 AC-3 | Multi-flight UAV uploads coexist (two ids, shared location_hash) | BT-19 (blackbox); UavTileUploadHandlerTests.HandleAsync_TwoFlightsSameCell_ProduceDistinctIdsAndPathsButSameLocationHash (unit); UavUploadTests.MultiFlightUavRowsCoexist_AZ503_AC3 (integration) |
✓ |
| AZ-503 AC-4 | Float-rounded coordinates collapse to a single row | BT-21 (blackbox); UavUploadTests.FloatRoundingDoesNotBreakIdempotence_AZ503_AC4 (integration) |
✓ |
| AZ-503 AC-7 | content_sha256 is computed and persisted; byte-identical bodies produce identical digest | BT-20 (blackbox); UavTileUploadHandlerTests.HandleAsync_IdenticalUpload_ProducesIdenticalIdAndDeterministicContentSha (unit) |
✓ |
| AZ-503 AC-8 | Migration 014 adds columns + supersedes AZ-484 index + backfills location_hash deterministically | BT-22 (blackbox); MigrationTests.Az503ColumnsExistAndLocationHashIsNotNull, Az503NewUniqueIndexCoversIntegerKeyAndFlightId, Az503LocationHashBackfillIsDeterministic, Az503MigrationSupersedesAz484UniqueIndex (integration) |
✓ |
| AZ-503 AC-11 | Per-flight on-disk separation (./tiles/uav/{flight_id|none}/{z}/{x}/{y}.jpg) |
BT-19 (blackbox); UavTileFilePathTests.BuildUavTileFilePath_AnonymousFlight_UsesNoneSegment, _PerFlight_UsesFlightIdDirectory, _DifferentFlights_ProduceDifferentPaths (unit); UavUploadTests.MultiFlightUavRowsCoexist_AZ503_AC3 (integration; per-flight file_path assertion) |
✓ |
| AZ-503 AC-5 | Inventory endpoint POST /api/satellite/tiles/inventory returns one entry per requested coord |
BT-23 (blackbox); resolved by AZ-505 AC-1 — see row below | ✓ (via AZ-505) |
| AZ-503 AC-6 | Leaflet path returns most-recent variant via location_hash |
BT-24 (blackbox); resolved by AZ-505 AC-2 — see row below | ✓ (via AZ-505) |
| AZ-503 AC-9 | Inventory endpoint p95 ≤ 500 ms for 2500 tiles | PT-09 (performance); resolved by AZ-505 AC-4 — see row below (budget relaxed to 1000 ms under AZ-505 scoping, AZ-505 Risk 1) | ✓ (via AZ-505, relaxed budget) |
| AZ-503 AC-10 | Leaflet hot path is index-only (EXPLAIN: no heap fetch when voting_status='trusted') |
Resolved by AZ-505 AC-3 — see row below (voting layer is deferred to a future task per AZ-505 Non-Goals; AC-3 verifies the index-only access path against tiles_leaflet_path directly via EXPLAIN ANALYZE + Heap Fetches: 0 assertion) |
✓ (via AZ-505, voting-filter deferred) |
| AZ-503 AC-12 | HTTP/2 multiplexed responses for /tiles/{z}/{x}/{y} |
BT-25 (blackbox); resolved by AZ-505 AC-5 — see row below | ✓ (via AZ-505) |
| AZ-505 AC-1 | Inventory endpoint returns one entry per requested coord in input order; present/absent shaping per tile-inventory.md Inv-1..Inv-6 |
BT-23 (blackbox); TileInventoryTests.OrderingAndPresentAbsentShaping_AC1 (integration) |
✓ |
| AZ-505 AC-2 | Leaflet read path returns most-recent variant keyed on location_hash (captured_at DESC, updated_at DESC, id DESC LIMIT 1) |
BT-24 (blackbox); TileInventoryTests.LeafletReadReturnsMostRecentViaLocationHash_AC2 (integration; DB-level verification of the exact SELECT used by TileRepository.GetByTileCoordinatesAsync, which ServeTile wraps unchanged) |
✓ |
| AZ-505 AC-3 | Leaflet hot path uses Index Only Scan using tiles_leaflet_path; Heap Fetches ≤ 1 after VACUUM ANALYZE; query time < 1 ms |
LeafletPathIndexOnlyTests.RunAll (integration; EXPLAIN ANALYZE + regex + Heap Fetches ≤ 1; smoke run falls back to SET enable_seqscan = off if the optimiser hasn't picked the index naturally — measures index capability, not optimiser heuristic) |
✓ |
| AZ-505 AC-4 | Inventory endpoint p95 ≤ 1000 ms for 2500 tiles over 20 calls | PT-09 (performance); TileInventoryTests.PerformanceBudget_AC4 (integration; full-suite only, smoke prints a documented skip). Cycle 6 measured: p95=66ms, max=117ms — well under budget. |
✓ |
| AZ-505 AC-5 | HTTP/2 multiplexed responses for /tiles/{z}/{x}/{y} over a single connection with preserved ETag + Cache-Control headers |
BT-25 (blackbox); Http2MultiplexingTests.RunAll (integration; 20 concurrent GETs over a single TLS connection with SocketsHttpHandler { EnableMultipleHttp2Connections = false } + HttpVersion.Version20 + RequestVersionExact). Implementation uses TLS+ALPN on the dev https://+:8080 listener (cert generated by scripts/run-tests.sh into ./certs/api.pfx, trusted in the integration-tests container via update-ca-certificates) — the original h2c plan was switched mid-cycle because Kestrel silently downgrades Http1AndHttp2 to HTTP/1.1 over plaintext (no ALPN). See _docs/03_implementation/implementation_report_tile_inventory_cycle6.md → "Post-merge correction". |
✓ |
| AZ-505 AC-6 | Request validation — 400 on both populated, 400 on neither, 400 on > 5000 entries, 401 on anonymous | BT-26 (blackbox); TileInventoryTests.ValidationRejectsBothPopulated_AC6, ValidationRejectsNeitherPopulated_AC6, ValidationRejectsOversizedBatch_AC6, UnauthenticatedRequestReturns401_AC6 (integration) |
✓ |
| AZ-505 AC-7 | Contract artifacts produced in the same commit as code (tile-inventory.md v1.0.0, tile-storage.md v2.0.0 Change Log, module-layout.md rows) |
Doc inspection at completeness gate — _docs/03_implementation/implementation_completeness_cycle6_report.md "Files / Symbols Checked" + "Contracts" sections list the v1.0.0 / v2.0.0 artifacts; no runtime test (deliberately doc-only) |
✓ (doc-only) |
| AZ-504 AC-1 | PT-08 completes on zero-rejected response (no script exit under set -e -o pipefail) |
Standalone shell harness (4-case) executed in batch_01_cycle5_report.md — accepted/rejected counters wrapped in { grep -o … || true; } at scripts/run-performance-tests.sh:416-417; structural: rg "grep -o .* \\| wc -l" scripts/run-performance-tests.sh returns 0 unguarded sites |
✓ |
| AZ-504 AC-2 | PT-08 completes on zero-accepted response (defensive) | Same standalone shell harness (case 4) — accepted=0, rejected=N path no longer kills the script |
✓ |
| AZ-504 AC-3 | PT-08 summary line prints in full default-parameter perf run | Verified at autodev Step 15 (Performance Test) by running scripts/run-performance-tests.sh with PERF_REPEAT_COUNT=20 PERF_UAV_BATCH_SIZE=10; pass criterion is the PT-08 UAV batch upload: PASS p95=Xms / 2000ms (...) line in the run output |
◐ gate at Step 15 |
| AZ-504 AC-4 | Leftover _docs/_process_leftovers/2026-05-12_perf-cycle3-harness-execution.md deleted on green full run |
Verified at autodev Step 15 by test -f _docs/_process_leftovers/2026-05-12_perf-cycle3-harness-execution.md returning non-zero after the green run + commit |
◐ gate at Step 15 |
| AZ-794 AC-1 | Inventory request body uses short names {z, x, y} (OSM convention) |
BT-27 sub-case pos (TileInventoryValidationTests.HappyPath_Returns200); existing AZ-505 integration suite (TileInventoryTests.OrderingAndPresentAbsentShaping_AC1 + the AC-6 validation tests already use z/x/y after the rename); deserializer-level proof via BT-27 sub-case 9c (OldV1FieldName_Returns400) — old names now hard-fail |
✓ |
| AZ-794 AC-2 | Inventory response body uses short names {z, x, y}; all other fields (locationHash, present, id, capturedAt, source, flightId, resolutionMPerPx) unchanged byte-for-byte from the pre-rename contract |
TileInventoryTests.OrderingAndPresentAbsentShaping_AC1 (integration; asserts entry.Z, entry.X, entry.Y, entry.LocationHash, entry.Present, entry.Id, entry.CapturedAt, entry.Source for 25 mixed present/absent entries against the v2.0.0 wire shape) |
✓ |
| AZ-794 AC-3 | OpenAPI / Swagger spec declares z, x, y (not the old names) as the required coordinate properties |
Doc-state AC — verified at Step 13 (Update Docs) review against /swagger/v1/swagger.json schema definitions for TileInventoryRequest + TileCoord |
◐ doc-verified at Step 13 |
| AZ-794 AC-4 | _docs/02_document/contracts/api/tile-inventory.md bumped to v2.0.0; Migration / Coexistence section names AZ-794 and the breaking-rename |
Doc-state AC — _docs/02_document/contracts/api/tile-inventory.md v2.0.0 Change Log entry naming AZ-794 (verified at Step 13 Update Docs review) |
✓ |
| AZ-795 (epic) | Shared input-validation infra in place: FluentValidation 12.0.0 + ValidationEndpointFilter<T> + GlobalExceptionHandler + JsonSerializerOptions.UnmappedMemberHandling.Disallow + camelCase naming policy + new error-shape.md v1.0.0 contract |
Structural: SatelliteProvider.Api/Validators/{ValidationEndpointFilter,GlobalValidatorConfig,ValidationEndpointFilterExtensions}.cs + SatelliteProvider.Api/GlobalExceptionHandler.cs exist; error-shape.md v1.0.0 frozen with 8 documented test cases; end-to-end exercise via the AZ-796 test suite (every BT-27 sub-case routes through this infra). Per-endpoint child task tracker (AZ-796 is the first; siblings to follow) is owned by Jira AZ-795. |
✓ (infra + contract; per-endpoint child coverage tracked individually) |
| AZ-796 AC-1 | Each of the 9 validation rules rejects with HTTP 400 + RFC 7807 ProblemDetails; errors[] array has single-rule precision (no unrelated rules) |
BT-27 (blackbox; sub-cases 1, 2a, 2b, 3, 4, 5a, 5b, 6a, 6b, 7, 8a, 8b, 9a, 9b, 9c — one per rule); TileInventoryValidationTests.* (integration: 15 failure tests) + InventoryRequestValidatorTests.* (unit: covers the rules expressible at the validator layer in isolation) |
✓ |
| AZ-796 AC-2 | Happy path unchanged (HTTP 200 with existing result shape; one entry per requested tile, same ordering, fields preserved) | BT-27 sub-case pos (TileInventoryValidationTests.HappyPath_Returns200); no regression in existing TileInventoryTests.OrderingAndPresentAbsentShaping_AC1 (which still passes at cycle 7 Step 11) |
✓ |
| AZ-796 AC-3 | InventoryRequestValidator lives in its own file under SatelliteProvider.Api/Validators/; xUnit class has one test method per RuleFor(...) (≥ 9 unit-test methods) |
Structural: SatelliteProvider.Api/Validators/InventoryRequestValidator.cs exists (74 lines, isolated validator + TileCoordValidator); SatelliteProvider.Tests/Validators/InventoryRequestValidatorTests.cs contains 16 test methods (Validate_TilesPopulated_LocationHashesNull_Passes, Validate_LocationHashesPopulated_TilesNull_Passes, Validate_BothPopulated_FailsXorRule, Validate_NeitherPopulated_FailsXorRule, Validate_BothEmpty_FailsXorRule, Validate_TilesAtCap_Passes, Validate_TilesOverCap_FailsCapRule, Validate_LocationHashesOverCap_FailsCapRule, Validate_TileZoomOutOfRange_FailsRangeRule ×3, Validate_TileZoomInRange_PassesRangeRule ×3, Validate_TileXNegative_FailsRangeRule, Validate_TileXAtUpperBound_FailsRangeRule, Validate_TileYNegative_FailsRangeRule, Validate_TileYAtUpperBound_FailsRangeRule, Validate_AxesAtMaxForZoom_Passes) |
✓ |
| AZ-796 AC-4 | Integration tests cover happy + failure per rule (≥ 10 methods) in SatelliteProvider.IntegrationTests/TileInventoryValidationTests.cs |
TileInventoryValidationTests contains 16 integration test methods (1 happy + 15 failure: HappyPath_Returns200, EmptyBody_Returns400, NeitherPopulated_Returns400, BothPopulated_Returns400, EmptyTilesArray_Returns400, TilesOverCap_Returns400, MissingZ_Returns400WithFieldPath, MissingXAndY_Returns400, ZoomOutOfRange_Returns400WithFieldPath, XBeyondZoomBounds_Returns400, YBeyondZoomBounds_Returns400, NegativeAxis_Returns400, UnknownRootField_Returns400, UnknownNestedField_Returns400, OldV1FieldName_Returns400, TypeMismatch_Returns400) — 16 ≥ 10. Cycle 7 Step 11 full run reports all 16 passing. |
✓ |
| AZ-796 AC-5 | /swagger/v1/swagger.json marks required fields, declares integer ranges per validation rules, declares 400 response with ProblemDetails schema |
Doc-state AC — verified at Step 13 (Update Docs) review against the published OpenAPI document; integration smoke is the existing JwtIntegrationTests.SwaggerDocument_AdvertisesBearerSecurityScheme pattern (a future analogous test against the validation schema is out-of-scope this cycle) |
◐ doc-verified at Step 13 |
| AZ-796 AC-6 | _docs/02_document/contracts/api/tile-inventory.md updated to document the 9 validation rules + error contract reference |
Doc-state AC — _docs/02_document/contracts/api/tile-inventory.md v2.0.0 Change Log entry naming AZ-796 (verified at Step 13 Update Docs review) |
✓ |
| AZ-796 AC-7 | scripts/probe_inventory_validation.sh committed; exercises each failure mode via curl + JWT for documentation / regression |
Structural: scripts/probe_inventory_validation.sh exists in repo and is manually runnable |
✓ |
| AZ-808 AC-1 | Each of the 8 region-request validations rejects with HTTP 400 + ValidationProblemDetails (single-rule precision) | BT-28 sub-cases 1, 2a, 2b, 3, 4, 5, 6, 7, 8, 9, 10 (blackbox); RegionRequestValidationTests (integration, 11+ failure methods) + RegionRequestValidatorTests (unit, ≥ 8 methods covering each RuleFor); shared ProblemDetailsAssertions enforces error-shape v1.0.0 conformance |
✓ |
| AZ-808 AC-2 | Happy path unchanged — valid body returns HTTP 200 + RegionStatusResponse; background processing still runs; probe's 9-tile Derkachi case completes < 10 s |
BT-28 sub-case pos (RegionRequestValidationTests.HappyPath_Returns200); no regression in existing RegionRequestTests.cs (cycle 8 Step 11 — green) |
✓ |
| AZ-808 AC-3 | RegionRequestValidator in its own file under SatelliteProvider.Api/Validators/; unit-tested (≥ 1 per RuleFor) |
Structural: SatelliteProvider.Api/Validators/RegionRequestValidator.cs exists; SatelliteProvider.Tests/Validators/RegionRequestValidatorTests.cs covers each RuleFor chain |
✓ |
| AZ-808 AC-4 | RegionRequestValidationTests.cs covers happy + 8+ failure modes; MUST include Post_WithMissingId_ReturnsBadRequest reproducing 2026-05-22 silent-coercion case |
SatelliteProvider.IntegrationTests/RegionRequestValidationTests.cs includes MissingId_Returns400 (the renamed AZ-777 Phase 2 reproducer) and ≥ 11 total failure methods; uses ProblemDetailsAssertions from AZ-795 |
✓ |
| AZ-808 AC-5 | _docs/02_document/contracts/api/region-request.md v1.0.0 created and published |
Doc-state AC — region-request.md v1.0.0 created in cycle-8 batch (coordinated with AZ-812 — published directly with lat/lon names per AZ-812 AC-6); verified at Step 13 (Update Docs) review |
✓ |
| AZ-808 AC-6 | _docs/02_document/system-flows.md F2 updated to reference the new contract doc + error shape |
Doc-state AC — verified at Step 13 (Update Docs) review | ◐ doc-verified at Step 13 |
| AZ-808 AC-7 | OpenAPI marks RequestRegionRequest fields required, declares ranges, documents 400 response |
Doc-state AC — verified at Step 13 (Update Docs) review against published /swagger/v1/swagger.json; Swashbuckle annotations match AZ-796 pattern |
◐ doc-verified at Step 13 |
| AZ-808 AC-8 | Manual probe script exercises each failure mode via curl + JWT |
Structural: scripts/probe_region_validation.sh exists and is manually runnable |
✓ |
| AZ-809 AC-1 | Each of the 14 route-creation validations rejects with HTTP 400 + ValidationProblemDetails (single-rule precision) | BT-29 sub-cases 1..14 (blackbox); CreateRouteValidationTests (integration, 14+ failure methods covering deserializer + per-DTO + per-element + cross-field layers) + CreateRouteRequestValidatorTests + RoutePointValidatorTests + GeofencePolygonValidatorTests (unit, ≥ 13 methods total) |
✓ |
| AZ-809 AC-2 | Happy path unchanged — valid body returns HTTP 200 + RouteResponse; F5 background still runs when requestMaps=true; probe's 2-point 132 m route completes < 20 s |
BT-29 sub-case pos (CreateRouteValidationTests.HappyPath_Returns200); no regression in existing RouteCreationTests.cs (cycle 8 Step 11 — green) |
✓ |
| AZ-809 AC-3 | CreateRouteRequestValidator, RoutePointValidator, GeofencePolygonValidator each in their own files under SatelliteProvider.Api/Validators/; ≥ 13 unit-test methods total |
Structural: three validator files exist as separate .cs files; unit-test files together contain ≥ 13 methods covering every RuleFor / RuleForEach chain |
✓ |
| AZ-809 AC-4 | CreateRouteValidationTests.cs covers happy + 13+ failure modes; MUST include Post_WithMissingId_ReturnsBadRequest |
SatelliteProvider.IntegrationTests/CreateRouteValidationTests.cs includes MissingId_Returns400 (the AZ-777 Phase 2 reproducer for route variant) and ≥ 14 total failure methods |
✓ |
| AZ-809 AC-5 | _docs/02_document/contracts/api/route-creation.md v1.0.0 created and published |
Doc-state AC — route-creation.md v1.0.0 created in cycle-8 batch; verified at Step 13 review |
✓ |
| AZ-809 AC-6 | _docs/02_document/system-flows.md F4 + F5 updated to reference new contract doc + error shape |
Doc-state AC — verified at Step 13 (Update Docs) review | ◐ doc-verified at Step 13 |
| AZ-809 AC-7 | OpenAPI marks all required fields at every nesting level, declares ranges, documents 400 response | Doc-state AC — verified at Step 13 against published /swagger/v1/swagger.json |
◐ doc-verified at Step 13 |
| AZ-809 AC-8 | Manual probe script exercises each failure mode via curl + JWT |
Structural: scripts/probe_route_validation.sh exists and is manually runnable |
✓ |
| AZ-809 AC-9 | (Advisory) RequestRegionRequest.sizeMeters vs CreateRouteRequest.regionSizeMeters naming inconsistency surfaced for parent-suite decision |
Not tested — surfaced in _docs/03_implementation/batch_*_cycle8_report.md for parent-suite team follow-up |
◐ advisory (not tested) |
| AZ-809 AC-10 | (Advisory) Input points: [{lat, lon}] vs output points: [{latitude, longitude}] round-trip asymmetry surfaced for parent-suite decision |
Not tested — surfaced in _docs/03_implementation/batch_*_cycle8_report.md for parent-suite team follow-up |
◐ advisory (not tested) |
| AZ-810 AC-1 | Each of the 14 upload-metadata validations rejects with HTTP 400 + ValidationProblemDetails (single-rule precision) | BT-30 sub-cases 1..13 (blackbox); UavUploadValidationTests (integration, ≥ 13 failure methods covering deserializer + FluentValidation + envelope cross-field layers) + UavTileMetadataValidatorTests + UavTileBatchMetadataPayloadValidatorTests (unit, ≥ 11 methods total) |
✓ |
| AZ-810 AC-2 | Happy path unchanged — valid envelope returns HTTP 200 + per-item result list; per-item file rejections (IUavTileQualityGate) still return HTTP 200 with per-item status |
BT-30 sub-case pos (UavUploadValidationTests.HappyPath_Returns200); existing AZ-488 BT-13..BT-17 + UavUploadTests continue green (cycle 8 Step 11 after the AZ-810 test-data coord-clamp fix in commit b763da3) |
✓ |
| AZ-810 AC-3 | UavTileMetadataValidator + UavTileBatchMetadataPayloadValidator each in their own files under SatelliteProvider.Api/Validators/; ≥ 11 unit-test methods total |
Structural: two validator files plus UavUploadValidationFilter.cs (the multipart envelope filter) exist under SatelliteProvider.Api/Validators/; unit-test files contain ≥ 11 methods covering each RuleFor |
✓ |
| AZ-810 AC-4 | UavUploadValidationTests.cs covers happy + 12+ failure modes with full ValidationProblemDetails assertion |
SatelliteProvider.IntegrationTests/UavUploadValidationTests.cs contains ≥ 13 integration test methods spanning all three enforcement layers; uses ProblemDetailsAssertions |
✓ |
| AZ-810 AC-5 | _docs/02_document/contracts/api/uav-tile-upload.md bumped to v1.2.0 with the new validation section |
Doc-state AC — uav-tile-upload.md v1.2.0 Change Log entry naming AZ-810; verified at Step 13 review |
✓ |
| AZ-810 AC-6 | _docs/02_document/modules/api_program.md documents the new multipart validation endpoint filter |
Doc-state AC — api_program.md updated in cycle-8 batch; verified at Step 13 review |
✓ |
| AZ-810 AC-7 | OpenAPI marks UavTileBatchMetadataPayload + UavTileMetadata fields required, declares ranges, documents 400 response |
Doc-state AC — verified at Step 13 against published /swagger/v1/swagger.json; [JsonRequired] annotations propagate to Swashbuckle as required: [latitude, longitude, tileZoom, tileSizeMeters, capturedAt] and required: [items] |
◐ doc-verified at Step 13 |
| AZ-810 AC-8 | Manual probe script exercises each failure mode via multipart curl + JWT |
Structural: scripts/probe_upload_validation.sh exists, reuses the AZ-808/AZ-809/AZ-811 probe-script pattern, and is manually runnable |
✓ |
| AZ-810 AC-9 | No regression in existing AZ-488 integration tests (UavTileBatchUploadTests.cs, UavTileQualityGateTests.cs) |
Cycle 8 Step 11 full integration run green AFTER fixing a pre-existing latent bug in UavUploadTests.NextTestCoordinate that AZ-810 exposed (seed (Ticks/TicksPerSecond) % 1_000_000 produced lat > 90°; clamped to lat ∈ [50, 70), lon ∈ [10, 40) in commit b763da3). The original AC-9 verification (cycle 8 batch_04 report — "verified by tracing source") was a false-PASS; the green re-run is the binding evidence. Lesson recorded in _docs/LESSONS.md (2026-05-23) |
✓ (verified by full-suite re-run) |
| AZ-811 AC-1 | Each of the 5 GET-lat/lon validations rejects with HTTP 400 + ValidationProblemDetails | BT-31 sub-cases 1..5 + 4a/4b/4c (blackbox); GetTileByLatLonValidationTests (integration, ≥ 7 failure methods) + GetTileByLatLonQueryValidatorTests (unit, ≥ 3 methods) |
✓ |
| AZ-811 AC-2 | Happy path unchanged — ?lat=&lon=&zoom= returns HTTP 200 + DownloadTileResponse; tile still downloaded/persisted |
BT-31 sub-case pos (GetTileByLatLonValidationTests.HappyPath_Returns200); no regression in existing TileByLatLonTests.cs (cycle 8 Step 11 — green) |
✓ |
| AZ-811 AC-3 | GetTileByLatLonQueryValidator in its own file under SatelliteProvider.Api/Validators/; unit-tested (≥ 3 methods) |
Structural: GetTileByLatLonQueryValidator.cs exists; unit-test file covers the 5 rules in ≥ 3 methods |
✓ |
| AZ-811 AC-4 | GetTileByLatLonValidationTests.cs covers happy + 4+ failure modes |
SatelliteProvider.IntegrationTests/GetTileByLatLonValidationTests.cs contains ≥ 7 failure methods + 1 happy path; uses ProblemDetailsAssertions |
✓ |
| AZ-811 AC-5 | _docs/02_document/contracts/api/tile-latlon.md v1.0.0 created and published |
Doc-state AC — tile-latlon.md v1.0.0 created in cycle-8 batch; verified at Step 13 review |
✓ |
| AZ-811 AC-6 | _docs/02_document/modules/api_program.md::GetTileByLatLon Handler updated to reference the validator + new contract doc |
Doc-state AC — api_program.md updated in cycle-8 batch; verified at Step 13 review |
✓ |
| AZ-811 AC-7 | OpenAPI marks query params required + ranges + 400 response | Doc-state AC — verified at Step 13 against published /swagger/v1/swagger.json |
◐ doc-verified at Step 13 |
| AZ-811 AC-8 | Manual probe script exercises each failure mode via curl + JWT |
Structural: scripts/probe_latlon_validation.sh exists and is manually runnable |
✓ |
| AZ-811 AC-9 | The novel UnknownQueryParameterEndpointFilter (rule 4 — unknown-query-param rejection) is documented in _docs/02_document/modules/api_program.md so the next query-param endpoint can reuse it |
Doc-state AC — the filter's behavior + reuse contract documented in api_program.md; verified at Step 13 review. BT-31 sub-cases 4b (legacy ?Latitude=&Longitude=&ZoomLevel= rejected) and 4c (hostile ?debug=1&admin=true rejected) prove the filter works as documented |
✓ |
| AZ-812 AC-1 | RequestRegionRequest DTO uses Lat / Lon (C#) + [JsonPropertyName("lat")] / [JsonPropertyName("lon")] |
Structural: SatelliteProvider.Common/DTO/RequestRegionRequest.cs diff shows the rename + JsonPropertyName attributes; sibling DTOs RoutePoint and GeoPoint already used lat/lon (no change there) |
✓ |
| AZ-812 AC-2 | Wire format is {"lat":..,"lon":..} end-to-end (request body, OpenAPI schema, docs, all integration tests) |
BT-28 sub-case pos exercises the post-rename wire shape; integration test RegionRequestValidationTests + RegionRequestTests use lat/lon in every body; region-request.md v1.0.0 ships with lat/lon from day one (AZ-812 AC-6 coordination with AZ-808); OpenAPI verified at Step 13 |
✓ |
| AZ-812 AC-3 | RegionTests.cs happy-path tests pass against new wire format |
Cycle 8 Step 11 full run — green; all RegionRequestTests updated to send lat/lon in the same commit as the DTO rename |
✓ |
| AZ-812 AC-4 | curl probe with {"id":"<guid>","lat":49.94,"lon":36.31,"sizeMeters":200,"zoomLevel":18,"stitchTiles":false} returns HTTP 200 + valid regionId; old {"latitude":..,"longitude":..} returns HTTP 400 with UnmappedMemberHandling.Disallow rejecting the unknown fields |
BT-28 sub-case pos (new names accepted) + sub-case 9 (OldLatLongNames_Returns400 — old latitude/longitude rejected as unknown). The strict-deserializer behavior is what AZ-795's UnmappedMemberHandling.Disallow makes possible; pre-cycle-8 the rename would have silently coerced old names to Lat=0, Lon=0 |
✓ |
| AZ-812 AC-5 | Docs updated: common_dtos.md, api_program.md, system-flows.md (F2) |
Doc-state AC — all three files updated in cycle-8 batch; verified at Step 13 review | ◐ doc-verified at Step 13 |
| AZ-812 AC-6 | Contract doc coordination: region-request.md v1.0.0 published directly with lat/lon (because AZ-808 + AZ-812 shipped in same cycle) — no v1.0.0 → v2.0.0 bump needed |
Doc-state AC — region-request.md v1.0.0 Change Log section names both AZ-808 (validation rules) and AZ-812 (lat/lon field names); verified at Step 13 review |
✓ |
Restrictions → Test Mapping
| Restriction | Tests | Coverage |
|---|---|---|
| .NET 10 runtime (cycle 4 — was .NET 8.0 LTS through cycle 3) | All (via Docker image mcr.microsoft.com/dotnet/aspnet:10.0); cycle 4 Step 11 full suite green |
✓ |
| PostgreSQL 16 | All (via docker-compose) | ✓ |
| Single instance | PT-05 (concurrent regions on one instance) | ✓ |
| Max 4 concurrent downloads | RS-05, RL-03 | ✓ |
| Max 20 concurrent regions | RL-04 | ✓ |
| Queue capacity 1000 | RS-04, RL-02 | ✓ |
| Max ZIP 50 MB | RL-01 | ✓ |
GPS permission on UAV upload |
SEC-01..SEC-04 (input handling); SEC-05..SEC-09 (AZ-487 JWT layer); SEC-10..SEC-11 (AZ-488 permission + leak hygiene) | ✓ (superseded by AZ-487 + AZ-488, cycle 2) |
NFRs → Test Mapping
| NFR | Source | Tests | Coverage |
|---|---|---|---|
AZ-484 Perf — GetTilesByRegionAsync p95 ≤ 1.10 × pre-AZ-484 baseline |
AZ-484 task spec § Non-Functional Requirements | PT-07 (Implemented in AZ-492 — cold + warm distribution, p50/p95 reported; cross-commit baseline comparison remains operator-driven at Step 15) | ✓ |
AZ-484 Compatibility — no public HTTP response field added/removed; vestigial maps_version/version columns preserved (nullable) |
AZ-484 task spec § Non-Functional Requirements | Existing integration suite (no API contract change observable); BT-01 / region status responses verify response shape | ✓ |
| AZ-487 Performance — JWT validation < 1 ms overhead per request | AZ-487 task spec § Non-Functional Requirements | Not separately measured (HMAC-SHA256 + claims parse is sub-millisecond on any modern x86; no caching needed). Re-measure if PT-07 / PT-08 (AZ-492 harness) shows aggregate regression. | ◐ recorded |
AZ-487 Security — RequireSignedTokens, RequireExpirationTime, ClockSkew = 30 s, secret ≥ 32 bytes, iss + aud validated (extended by AZ-494) |
AZ-487 + AZ-494 task specs § Non-Functional Requirements + Constraints | AuthenticationServiceCollectionExtensionsTests (unit) + SEC-05..SEC-09 + AZ-494 AC-1/AC-2 wrong-iss/aud (integration) |
✓ |
AZ-487 Reliability — Fail-fast on missing / short JWT_SECRET at startup (extended by AZ-494 to iss + aud) |
AZ-487 + AZ-494 task specs § Non-Functional Requirements | SEC-08 (behavioral) + unit AddSatelliteJwt_ThrowsOnMissingSecret + _ThrowsOnMissingIssuer + _ThrowsOnMissingAudience |
✓ |
| AZ-488 Performance — Per-item gate cost < 50 ms; p95 batch-of-10 < 2 s | AZ-488 task spec § Non-Functional Requirements | PT-08 (Implemented in AZ-492 — 20-batch distribution, batch p95 gated at 2000 ms; per-item gate cost reported as derived proxy batch_p95 / batch_size. True per-call UavTileQualityGate.Validate timing requires server-side instrumentation — follow-up). |
✓ (batch p95) / ◐ (per-item proxy only) |
| AZ-488 Reliability — File-first then DB row; per-item failures never fail the batch envelope (except 400/401/403) | AZ-488 task spec § Non-Functional Requirements | BT-14 (mixed-batch shows per-item isolation); UavTileUploadHandlerTests.*PersistAsync* (unit); reject reason STORAGE_FAILURE defined in contract for the orphan-row recovery path |
✓ |
AZ-488 Compatibility — Replaces 501 stub; coexists with AZ-484 tile-storage v1.0.0 contract on the write side |
AZ-488 task spec § Non-Functional Requirements + Contract | StubAndErrorContractTests updated to drop the stub-501 expectation; BT-15 + BT-16 validate the AZ-484 invariants under live UAV writes |
✓ |
| AZ-488 Security — Reject details never leak server internals; integer-only file-path construction | AZ-488 task spec § Non-Functional Requirements + Risk 2 | SEC-11 (blackbox); UavTileFilePathTests (unit) |
✓ |
AZ-503 Cross-repo contract — UUIDv5 namespace pinned to 5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c; C# and Python (gps-denied-onboard) MUST produce byte-identical output |
AZ-503 task spec § Constraints | Uuidv5Tests.Create_MatchesPythonReferenceVectors_AC1 covers the C# side; cross-repo (Python) side is enforced by gps-denied-onboard AZ-304 and is out of this workspace's automated suite. The constant value is asserted structurally by Uuidv5Tests referencing Uuidv5.TileNamespace. |
✓ (C# side) / ◐ (Python parity verified by reference vectors; sibling workspace owns the runtime check) |
AZ-503 Compatibility — No column renames; tile_zoom/tile_x/tile_y/latitude/longitude preserved; legacy id retained in legacy_id for one cycle |
AZ-503 task spec § Constraints + Risk 1 | MigrationTests.Az503ColumnsExistAndLocationHashIsNotNull (asserts existing column names unchanged); migration 014 SQL inspection (no DROP / RENAME COLUMN on existing columns); structural: legacy_id populated from id for all pre-existing rows |
✓ |
AZ-503 Migration constraint — Additive non-blocking ALTER TABLE ADD COLUMN; backfill in-migration; NOT NULL set only on location_hash (legacy content_sha256 left NULLable — see batch_02_cycle5_report.md "Low" finding) |
AZ-503 task spec § Constraints + Risk 3 | Migration script inspection (014_AddTileIdentityColumns.sql); MigrationTests.Az503ColumnsExistAndLocationHashIsNotNull asserts location_hash NOT NULL; application-layer NOT NULL invariant for new content_sha256 writes enforced in TileService.BuildTileEntity + UavTileUploadHandler.PersistAsync |
✓ |
AZ-503 Selection-rule preservation — AZ-484 read-side tie-break (captured_at DESC, updated_at DESC, id DESC) unchanged |
AZ-503 task spec § Constraints | TileRepository.GetByTileCoordinatesAsync unchanged on the read path; AZ-484 AC-2 row above (MostRecentAcrossSourcesSelection_AZ484_AC2) still passes at Step 11 |
✓ |
AZ-504 Compatibility — Fix preserves set -e -o pipefail globally; only the empty-grep-match case is tolerated locally; no silent error swallowing |
AZ-504 task spec § Non-Functional Requirements + Constraints | Standalone shell harness (batch_01_cycle5_report.md) — case "non-zero exit code other than 1 from grep" still propagates failure; structural: scripts/run-performance-tests.sh:13 retains set -euo pipefail; only the two grep counters at lines 416-417 are wrapped in { grep -o … || true; } (not blanket || true over the whole assignment) |
✓ |
Coverage Summary
| Category | Total Tests | ACs Covered | Restrictions Covered |
|---|---|---|---|
| Blackbox (positive) | 12 | 19/22 | — |
| Blackbox (negative) | 5 | — | — |
| Performance | 8 | 4 | 1 |
| Resilience | 6 | 4 | 3 |
| Security | 11 | 9 (AZ-487 AC-1..AC-7, AZ-488 AC-6, leak-hygiene NFR) | 1 (AZ-487 supersedes "No authentication") |
| Resource Limits | 7 | 5 | 4 |
| Cycle 1 — AZ-484 (integration + unit) | 6 | 7/7 | — |
| Cycle 2 — AZ-487 (integration + unit + behavioral) | 4 integration + 3 unit + 1 behavioral | 8/8 | — |
| Cycle 2 — AZ-488 (integration + unit + blackbox) | 7 integration + 14 unit + 6 blackbox | 10/10 | — |
| Cycle 5 — AZ-503 foundation (integration + unit + blackbox) | 2 integration + 6 unit + 4 blackbox | 7/12 in-scope (AC-1, 2, 3, 4, 7, 8, 11); 5 ACs deferred → AZ-505 (now resolved in cycle 6) | — |
| Cycle 5 — AZ-504 perf-script fix (shell harness + Step-15 gate) | 1 standalone shell harness (4 cases) | 2/4 verified now (AC-1, AC-2); 2/4 gated at Step 15 (AC-3, AC-4) | — |
| Cycle 6 — AZ-505 inventory + HTTP/2 + leaflet covering index (integration + blackbox + perf) | 3 integration files + 4 blackbox (BT-23..BT-26) + 1 perf (PT-09) | 7/7 (AC-1..AC-7; AC-7 is doc-only). Also resolves the 5 AZ-503 deferrals (AC-5, 6, 9, 10, 12). | — |
| Cycle 7 — AZ-794 + AZ-795 + AZ-796 strict inventory validation + z/x/y rename (integration + unit + blackbox + contract) | 1 integration file (TileInventoryValidationTests, 16 tests) + 1 unit file (InventoryRequestValidatorTests, 16 tests) + 1 blackbox (BT-27 with 16 sub-cases) + 1 new contract (error-shape.md v1.0.0) + 1 bumped contract (tile-inventory.md v2.0.0) |
12/12 in-scope (AZ-794 AC-1..AC-4, AZ-795 epic-level, AZ-796 AC-1..AC-7); 2 ACs (AZ-794 AC-3 + AZ-796 AC-5) are ◐ doc-verified at Step 13. |
— |
| Cycle 8 — AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812 strict validation sweep + region OSM rename (integration + unit + blackbox + contracts) | 4 integration files (RegionRequestValidationTests, CreateRouteValidationTests, UavUploadValidationTests, GetTileByLatLonValidationTests — ≥ 45 failure methods + 4 happy paths) + 5 unit files (RegionRequestValidatorTests, CreateRouteRequestValidatorTests, RoutePointValidatorTests, GeofencePolygonValidatorTests, UavTileMetadataValidatorTests, UavTileBatchMetadataPayloadValidatorTests, GetTileByLatLonQueryValidatorTests — ≥ 35 methods across the 4 endpoints) + 4 blackbox (BT-28..BT-31 with ≥ 41 sub-cases) + 4 new contracts (region-request.md v1.0.0, route-creation.md v1.0.0, tile-latlon.md v1.0.0, uav-tile-upload.md v1.2.0 bump) + 4 probe scripts |
41/41 in-scope (AZ-808 AC-1..AC-8, AZ-809 AC-1..AC-8, AZ-810 AC-1..AC-9, AZ-811 AC-1..AC-9, AZ-812 AC-1..AC-6); 8 ACs are ◐ doc-verified at Step 13 (per-endpoint OpenAPI / system-flows updates) + 2 advisory non-tested (AZ-809 AC-9, AC-10 — naming consistency surfaced for parent-suite). AZ-810 AC-9 (no AZ-488 regression) verified after the AZ-810 test-data coord-clamp fix (commit b763da3) — the original "traced by source" verification was a false-PASS; the green full-suite re-run is the binding evidence. |
— |
| Total | 167 | 116/116 in-scope (100%); 2 AZ-504 ACs gated at Step 15; 10 ACs doc-verified at Step 13 (2 cycle-7 + 8 cycle-8); 2 advisory non-tested (cycle-8 AZ-809 AC-9/AC-10) | 8/8 (100%) |
Coverage shape notes (Cycle 5 — AZ-503 foundation):
- AZ-503 was split mid-cycle (Option C, autodev Step 10 batch 2): 7 of 12 original ACs land here; 5 (AC-5, AC-6, AC-9, AC-10, AC-12) are deferred to AZ-505 with a
Blockslink in Jira and an entry in_docs/02_tasks/_dependencies_table.md. The deferred rows above are marked◐ deferred → AZ-505so the matrix surfaces the scope boundary explicitly. - AZ-503 introduces no new HTTP route or wire-protocol change beyond the optional
metadata.flightIdfield on the existingPOST /api/satellite/upload. The 4 new BT scenarios (BT-19..BT-22) describe the new observable behaviors (multi-flight coexistence, deterministic id+content_sha256 on re-upload, float-rounded collapse, migration shape) without inventing a new endpoint surface. - The cross-repo UUIDv5 namespace constant (
5b8d0c2e-7f1a-4d3b-9c5e-1f3a8e7d2b6c) is pinned inSatelliteProvider.Common/Utils/Uuidv5.csand verified C#-side by 10 Python-generated reference vectors inUuidv5Tests. The Python side runtime check is owned by sibling workspacegps-denied-onboard(AZ-304) — this matrix records the C# side as✓and the Python side as◐(out-of-workspace).
Coverage shape notes (Cycle 5 — AZ-504 perf-script fix):
- AZ-504 is a one-line harness bug fix; ACs are verified by a standalone shell harness (4 cases under
set -e -o pipefail) embedded inbatch_01_cycle5_report.md, not by the normal unit / integration suite. There is no production code path to add a test against — the bug is entirely inscripts/run-performance-tests.sh. - AC-3 (PT-08 prints summary in full default-parameter run) and AC-4 (cycle-3 perf-harness leftover deleted on green full run) are gated at autodev Step 15 (Performance Test). The matrix marks them
◐ gate at Step 15rather than✓until that step runs.
Coverage shape notes (Cycle 2):
- AZ-487 AC-7 (Swagger UI Authorize) is verified programmatically (
SwaggerDocument_AdvertisesBearerSecurityScheme) rather than via a real UI flow; marked◐ doc-verified. The end-to-end browser-UI Authorize-button check remains a manual smoke before deploy. - AZ-487 perf NFR (< 1 ms JWT overhead) remains
◐ recorded; not separately gated. AZ-488 perf NFR (PT-08) moved from◐ recorded (Deferred)to✓for batch p95 — see PT-08 row above. AZ-484 perf NFR (PT-07) moved from◐ recordedto✓— see PT-07 row above. The harness work landed in AZ-492 (cycle 3) along with theAuthorization: Bearer …attach that AZ-487 silently broke for the perf script.
Coverage shape notes (Cycle 4 — AZ-500 .NET 8 → .NET 10 migration):
- All 8 AZ-500 ACs are infrastructure-level (TFM/SDK pin/Docker base/package version/build/test-suite/doc) and are verified by re-running the existing test suite, the build pipeline, and
grepover manifests. No new test cases were added — the contract being tested is "the previous 78 tests still pass on the new toolchain", which Step 11 confirmed (271 unit + integration green). Total counts above are unchanged. - AZ-500 AC-5 (perf-script bootstrap) demoted the cycle-3 SDK-mismatch leftover to a script-bug leftover (PT-08 grep-pipefail at
scripts/run-performance-tests.sh:417). The full PT-01..PT-08 perf gate moves to cycle 4 Step 15 (Performance Test). The PT-07 / PT-08 coverage rows above remain✓because they reflect the harness's measurement capability, not the per-cycle measurement run. - AZ-500 NFRs (Compatibility / Performance / Reliability / Security) propagate to existing rows rather than introducing new gates: Compatibility ⇒ cycle-3 architecture-compliance baseline (verified by Step 11 suite); Performance ⇒ Step 15 perf gate (PT-07/PT-08); Reliability ⇒ no
dotnet restorefailures in the migrated state (Step 11 build path); Security ⇒ Step 14 dependency-scan re-run. - Restriction ".NET 8.0 runtime" was rewritten to ".NET 10 runtime" — this is a supersession (toolchain bump) not a new gate, so no Choose was needed per cycle-update rule 3.
Coverage shape notes (Cycle 6 — AZ-505 inventory + HTTP/2 + leaflet covering index):
- AZ-505 resolves the five AZ-503 deferrals (AC-5, AC-6, AC-9, AC-10, AC-12) and adds two strictly new ACs (AC-6 request validation, AC-7 contract-artifacts-in-same-commit). The deferred AZ-503 rows above are rewritten from
◐ deferred → AZ-505to✓ (via AZ-505)and now point at the cycle-6 test entries — the AZ-503 contract is preserved, the implementation just landed one cycle later. - AZ-503 AC-9's original 500 ms p95 budget for 2500 tiles was relaxed to 1000 ms during AZ-505 scoping (AZ-505 Risk 1 documents the trade-off: the inventory result set projects columns beyond
tiles_leaflet_path's INCLUDE list, so a bounded heap fetch is unavoidable). The cycle-6 measured p95 is66 ms— 15× under the relaxed budget — so the relaxation is conservative, not load-bearing. - AZ-503 AC-10 originally specified
Heap Fetches: 0 when voting_status='trusted'. The voting layer is deferred to a future task pertile-inventory.mdv1.0.0 Non-Goals (voting / trust-promotion filtering). AZ-505 AC-3 verifies the index-only access path againsttiles_leaflet_pathdirectly viaEXPLAIN ANALYZE+Heap Fetches ≤ 1assertion, which is the AC-10 intent minus the voting filter. When voting lands, AC-10'svoting_status='trusted'predicate will be re-verified by the voting task. - AZ-505 AC-5 originally specified h2c (HTTP/2 over plaintext). Kestrel was switched to TLS+ALPN on
https://+:8080during the cycle-6 Run Tests step becauseHttpProtocols.Http1AndHttp2silently downgrades to HTTP/1.1 over plaintext (no ALPN). The functional gate (multiplexing semantics) is unchanged — the test still assertsHttpResponseMessage.Version == 2.0over 20 concurrent GETs on a single connection. The deployment caveat (dev cert vs. production TLS termination at the ingress) is documented intile-inventory.mdNon-Goals. - AZ-505 NFRs propagate as follows: Performance (AC-3, AC-4) ⇒ PT-09 entry (full PT-09 row in
performance-tests.md); Compatibility (existingGET /tiles/{z}/{x}/{y}byte-identical) ⇒ no new test — the AZ-484 / AZ-503-foundation selection rule is unchanged, and the test that exercised it under the old(z, x, y)-keyed SELECT now exercises it under thelocation_hash-keyed SELECT via AC-2; Security (JWT +RequireAuthorization()) ⇒ AC-6 anonymous-401 case, BT-26. - Cycle-update rule check: no NFR conflicts surfaced. The 500 ms → 1000 ms perf budget relaxation between AZ-503 AC-9 and AZ-505 AC-4 is not a conflict in the cycle-update sense — AZ-503 AC-9 was explicitly deferred (
◐ deferred → AZ-505) so AZ-505 owns the binding budget; AZ-503's number was a pre-implementation estimate. The matrix records both numbers and the rationale so the budget history stays auditable.
Coverage shape notes (Cycle 7 — AZ-794 + AZ-795 + AZ-796 strict inventory validation + z/x/y rename):
- Cycle 7 is the first application of the AZ-795 shared validation infrastructure (FluentValidation 12.0.0 +
ValidationEndpointFilter<T>+GlobalExceptionHandler+JsonSerializerOptions.UnmappedMemberHandling.Disallow+ camelCase naming policy). The infra is exercised end-to-end by every AZ-796 sub-case — the matrix recordsAZ-795 (epic)as✓because the infra is in place and the first concrete child (AZ-796) demonstrates it functions correctly. Sibling per-endpoint child tasks (other public-facing JSON endpoints) will land under AZ-795 in future cycles and each will get its own AC row at that time. - AZ-794 (z/x/y rename) and AZ-796 (strict validation) shipped in the same commit (
865dfdb). The matrix surfaces this coupling at AZ-794 AC-2 / AZ-796 AC-1 sub-case9c— the BT-27 sub-case that POSTs the legacy{"tileZoom","tileX","tileY"}payload now returns HTTP 400 witherrors[…]namingtileZoom, proving (a) AZ-794's rename is observable on the wire and (b) AZ-795'sUnmappedMemberHandling.Disallowcatches the old names instead of silently coercing to(0,0,0). The same sub-case carries the AZ-777 Phase 1 reproducer body verbatim — that exact request now fails-fast, closing the original discovery loop. - AZ-794 has no perf, security, or resilience NFRs distinct from AZ-505's. Wire size is reduced ~3× on field names (per AZ-794 spec); not separately measured because the AZ-505 AC-4 p95 budget (1000 ms / 2500 tiles, measured 66 ms in cycle 6) already absorbs the rename with margin. Cycle 7 Step 11 reran the full integration suite (311 unit + integration) green; the AZ-505 perf budget is re-measured at Step 15 of cycle 7 per the existing
◐ gate at Step 15rows. - AZ-796 AC-3 (validator unit-tested) and AC-4 (integration-tested) each specify a minimum count (≥ 9 unit, ≥ 10 integration). Cycle 7 delivered 16 + 16 = 32 — comfortably over the floor — covering every
RuleFor(…)inInventoryRequestValidatorandTileCoordValidatorplus the JSON-deserializer-level rules (JsonRequired,UnmappedMemberHandling.Disallow, type mismatch) that don't reach the validator. The split is documented in BT-27 §Notes. - Doc-only ACs (AZ-794 AC-3, AZ-796 AC-5 — OpenAPI / Swagger spec accuracy) are marked
◐ doc-verified at Step 13because they require inspection of the generated/swagger/v1/swagger.jsonduring the Update Docs step. Cycle 7's Swashbuckle output reflects the rename + ranges automatically via the DTOs'[JsonRequired]and the validator'sRuleForconstraints — no manual OpenAPI XML doc edits were needed. Step 13 will verify against the running container's swagger document. - AZ-505 AC-6's existing row (cycle 6 — "Request validation — 400 on both populated, 400 on neither, 400 on > 5000 entries, 401 on anonymous") remains accurate. Its 4 cases overlap with AZ-796 AC-1 sub-cases 2a, 2b, 4, and the anonymous case (also SEC-05). Both rows are kept per cycle-update rule 4 ("Preserve existing traceability IDs"); the duplication is by design — AZ-505 AC-6 was the cycle-6 contract (status-code-only), AZ-796 AC-1 is the cycle-7 contract (status code + ProblemDetails shape + field-path errors). The cycle-7 row is the binding one going forward; the cycle-6 row stays as historical record.
- Cycle-update rule check: no NFR conflicts. The 5000-entry cap is reaffirmed (matches AZ-505); the supported zoom range 0..22 is reaffirmed (matches
tile-inventory.mdInv-7); the error shape contract is new (error-shape.mdv1.0.0) — but no prior cycle declared a different error shape, so this is greenfield content, not a conflict. - Step 10 artifact gap (cycle 7): no
implementation_report_*_cycle7.mdwas produced in_docs/03_implementation/. The actual implementation evidence lives in commitsdceaddc(cycle 7 task adoption) +865dfdb(cycle 7 Step 10 implementation), in the state file'sdetailfield (which recorded the test-run outcome), and in the new test artifacts themselves (InventoryRequestValidator.cs,InventoryRequestValidatorTests.cs,TileInventoryValidationTests.cs,ProblemDetailsAssertions.cs,error-shape.mdv1.0.0). This artifact gap is recorded here for cycle 7 retrospective follow-up — the matrix itself is unaffected because cycle-update mode's source-of-truth is the task specs in_docs/02_tasks/done/, not the implementation report.
Coverage shape notes (Cycle 8 — AZ-808 + AZ-809 + AZ-810 + AZ-811 + AZ-812 strict validation sweep + region OSM rename):
- Cycle 8 completes the AZ-795 epic's per-endpoint rollout — every public-facing endpoint now goes through the shared validation infra. AZ-795's
AZ-795 (epic)row from cycle 7 remains✓; cycle 8 adds 4 endpoint-scoped per-AC rows (AZ-808, AZ-809, AZ-810, AZ-811) plus the AZ-812 region-rename rows that ride the AZ-795UnmappedMemberHandling.Disallowinfra to make the old field names fail-fast (mirroring cycle 7's AZ-794 / AZ-796 coupling). - AZ-810 introduced a new validation enforcement shape — the
multipart/form-dataenvelope — becausePOST /api/satellite/uploadis the only endpoint that can't use the genericValidationEndpointFilter<T>. The bespokeUavUploadValidationFiltercomposes three layers (deserializer, FluentValidation, envelope cross-field) with a different error-key convention (errors["metadata"]for deserializer-level failures vserrors["metadata.items[i].field"]for FluentValidation-layer failures). This is documented in BT-30 §Notes and_docs/02_document/contracts/api/uav-tile-upload.mdv1.2.0 §Validation Rules so future multipart endpoints can reuse the pattern. - AZ-811 introduced a new generic infra piece —
UnknownQueryParameterEndpointFilter(rule 4 — the parallel ofUnmappedMemberHandling.Disallowfor query strings). Documented in_docs/02_document/modules/api_program.mdper AZ-811 AC-9. The next query-param endpoint can reuse it without reinventing the unknown-key rejection logic. - AZ-808 + AZ-812 shipped in the same cycle. The AZ-812 OSM rename (
Latitude/Longitude→Lat/Lon) was coordinated with AZ-808's validator authoring so the validator was never written against the old names (per AZ-812 AC-6 coordination).region-request.mdis published as v1.0.0 (not v1.0.0→v2.0.0 bump) with both AZ-808 (validation rules) and AZ-812 (lat/lonfield names) in the Change Log. - BT-N01 and BT-N02 (legacy negative scenarios for
GET /api/satellite/tiles/latlonthat loosely asserted "HTTP 4xx") are NOT rewritten — they remain as historical record. BT-31 sub-cases 1, 2, 3 supersede them with strict assertions (HTTP 400 + namederrorskey). Both rows are kept per cycle-update rule 4 ("Preserve existing traceability IDs"); the cycle-8 row is the binding one going forward. - AZ-809 ACs 9 + 10 are advisory (surfaced for parent-suite team decision, not implemented or tested this cycle). Matrix marks them
◐ advisory (not tested). They're recorded so the next cycle / parent-suite review sees them without having to re-discover them from the task spec. AC-9:RequestRegionRequest.sizeMetersvsCreateRouteRequest.regionSizeMetersnaming inconsistency. AC-10: inputpoints: [{lat, lon}]vs outputpoints: [{latitude, longitude}]round-trip asymmetry on the route endpoint. Either keep + document, or harmonize in a follow-up MAJOR contract bump for both — parent-suite team's call. - AZ-810 AC-9 (no AZ-488 regression) has a process annotation: cycle 8's batch_04 report originally claimed AC-9 "verified by tracing each AZ-488 test payload's metadata shape against the new rules" without running the integration suite. That verification was a false-PASS — the suite was actually red on the AZ-488 happy path because
UavUploadTests.NextTestCoordinate()produced lat > 90° (a pre-existing latent bug masked by the absence of any validator before AZ-810). The bug was fixed by clamping the test-data generator to OSM-valid ranges in commitb763da3and AC-9 is now bound to the green full-suite re-run, not to source tracing. Process lesson recorded in_docs/LESSONS.md(2026-05-23). - Cycle-update rule check: no NFR conflicts. Range bounds (
lat ∈ [-90, 90],lon ∈ [-180, 180],zoom ∈ [0, 22],tileSizeMeters > 0) are reaffirmed across all 4 endpoints — they were never previously contested. The error-shape contract (error-shape.mdv1.0.0 from cycle 7) is reused unchanged.