[AZ-488] UAV tile batch upload + 5-rule quality gate

Replaces the 501 stub at POST /api/satellite/upload with a multipart
batch endpoint that ingests UAV-captured tiles, runs each item through
a 5-rule quality gate, and persists accepted tiles via the AZ-484
multi-source storage path with source='uav'.

Quality gate (in fixed order, first failure wins): JPEG format
(content-type + magic), size band 5 KiB-5 MiB, exact 256x256
dimensions, captured-at age (no future >30 s skew, no older than
7 days), luminance variance on 32x32 downsample. Closed reject-reason
enumeration in v1.0.0 contract.

Authorization: custom PermissionsRequirement / PermissionsAuthorization
Handler that reads the JWT `permissions` claim (tolerates both
repeated-string and JSON-array shapes). Endpoint protected by
RequiresGpsPermission policy; 401 without token, 403 without GPS perm.

Persistence: file-first to ./tiles/uav/{z}/{x}/{y}.jpg, then
ITileRepository.InsertAsync UPSERT (per-source UPSERT contract from
AZ-484). Per-item failures reported in response without aborting the
batch. Kestrel MaxRequestBodySize and FormOptions limits set to
MaxBatchSize x MaxBytes (default 100 x 5 MiB = 500 MiB).

New frozen contract: _docs/02_document/contracts/api/uav-tile-upload.md
v1.0.0. PT-08 NFR added to performance-tests.md as Deferred (harness
work tracked in PT-07 leftover, per AZ-488 § Risk 4).

Tests: 11 quality-gate unit tests, 5 handler unit tests, 3 file-path
unit tests, 12 permission-handler unit tests, 7 integration tests
(AC-1..AC-6, AC-8). All 253 unit tests + smoke integration suite
green.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 23:50:49 +03:00
parent 11b7074485
commit 1802d32107
35 changed files with 2280 additions and 107 deletions
@@ -0,0 +1,63 @@
# Batch Report — Batch 02 cycle 2
**Batch**: 02 (cycle 2)
**Tasks**: AZ-488 (UAV tile upload endpoint + 5-rule quality gate)
**Date**: 2026-05-11
## Task Results
| Task | Status | Files Modified | Tests | AC Coverage | Issues |
|------|--------|---------------|-------|-------------|--------|
| AZ-488_uav_tile_upload | Done | 9 modified + 13 added (`UavTileBatchUploadRequest.cs`, `UavQualityConfig.cs`, `UavTileMetadata.cs`, `UavTileBatchUploadResponse.cs`, `PermissionsRequirement.cs`, `UavTileQualityGate.cs`, `UavTileUploadHandler.cs`, `UavTileImageFactory.cs`, `UavTileQualityGateTests.cs`, `UavTileUploadHandlerTests.cs`, `UavTileFilePathTests.cs`, `PermissionsRequirementTests.cs`, `UavUploadTests.cs`, contract doc `uav-tile-upload.md`); `SatelliteProvider.Api/DTOs/UploadImageRequest.cs` deleted | All green (unit 253/253 + smoke integration including `UavUploadTests`) | 10/10 ACs covered | 0 blockers; 4 Low findings (see review) |
## AC Test Coverage: All covered (10 of 10)
## Code Review Verdict: PASS_WITH_WARNINGS
## Auto-Fix Attempts: 1 (in-flight build fix: removed unused `using Microsoft.AspNetCore.Http;` in `UavTileUploadHandler.cs` after first `--unit-only` revealed it broke Service-layer build)
## Stuck Agents: None
## What was implemented
- New batch DTOs replacing the old stub: `UavTileBatchUploadRequest` (multipart envelope with JSON `metadata` + `IFormFileCollection`) in `Api/DTOs`; `UavTileMetadata`, `UavTileBatchMetadataPayload`, `UavTileBatchUploadResponse`, `UavTileUploadResultItem`, `UavTileUploadStatus`, and the closed `UavTileRejectReasons` enumeration in `Common/DTO` (placed in Common so Layer 3 services can reference them without a Service → API back-edge). Legacy `UploadImageRequest` deleted.
- New config: `Common/Configs/UavQualityConfig.cs` (MinBytes/MaxBytes/MaxAgeDays/CapturedAtFutureSkewSeconds/MinLuminanceVariance/MaxBatchSize/LuminanceSampleSize). `appsettings.json` ships defaults under `UavQuality`.
- New service `Services.TileDownloader.UavTileQualityGate` (impls `IUavTileQualityGate`) running the 5 rules in fixed order (Format → Size → Dimensions → Captured-at → Uniformity). Welford's online variance on a 32×32 ImageSharp downsample keeps the heuristic ~< 50 ms / item. `TimeProvider` injected for deterministic age tests.
- New service `Services.TileDownloader.UavTileUploadHandler` (impls `IUavTileUploadHandler`) orchestrating envelope validation (batch size / mismatch / malformed JSON), per-item gate run, file-first-then-row persistence (`./tiles/uav/{z}/{x}/{y}.jpg`), and per-item result construction. Uses `TileSourceConverter.ToWireValue(TileSource.Uav)` per L-001.
- New authorization: `Api/Authentication/PermissionsRequirement.cs` + `PermissionsAuthorizationHandler` reading the `permissions` claim — tolerates both repeated-string and JSON-array shapes. `SatellitePermissions.UavUploadPolicy` ("RequiresGpsPermission") wires the `GPS` permission requirement.
- `Program.cs` wires: `UavQualityConfig` binding, Kestrel `MaxRequestBodySize = MaxBatchSize × MaxBytes = 500 MiB`, `FormOptions.MultipartBodyLengthLimit` + `ValueLengthLimit`, `IUavTileQualityGate` + `IUavTileUploadHandler` + `PermissionsAuthorizationHandler` DI registrations, `AddAuthorization(RequiresGpsPermission policy)`, Swagger `MapType<UavTileBatchUploadRequest>` so the multipart shape renders correctly, and the new `UploadUavTileBatch` endpoint replacing the 501 stub.
- Tests:
- Unit: `UavTileQualityGateTests` (11 — every rule happy + reject + ordering), `UavTileUploadHandlerTests` (5 — happy/mixed/oversize/mismatch/invalid JSON), `UavTileFilePathTests` (3 — path shape + invariants), `PermissionsRequirementTests` (12 — claim shape coverage), `UavTileImageFactory` test utility.
- Integration: `UavUploadTests.RunAll` (AC-1 happy, AC-2 mixed-batch, AC-3 multi-source coexistence with pre-seeded `google_maps` row, AC-4 same-source UPSERT with file overwrite + db refresh, AC-5 401 no-token, AC-6 403 wrong perm, AC-8 oversized 400). `StubAndErrorContractTests` updated to drop the old 501-stub assertion.
- Docs:
- **New frozen contract** `_docs/02_document/contracts/api/uav-tile-upload.md` v1.0.0 — endpoint shape, request/response, 5-rule quality gate, closed reject-reason enum, file-path layout, concurrency model, versioning rules, test cases.
- `architecture.md`: UAV ingestion is live; permission-handler description; ADR-004 updated for the per-source file-path split (UAV under `./tiles/uav/`, google_maps grandfathered at bare `./tiles/`).
- `glossary.md`: `UAV Tile Upload`, `Quality Gate`, and all 7 reject-reason constants.
- `modules/api_program.md`: new endpoint row, new local DTOs section, DI registration steps including the body-size cap math, security policy description, configuration section adds `UavQuality`.
- `components/03_tile_downloader/description.md`: documents the two new public types, their dependencies, and the file-path divergence vs. legacy Google Maps tiles.
- `data_model.md`: `file_path` semantics now per-source (UAV vs google_maps).
- `tests/performance-tests.md`: PT-08 (UAV upload latency NFR) added with Status `Deferred — harness work tracked in PT-07 leftover`. `_docs/_process_leftovers/2026-05-11_perf-pt07-harness.md` updated with the PT-08 follow-on instruction so PT-08 lands when PT-07 lands.
## Test results (Step 10 verification)
- **Unit**: 253/253 passed (single docker container, `dotnet/sdk:8.0`, ~3.2 s test time after restore).
- **Integration (smoke)**: all green including the new `UavUploadTests` suite (which runs before the smoke/full branching).
- **Pre-existing AZ-487 test bugs surfaced and fixed in separate `fix:` commits** (see below) — were masked by a CS0104 build error.
## Pre-existing fixes shipped alongside this batch
Three small `fix:` commits were made on `dev` BEFORE the AZ-488 batch commit because they were blocking the test gate for AZ-488:
1. `753be43 [AZ-487] fix: resolve CS0104 ambiguity in AuthN tests``Microsoft.Extensions.DependencyInjection.AuthenticationServiceCollectionExtensions` collided with our same-named class in `SatelliteProvider.Api.Authentication`. Resolved via `using` alias.
2. `f64d0d7 [AZ-487] fix: JWT factory + tests now pass on net8.0``JwtTokenFactory.Create` with a negative lifetime produced `Expires < NotBefore`, which `JwtSecurityToken` rejects at construction. Shifted `notBefore` behind `expires` for non-positive lifetimes. Also disabled `MapInboundClaims` in `JwtTokenFactoryTests` so assertions read the factory's actual claim names ("sub", "email", "permissions") rather than `.NET`-default `ClaimTypes.*` aliases.
3. `11b7074 [AZ-487] fix: integration-test JWT factory handles negative lifetime` — same `Expires < NotBefore` issue in the integration-test side's own copy at `SatelliteProvider.IntegrationTests/JwtTestHelpers.cs`.
All three are AZ-487 test-side hygiene that became observable only after the CS0104 build error was lifted. They are independent of the AZ-488 feature commit; user implicitly approved option B during the autodev pause.
## Open follow-ups (non-blocking)
- **Doc-folder choice (F1, carried over from batch 01)**: `_docs/02_document/components/01_web_api/description.md` referenced by the spec doesn't exist; updates went into `modules/api_program.md` instead. Needs an operator decision on whether to add a stub `01_web_api` folder or formalize the convention.
- **`File.WriteAllBytesAsync(byte[])` allocation** (F4 in review): up to 5 MiB array copy per accepted tile. Replace with `FileStream.WriteAsync(ReadOnlyMemory<byte>, ct)` when PT-08 measurement begins. Not blocking — Rule 5 decode + downsample dominates the gate cost target.
- **PT-08 runner-script scenario**: deferred to land with the PT-07 harness expansion (per cycle 1 retro Action 2 / AZ-488 § Risk 4). Tracked in `_docs/_process_leftovers/2026-05-11_perf-pt07-harness.md`.
- **Coordinate external consumers** for AZ-488: `gps-denied-onboard` and any mission-planner client that posts to `/api/satellite/upload` must attach a Bearer token with `permissions: ["GPS"]` (or the JSON-array shape `"[\"GPS\"]"` — handler accepts both). Coordination is the operator's at Step 16 (Deploy).
## Next: Step 11 (Run Functional Tests) — autodev auto-chain
Cycle 2 batches all closed. Next autodev step is `test-run``deploy` (per `flows/existing-code.md` auto-chain rules).