chore: WIP pre-implement cycle 14 baseline

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-06-26 16:13:37 +03:00
parent 50d4a76be3
commit 80ef5608f1
33 changed files with 619 additions and 47 deletions
+4 -3
View File
@@ -203,9 +203,10 @@ No dependency cycles detected. The dependency graph is a clean DAG.
## CI/CD
- **Woodpecker CI** pipelines in `.woodpecker/`:
- `01-test.yml`: runs `dotnet restore` + `dotnet test` on push/PR to dev/stage/main (ARM64)
- `02-build-push.yml`: builds Docker image and pushes to private registry (depends on 01-test, ARM64 matrix with AMD64 slot commented out)
- **Woodpecker CI** pipelines in `.woodpecker/` (suite contract — see `suite/_infra/ci/README.md`):
- `01-test.yml`: `dotnet restore` + `dotnet test` on push/PR to dev/stage/main (`platform: arm64` only — unit tests are arch-neutral)
- `02-build-push.yml`: matrix fans out to `arm64` (`{branch}-arm`) and `amd64` (`{branch}-amd64`) agents; pushes to Gitea registry. Production deploy (`suite/_infra/deploy/satellite-provider/`) pulls `*-amd64`.
- **Local dev** (Apple Silicon Mac): `scripts/run-tests.sh` runs unit + integration tests as native `linux/arm64` (no Rosetta).
## Updates Since Baseline
@@ -34,7 +34,7 @@
|--------|-------|--------|-------|-------------|
| `Validate` | imageBytes, contentType, `UavTileMetadata` | `UavTileQualityResult` (accept + reason code) | No | none (decode exceptions caught and translated to `INVALID_FORMAT`) |
Rules run in fixed order (Format → Size band → Dimensions → Captured-at age → Blank/uniform); first failure short-circuits. Thresholds come from `UavQualityConfig`. Time comes from injected `TimeProvider` (defaults to `TimeProvider.System`) for deterministic tests.
Rules run in fixed order (Format → Size band → Dimensions → Captured-at age → Blank/uniform); first failure short-circuits. Thresholds come from `UavQualityConfig`. Time comes from injected `TimeProvider` (defaults to `TimeProvider.System`) for deterministic tests. AZ-1126: `UavTileMetadata.CapturedAt` is `DateTimeOffset`; Rule 4 compares via `UtcDateTime` without `DateTimeKind` branching.
### Service: UavTileUploadHandler (implements IUavTileUploadHandler, AZ-488)
| Method | Input | Output | Async | Error Types |
+33 -14
View File
@@ -2,15 +2,24 @@
## Platform
**CI Server**: Woodpecker CI (self-hosted)
**Agent architecture**: ARM64 (AMD64 prepared but not yet active)
**CI Server**: Woodpecker CI (self-hosted) — see suite [`_infra/ci/README.md`](../../../../_infra/ci/README.md) for agent install and registry wiring.
| Agent pool | Woodpecker label | Host | Role for this repo |
|------------|------------------|------|-------------------|
| ARM64 | `platform: arm64` | Colocated with CI server (Jetson) | Unit tests (`01-test`); builds `{branch}-arm` images |
| AMD64 | `platform: amd64` | Separate remote host | Builds `{branch}-amd64` images consumed by production deploy |
**Developer machine**: Apple Silicon Mac (M1/M2/M3, `darwin/arm64`). Local Docker runs native `linux/arm64` — see [tests/environment.md](../tests/environment.md) § Platform.
## Pipeline Stages
```mermaid
flowchart LR
Push[Push/PR to dev/stage/main] --> Test[01-test]
Test --> Build[02-build-push]
Push[Push/PR to dev/stage/main] --> Test[01-test arm64]
Test --> BuildArm[02-build-push arm64]
Test --> BuildAmd[02-build-push amd64]
BuildArm --> RegistryArm["registry … :branch-arm"]
BuildAmd --> RegistryAmd["registry … :branch-amd64"]
```
### 01-test (Unit Tests)
@@ -19,31 +28,41 @@ flowchart LR
|----------|-------|
| Trigger | push, pull_request, manual |
| Branches | dev, stage, main |
| Image | mcr.microsoft.com/dotnet/sdk:10.0 (was `:8.0` through cycle 3 — bumped by AZ-500) |
| Agent | `platform: arm64` only (unit tests are arch-neutral; suite convention) |
| Image | `mcr.microsoft.com/dotnet/sdk:10.0` |
| Steps | `dotnet restore``dotnet test` (Release config) |
| Output | TRX test results |
Integration and perf suites are **not** run in CI — they run locally via `scripts/run-tests.sh` and `scripts/run-performance-tests.sh` (Docker Compose).
### 02-build-push (Docker Build & Push)
| Property | Value |
|----------|-------|
| Trigger | push, manual |
| Branches | dev, stage, main |
| Depends on | 01-test (must pass) |
| Depends on | `01-test` (must pass) |
| Agent | `matrix:` fans out to `arm64` and `amd64` |
| Image | docker (DinD via socket mount) |
| Tag format | `{branch}-arm` (e.g., `dev-arm`) |
| Registry | Private (from secrets: registry_host, registry_user, registry_token) |
| Dockerfile | `SatelliteProvider.Api/Dockerfile` (same file for both arches — multi-arch base images) |
| Tag format | `{branch}-arm` (arm64 agent), `{branch}-amd64` (amd64 agent) |
| Registry | Gitea OCI via Caddy TLS (`registry_host`, `registry_user`, `registry_token` secrets) |
## Multi-Architecture Strategy
- Currently: ARM64 only
- Prepared: AMD64 entry commented out in matrix
- Tag suffix distinguishes architectures (`-arm`, `-amd`)
Follows the suite Woodpecker contract (`matrix:` + `labels: platform: ${PLATFORM}`):
| Matrix entry | Agent | Registry tag | Deploy consumer |
|--------------|-------|--------------|-----------------|
| `PLATFORM: arm64`, `TAG_SUFFIX: arm` | Colocated Jetson agent | e.g. `dev-arm` | Not used by current deploy profiles |
| `PLATFORM: amd64`, `TAG_SUFFIX: amd64` | Remote amd64 agent | e.g. `dev-amd64` | [`suite/_infra/deploy/satellite-provider/`](../../../../_infra/deploy/satellite-provider/) — Watchtower tracks `${BRANCH}-amd64` |
Production deploy is **amd64-only** (dedicated satellite-provider host). The arm64 build validates that the Dockerfile and gRPC proto codegen path work on the colocated agent.
## Secrets
| Secret | Purpose |
|--------|---------|
| registry_host | Container registry URL |
| registry_user | Registry username |
| registry_token | Registry password/token |
| registry_host | Container registry URL (Gitea + Caddy, host:port) |
| registry_user | Registry username (`azaion`) |
| registry_token | Gitea `ci-push` PAT (`write:package`) |
@@ -5,6 +5,9 @@
**Base image**: `mcr.microsoft.com/dotnet/aspnet:10.0` (was `:8.0` through cycle 3 — bumped by AZ-500)
**Build image**: `mcr.microsoft.com/dotnet/sdk:10.0` (was `:8.0` through cycle 3 — bumped by AZ-500)
**Build strategy**: Multi-stage (restore → build → publish → runtime)
**Target platform**: Native host architecture — no `platform:` pins in compose. **Mac M1** dev and the Woodpecker **arm64** agent run `linux/arm64`; the Woodpecker **amd64** agent runs `linux/amd64` for production images. On arm64, Docker build stages install Debian `protobuf-compiler` and set `PROTOBUF_PROTOC` (bundled `Grpc.Tools` protoc segfaults). See [tests/environment.md](../tests/environment.md) § Platform and [ci_cd_pipeline.md](ci_cd_pipeline.md) § Multi-Architecture Strategy.
**Registry tags** (Woodpecker `02-build-push`): `{branch}-arm` (arm64 agent), `{branch}-amd64` (amd64 agent). Production deploy (`suite/_infra/deploy/satellite-provider/`) pulls `${BRANCH}-amd64` via Watchtower.
**Exposed ports**: 8080 (HTTP), 8081 (management/metrics)
## Container Composition (docker-compose.yml)
+2 -2
View File
@@ -12,7 +12,7 @@ Application entry point. Configures DI container, sets up middleware, defines mi
| GET | `/api/satellite/tiles/latlon` | `GetTileByLatLon` | Download single tile by lat/lon/zoom. AZ-811 (cycle 8) renamed the query params `Latitude/Longitude/ZoomLevel``lat/lon/zoom` (OSM convention) and added strict validation: range-checked `lat`/`lon`/`zoom` via `WithValidation<GetTileByLatLonQuery>()`, plus a `RejectUnknownQueryParamsEndpointFilter` that rejects any extra query keys (catches typos like `?latitude=` that pre-AZ-811 silently bound to 0). Contract: `_docs/02_document/contracts/api/tile-latlon.md` v1.0.0 + `_docs/02_document/contracts/api/error-shape.md` v1.0.0. |
| POST | `/api/satellite/tiles/inventory` | `GetTilesInventory` | Bulk tile-existence/metadata lookup (AZ-505) — body is XOR of `tiles[{z,x,y}]` (Form A) and `locationHashes[uuid]` (Form B), each capped at 5000 entries. Response is one entry per request entry, in input order. AZ-794 (cycle 7) renamed the coord triple from `tileZoom/tileX/tileY``z/x/y` (OSM convention); AZ-796 (cycle 7) added strict input validation via `WithValidation<TileInventoryRequest>()` so malformed payloads return RFC 7807 `ValidationProblemDetails` instead of silently coercing to zero. Contracts: `_docs/02_document/contracts/api/tile-inventory.md` v2.0.0 + `_docs/02_document/contracts/api/error-shape.md` v1.0.0. |
| GET | `/api/satellite/tiles/mgrs` | `GetSatelliteTilesByMgrs` | MGRS stub (returns empty) |
| POST | `/api/satellite/upload` | `UploadUavTileBatch` | UAV tile batch upload (AZ-488) — multipart envelope, 5-rule quality gate, per-source UPSERT with `source='uav'`. Requires the `RequiresGpsPermission` policy. AZ-810 (cycle 8) added a strict **metadata-layer** validator that runs BEFORE the quality gate via the custom `UavUploadValidationFilter`: 14 rules covering required fields (`[JsonRequired]`), per-item ranges (`latitude`/`longitude`/`tileZoom`/`tileSizeMeters`/`capturedAt`), envelope alignment (`items.Count == files.Count`), and unknown-field / type-mismatch rejection via `UnmappedMemberHandling.Disallow`. Errors surface as RFC 7807 `ValidationProblemDetails` matching `error-shape.md` v1.0.0 with `errors["metadata.…"]` keys. Contract: `_docs/02_document/contracts/api/uav-tile-upload.md` v1.2.0 + `_docs/02_document/contracts/api/error-shape.md` v1.0.0. |
| POST | `/api/satellite/upload` | `UploadUavTileBatch` | UAV tile batch upload (AZ-488) — multipart envelope, 5-rule quality gate, per-source UPSERT with `source='uav'`. Requires the `RequiresGpsPermission` policy. AZ-810 (cycle 8) added a strict **metadata-layer** validator that runs BEFORE the quality gate via the custom `UavUploadValidationFilter`: 14 rules covering required fields (`[JsonRequired]`), per-item ranges (`latitude`/`longitude`/`tileZoom`/`tileSizeMeters`/`capturedAt`), envelope alignment (`items.Count == files.Count`), and unknown-field / type-mismatch rejection via `UnmappedMemberHandling.Disallow`. AZ-1126 (cycle 13) types `capturedAt` as `DateTimeOffset` with `UtcOffsetRequiredDateTimeOffsetConverter` — offset-less ISO strings fail at deserialization. Errors surface as RFC 7807 `ValidationProblemDetails` matching `error-shape.md` v1.0.0 with `errors["metadata.…"]` keys. Contract: `_docs/02_document/contracts/api/uav-tile-upload.md` v1.2.1 + `_docs/02_document/contracts/api/error-shape.md` v1.0.0. |
| POST | `/api/satellite/request` | `RequestRegion` | Queue region for async tile processing |
| GET | `/api/satellite/region/{id}` | `GetRegionStatus` | Get region processing status |
| POST | `/api/satellite/route` | `CreateRoute` | Create route with intermediate points. AZ-809 (cycle 8) added strict pre-handler validation via `WithValidation<CreateRouteRequest>()`: non-zero `id`, name length ∈ \[1, 200\], description length ≤ 1000, `regionSizeMeters` ∈ \[100, 10000\], `zoomLevel` ∈ \[0, 22\], `points` count ∈ \[2, 500\] with per-point lat/lon range checks, per-polygon NW-of-SE invariants, and the `createTilesZip ⇒ requestMaps` cross-field rule. Deserializer-layer failures (missing `[JsonRequired]` axes, unknown fields, type mismatches) are caught by `GlobalExceptionHandler` and produce the same RFC 7807 envelope. Contract: `_docs/02_document/contracts/api/route-creation.md` v1.0.0 + `_docs/02_document/contracts/api/error-shape.md` v1.0.0. |
@@ -46,7 +46,7 @@ Application entry point. Configures DI container, sets up middleware, defines mi
- `UavTileBatchUploadRequest` — multipart envelope with `metadata` (JSON string) and `files` (`IFormFileCollection`)
### Common/DTO (AZ-488)
- `UavTileMetadata`, `UavTileBatchMetadataPayload` — per-item metadata + envelope shape. AZ-810 cycle 8 added `[JsonRequired]` to every non-optional axis (`latitude`, `longitude`, `tileZoom`, `tileSizeMeters`, `capturedAt` on the per-item record; `items` on the envelope) so the deserializer rejects partial payloads with HTTP 400 before the FluentValidation + `IUavTileQualityGate` layers run. `flightId` stays optional per AZ-503 anonymous-flight semantics.
- `UavTileMetadata`, `UavTileBatchMetadataPayload` — per-item metadata + envelope shape. AZ-810 cycle 8 added `[JsonRequired]` to every non-optional axis (`latitude`, `longitude`, `tileZoom`, `tileSizeMeters`, `capturedAt` on the per-item record; `items` on the envelope) so the deserializer rejects partial payloads with HTTP 400 before the FluentValidation + `IUavTileQualityGate` layers run. AZ-1126 (cycle 13) adds `[JsonConverter(typeof(UtcOffsetRequiredDateTimeOffsetConverter))]` on `CapturedAt` so offset-less timestamps fail at deserialization. `flightId` stays optional per AZ-503 anonymous-flight semantics.
- `UavTileBatchUploadResponse`, `UavTileUploadResultItem` — per-item response shape
- `UavTileUploadStatus`, `UavTileRejectReasons` — string-constant enumerations exposed in the v1.0.0 contract
+1 -1
View File
@@ -92,7 +92,7 @@ Per-tile metadata payload inside a UAV batch upload (`POST /api/satellite/upload
- `Latitude`, `Longitude` (double)
- `TileZoom` (int)
- `TileSizeMeters` (double)
- `CapturedAt` (DateTime, UTC; subject to AZ-488 Rule 4 future-skew / age checks)
- `CapturedAt` (`DateTimeOffset`, JSON via `UtcOffsetRequiredDateTimeOffsetConverter` — AZ-1126): must include an explicit UTC offset (`Z` or `+00:00`); offset-less ISO-8601 strings are rejected at deserialization with HTTP 400. Freshness comparisons use `UtcDateTime` (no manual `DateTimeKind` normalization). Subject to AZ-488 Rule 4 future-skew / age checks at the quality-gate layer.
- `FlightId` (Guid?, JSON: `"flightId"`) — AZ-503 optional flight identifier. When set, the per-item `tiles.id` becomes `Uuidv5(TileNamespace, "{z}/{x}/{y}/uav/{flightId}")`, the on-disk path is `./tiles/uav/{flightId}/{z}/{x}/{y}.jpg`, and the UPSERT conflict key separates this row from rows belonging to other flights at the same cell. When `null`, the per-item id uses the zero-UUID `00000000-0000-0000-0000-000000000000` placeholder and the on-disk path uses the literal `none` segment (`./tiles/uav/none/{z}/{x}/{y}.jpg`). The placeholder UUID is purely a key-space marker — it never lands in the `flight_id` column (which stays `NULL`); the UPSERT uses `COALESCE(flight_id, '00000000-...')` for the conflict check.
### UavTileBatchMetadataPayload (added AZ-488)
@@ -1,7 +1,7 @@
# Module: Tests/SatelliteProvider.IntegrationTests
## Purpose
Console application that runs end-to-end integration tests against a live API instance. Designed to run in Docker alongside the API and PostgreSQL containers.
Console application that runs end-to-end integration tests against a live API instance. Designed to run in Docker alongside the API and PostgreSQL containers. Images build for the **host-native** Docker platform (`linux/arm64` on Apple Silicon Macs; `linux/amd64` on amd64 Linux). CI unit tests run arm64-only; production images are built on the Woodpecker amd64 agent (`{branch}-amd64`). See `docker-compose.tests.yml` header, [tests/environment.md](../tests/environment.md) § Platform, and [deployment/ci_cd_pipeline.md](../deployment/ci_cd_pipeline.md).
## Public Interface
@@ -19,7 +19,7 @@ Console application that runs end-to-end integration tests against a live API in
- `TileInventoryValidationTests` (added cycle 7 — AZ-796) — 16 tests: `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` (AZ-794 + AZ-796 intersection — exact AZ-777 Phase 1 reproducer body, asserts legacy `tileZoom/tileX/tileY` now yields 400), `TypeMismatch_Returns400`. Each test exercises one of the 9 validation rules end-to-end through `ValidationEndpointFilter<TileInventoryRequest>` + `GlobalExceptionHandler`, asserts HTTP 400 + RFC 7807 `ValidationProblemDetails` shape via the shared `ProblemDetailsAssertions` helper.
- `IdempotentPostTests` — pre-existing; cycle 7 adjusted the route-point payload from PascalCase (`Latitude`/`Longitude`) to camelCase (`lat`/`lon`) because the post-AZ-795 `UnmappedMemberHandling.Disallow` would otherwise reject the previously-silently-ignored fields. The `RoutePoint` DTO has carried `JsonPropertyName("lat"/"lon")` since AZ-309; cycle 7's strict JSON parsing exposed the test was sending the wrong shape and getting away with it via the pre-cycle-7 permissive deserializer.
- `RouteTileDeliveryGrpcTests` (added cycle 9 — AZ-1074/AZ-1075) — `RunHappyPath`, `RunInvalidRequests` (single waypoint / lat out of range / zoom out of range → `InvalidArgument`), `RunBackpressureSafe` (slow consumer preserves JPEG + SHA256), `RunRestConsistency` (REST route CSV tile keys overlap gRPC stream keys). Wired into both smoke and full suites via `Program.cs`.
- `RegionRequestValidationTests`, `CreateRouteValidationTests`, `UavUploadValidationTests`, `GetTileByLatLonValidationTests` (added cycle 8 — AZ-808..AZ-811) — per-endpoint strict-validation integration suites exercising `ValidationEndpointFilter<T>` / `UavUploadValidationFilter` / `RejectUnknownQueryParamsEndpointFilter` + `GlobalExceptionHandler` end-to-end. AZ-1113 (cycle 10) tightened assertions on deserializer/binding 400 paths to expect static messages per `error-shape.md` v1.0.1; `UavUploadValidationTests.MetadataNotAnObject_Returns400` additionally asserts the response body contains no `System.` substring.
- `RegionRequestValidationTests`, `CreateRouteValidationTests`, `UavUploadValidationTests`, `GetTileByLatLonValidationTests` (added cycle 8 — AZ-808..AZ-811) — per-endpoint strict-validation integration suites exercising `ValidationEndpointFilter<T>` / `UavUploadValidationFilter` / `RejectUnknownQueryParamsEndpointFilter` + `GlobalExceptionHandler` end-to-end. AZ-1113 (cycle 10) tightened assertions on deserializer/binding 400 paths to expect static messages per `error-shape.md` v1.0.1; `UavUploadValidationTests.MetadataNotAnObject_Returns400` additionally asserts the response body contains no `System.` substring. AZ-1126 (cycle 13): `ItemCapturedAtOffsetLess_Returns400` — offset-less `capturedAt` rejected with HTTP 400 mentioning `capturedAt`.
### Supporting Classes
- `Models.cs` — HTTP response DTOs for deserialization
+2 -1
View File
@@ -14,7 +14,8 @@ Existing baseline (pre-cycle-2) test classes cover `TileService`, `RegionService
- `Authentication/JwtTokenFactoryTests``Create_ProducesTokenValidatedByMatchingParameters`, `CreateExpired_TokenFailsValidationWithLifetimeException`, `Create_WithExtraClaims_PropagatesClaimsThroughValidation`, `TamperSignature_TokenFailsValidationWithSignatureException`. The factory itself lives in `SatelliteProvider.TestSupport` after AZ-491 (single source of truth); this project consumes it via `ProjectReference`.
### AZ-488 — UAV tile upload
- `UavTileQualityGateTests` — one happy path + ≥ 1 reject path per rule (Rule 1 INVALID_FORMAT × 2, Rule 2 SIZE_OUT_OF_BAND × 2, Rule 3 WRONG_DIMENSIONS × 1, Rule 4 CAPTURED_AT_FUTURE / _TOO_OLD × 2, Rule 5 IMAGE_TOO_UNIFORM × 1) + rule-ordering determinism. Uses a `FixedTimeProvider` for Rule-4 isolation and `UavTileImageFactory` for deterministic JPEG fixtures.
- `UavTileQualityGateTests` — one happy path + ≥ 1 reject path per rule (Rule 1 INVALID_FORMAT × 2, Rule 2 SIZE_OUT_OF_BAND × 2, Rule 3 WRONG_DIMENSIONS × 1, Rule 4 CAPTURED_AT_FUTURE / _TOO_OLD × 2, Rule 5 IMAGE_TOO_UNIFORM × 1) + rule-ordering determinism. Uses a `FixedTimeProvider` for Rule-4 isolation and `UavTileImageFactory` for deterministic JPEG fixtures. AZ-1126: freshness rules exercised against `DateTimeOffset` inputs.
- `UtcOffsetRequiredDateTimeOffsetConverterTests` (AZ-1126) — deserializer rejects offset-less ISO strings; accepts `Z` / `+00:00` forms.
- `UavTileUploadHandlerTests` — end-to-end with a mocked `ITileRepository`. Cycle-2 baseline: 1-item happy path, 3-item mixed batch (file written + `InsertAsync` called only for accepted), per-source UPSERT pass-through. AZ-503 additions: `HandleAsync_TwoFlightsSameCell_ProduceDistinctIdsAndPathsButSameLocationHash` (multi-flight coexistence with shared `location_hash`); `HandleAsync_IdenticalUpload_ProducesIdenticalIdAndDeterministicContentSha` (idempotent re-insert preserves deterministic `id` + `content_sha256`). AZ-1113 (cycle 10): `HandleAsync_InvalidMetadataJson_ReturnsEnvelopeError` — defense-in-depth metadata parse returns static envelope error (no `ex.Message` echo).
### AZ-1124 — PT-10 gRPC stream perf harness (cycle 12)
+13
View File
@@ -0,0 +1,13 @@
# Ripple Log — Cycle 13
Tasks: AZ-1126 (capturedAt DateTimeOffset / F-AZ810-2)
- `_docs/02_document/modules/common_dtos.md``UavTileMetadata.CapturedAt` type + converter (changed by AZ-1126)
- `_docs/02_document/modules/api_program.md` — upload endpoint contract v1.2.1 + `UtcOffsetRequiredDateTimeOffsetConverter` (changed by AZ-1126)
- `_docs/02_document/components/03_tile_downloader/description.md``UavTileQualityGate` Rule 4 uses `DateTimeOffset.UtcDateTime` (changed by AZ-1126)
- `_docs/02_document/modules/tests_unit.md``UtcOffsetRequiredDateTimeOffsetConverterTests` (Step 13)
- `_docs/02_document/modules/tests_integration.md``ItemCapturedAtOffsetLess_Returns400` (Step 13)
- `_docs/02_document/tests/blackbox-tests.md` — BT-34 (test-spec sync Step 12)
- `_docs/02_document/tests/traceability-matrix.md` — AZ-1126 AC-1..AC-4 rows (test-spec sync Step 12)
Contract `uav-tile-upload.md` v1.2.1 and `error-shape.md` were patched during implement (batch 01).
+19
View File
@@ -428,3 +428,22 @@ Cycle 8 extends the AZ-795 shared validation infrastructure (FluentValidation +
**AC trace**: AZ-1113 AC-1..AC-3 (blackbox); AC-4 (`UavTileUploadHandler` envelope) verified by `UavTileUploadHandlerTests` unit only; AC-5 (contract doc) verified at Step 13.
**Notes**: This is a cross-cutting tightening of Inv-5 for 4xx paths — BT-27..BT-31 strict-validation scenarios remain the binding functional specs; BT-33 adds the message-content contract on top. SEC-14..SEC-16 mirror these three sub-cases in the security category.
## Cycle 13 — AZ-1126 `capturedAt` DateTimeOffset (F-AZ810-2 closure)
Cycle 13 tightens UAV upload metadata time handling: `UavTileMetadata.CapturedAt` is `DateTimeOffset` with a strict JSON converter requiring an explicit UTC offset. Offset-less ISO-8601 strings fail at deserialization before FluentValidation. Contract `uav-tile-upload.md` patched 1.2.0 → 1.2.1.
## BT-34: UAV Upload `capturedAt` Requires Explicit UTC Offset
**Trigger**: `POST /api/satellite/upload` multipart calls exercising AZ-1126 AC-2 and AC-3 on the existing AZ-810 validation surface.
**Precondition**: API up; valid JWT with `permissions:["GPS"]`. `uav-tile-upload.md` v1.2.1 frozen.
**Expected**: Offset-less timestamps rejected at deserializer; offset-aware UTC clients unchanged.
| # | AC | Trigger excerpt | Expected | Test method |
|---|-----|-----------------|----------|-------------|
| 1 | AC-2 | Valid JPEG + metadata with `capturedAt: "2026-06-26T12:00:00"` (no `Z` / offset) | HTTP 400; `errors` mentions `capturedAt` | `UavUploadValidationTests.ItemCapturedAtOffsetLess_Returns400` |
| 2 | AC-3 | Valid JPEG + metadata with `capturedAt` as ISO-8601 UTC (`...Z` or `...+00:00`) | HTTP 200 (happy path unchanged vs AZ-810 BT-30 `pos`) | `UavUploadValidationTests.HappyPath_Returns200` + existing AZ-488 suite |
**Pass criterion**: Sub-case 1 returns HTTP 400 with `capturedAt` in the error surface. Sub-case 2 remains green in the full integration suite (cycle 13 Step 11: 457 unit + all integration groups passed).
**AC trace**: AZ-1126 AC-1 (type migration — unit: `UtcOffsetRequiredDateTimeOffsetConverterTests`, `UavTileMetadataValidatorTests`, `UavTileQualityGateTests`); AC-2, AC-3 (blackbox); AC-4 (contract doc — verified at Step 13).
**Notes**: Closes security finding F-AZ810-2. Does not change inventory response `capturedAt` (`DateTime?` read path) or gRPC surfaces. BT-30 sub-cases 9a/9b (future/too-old freshness) remain valid for offset-aware timestamps only.
+23
View File
@@ -1,5 +1,28 @@
# Test Environment
## Platform
Three execution contexts — all use **native** Docker for the host CPU (no Rosetta / QEMU emulation).
| Context | Host CPU | Docker platform | How tests/builds run |
|---------|----------|-----------------|----------------------|
| **Local dev** | Apple Silicon Mac (`darwin/arm64`, e.g. M1) | `linux/arm64` | `scripts/run-tests.sh` sets `DOCKER_DEFAULT_PLATFORM=linux/arm64`; compose builds follow the host |
| **CI unit tests** | Woodpecker `platform: arm64` agent (colocated Jetson) | `linux/arm64` | `.woodpecker/01-test.yml``dotnet test` in sdk image (arm64-only by suite convention) |
| **CI image build** | Woodpecker `platform: arm64` **or** `amd64` agent | matches agent | `.woodpecker/02-build-push.yml` matrix — same Dockerfile, tags `{branch}-arm` / `{branch}-amd64` |
**Mac M1 rule**: do **not** pin `platform: linux/amd64` in compose files or Dockerfiles. That forces Rosetta/QEMU emulation and logs warnings such as `The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8)`.
**amd64 agent rule**: the remote Woodpecker amd64 agent builds production images natively. Deploy (`suite/_infra/deploy/satellite-provider/`) pulls `${BRANCH}-amd64` — built only on the amd64 agent, not cross-compiled from arm64.
| Surface | Behavior |
|---------|----------|
| `scripts/run-tests.sh` | On `darwin/arm64`, exports `DOCKER_DEFAULT_PLATFORM=linux/arm64` before every `docker run` / `docker compose` |
| `docker-compose.tests.yml` | No `platform:` override — images match the host (arm64 on Mac, amd64 on an amd64 Linux dev box) |
| `docker-compose.yml` (dev / perf) | Same — no arch pin |
| API + integration Dockerfiles | Multi-arch `sdk:10.0` / `aspnet:10.0`; build stage installs Debian `protobuf-compiler` + `PROTOBUF_PROTOC=/usr/bin/protoc` on **arm64** (bundled `Grpc.Tools` `linux_arm64/protoc` segfaults; harmless on amd64) |
Suite CI/agent reference: [`suite/_infra/ci/README.md`](../../../../_infra/ci/README.md) § Agent pools and § Build-push step.
## Infrastructure
| Component | Technology | Configuration |
@@ -164,6 +164,10 @@
| 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 | ✓ |
| AZ-1126 AC-1 | `UavTileMetadata.CapturedAt` is `DateTimeOffset`; freshness comparisons use UTC without manual `DateTimeKind` normalization | `UtcOffsetRequiredDateTimeOffsetConverterTests` (unit); `UavTileMetadataValidatorTests` + `UavTileQualityGateTests` + `UavTileUploadHandlerTests` (unit); existing AZ-810 BT-30 sub-cases 9a/9b remain green with offset-aware timestamps (cycle 13 Step 11 full run) | ✓ |
| AZ-1126 AC-2 | Offset-less `capturedAt` (no explicit UTC offset) rejected with HTTP 400 referencing `capturedAt` | BT-34 sub-case 1 (blackbox); `UavUploadValidationTests.ItemCapturedAtOffsetLess_Returns400` (integration) | ✓ |
| AZ-1126 AC-3 | Compliant clients sending `Z` or `+00:00` timestamps unchanged | BT-34 sub-case 2 (blackbox); `UavUploadValidationTests.HappyPath_Returns200` + full AZ-488 integration suite green (cycle 13 Step 11) | ✓ |
| AZ-1126 AC-4 | `uav-tile-upload.md` v1.2.1 documents offset requirement and F-AZ810-2 closure | Doc-state AC — contract patch in cycle 13 batch; verified at Step 13 review | ✓ |
## Restrictions → Test Mapping
@@ -220,7 +224,8 @@
| Cycle 10 — AZ-1113 REST 400 error message sanitization (integration + unit + blackbox + contract patch) | 3 integration assertion paths (inventory deserializer, latlon bind, UAV metadata) + 3 unit methods (`GlobalExceptionHandlerTests` ×2, `UavTileUploadHandlerTests` ×1) + 1 blackbox (BT-33 with 3 sub-cases) + 3 security (SEC-14..SEC-16) + `error-shape.md` v1.0.1 patch | 5/5 in-scope (AZ-1113 AC-1..AC-5) | — |
| Cycle 11 — AZ-1123 perf compose documentation (deployment + test env docs) | doc-only (`containerization.md` compose overlays, `environment.md` perf cross-link) | 3/3 in-scope (AZ-1123 AC-1..AC-3); doc-verified at Step 13 | — |
| Cycle 12 — AZ-1124 PT-10 gRPC stream perf (perf harness + unit) | 1 perf (PT-10) + 3 unit (`PerfBootstrapPt10Tests`) + integration bootstrap (`SatelliteProvider.IntegrationTests --run-pt10`) | 6/6 in-scope (AZ-1124 AC-1..AC-6); 1 AC gated at Step 15 (AC-3); 1 doc-verified at Step 13 (AC-5) | — |
| **Total** | **173** | **130/130 in-scope (100%); 3 ACs gated at Step 15 (2 AZ-504 + 1 AZ-1124 AC-3); 11 prior-cycle ACs doc-verified at Step 13 (2 cycle-7 + 8 cycle-8 + 1 AZ-1124 AC-5 pending); 2 advisory non-tested (cycle-8 AZ-809 AC-9/AC-10)** | **8/8 (100%)** |
| Cycle 13 — AZ-1126 capturedAt DateTimeOffset (integration + unit + blackbox + contract patch) | 1 integration method (`UavUploadValidationTests.ItemCapturedAtOffsetLess_Returns400`) + 4 unit files (`UtcOffsetRequiredDateTimeOffsetConverterTests`, updated UAV validator/gate/handler tests) + 1 blackbox (BT-34 with 2 sub-cases) + `uav-tile-upload.md` v1.2.1 patch | 4/4 in-scope (AZ-1126 AC-1..AC-4); 1 doc-verified at Step 13 (AC-4); closes F-AZ810-2 | — |
| **Total** | **174** | **134/134 in-scope (100%); 3 ACs gated at Step 15 (2 AZ-504 + 1 AZ-1124 AC-3); 11 prior-cycle ACs doc-verified at Step 13 (2 cycle-7 + 8 cycle-8 + 1 AZ-1124 AC-5); 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 `Blocks` link in Jira and an entry in `_docs/02_tasks/_dependencies_table.md`. The deferred rows above are marked `◐ deferred → AZ-505` so the matrix surfaces the scope boundary explicitly.
+7
View File
@@ -264,6 +264,13 @@ Step 9 cycle 10: 1 task created (AZ-1113 = 2 pts) — REST 400 error message san
Step 9 cycle 11: 1 task created (AZ-1123 = 1 pt) — document `docker-compose.perf.yml` host-port conflict playbook (cycle 10 retro action).
Step 9 cycle 12: 1 task created (AZ-1124 = 3 pts) — PT-10 gRPC `DeliverRouteTiles` stream perf scenario (cycle 911 retro carry-over).
Step 9 cycle 13: 1 task created (AZ-1126 = 2 pts) — `DateTime``DateTimeOffset` on `UavTileMetadata.capturedAt` (F-AZ810-2). Child of AZ-795.
Step 9 cycle 14: 1 task created (AZ-1131 = 1 pt) — align `environment.md` integration command with `run-tests.sh` (cycle 13 retro carry-over).
### Step 9 cycle 14 (environment.md integration command — AZ-1131)
| Task | Depends On | Points | Status |
|------|-----------|--------|--------|
| AZ-1131 environment.md integration command | — | 1 | Todo |
### Step 9 cycle 13 (capturedAt DateTimeOffset — AZ-1126)
@@ -0,0 +1,86 @@
# Align environment.md integration test command with run-tests.sh
**Task**: AZ-1131_environment_md_integration_command
**Name**: Align environment.md integration command with run-tests.sh
**Description**: Fix stale integration-test orchestration docs that still reference `docker-compose.yml` + `docker-compose.tests.yml` while `scripts/run-tests.sh` uses self-contained `docker-compose.tests.yml` only.
**Complexity**: 1 points
**Dependencies**: None
**Component**: test environment documentation
**Tracker**: AZ-1131 (https://denyspopov.atlassian.net/browse/AZ-1131)
**Epic**: None
## Problem
`_docs/02_document/tests/environment.md` § Test Execution documents:
`docker-compose -f docker-compose.yml -f docker-compose.tests.yml up --build --abort-on-container-exit`
`scripts/run-tests.sh` (canonical Step 11 gate) runs integration tests with:
`docker compose -f docker-compose.tests.yml up --build --abort-on-container-exit --exit-code-from integration-tests`
`docker-compose.tests.yml` is self-contained (postgres + api + integration-tests). The dual-file command is stale since the test stack was split out. This mismatch has been a carry-over since cycle 12 retro and misleads operators copying docs instead of the script.
## Outcome
- `environment.md` matches the real integration test orchestration path
- Operators can run integration tests by following either `environment.md` or `run-tests.sh` without conflicting instructions
- Stale dual-compose references in agent-facing docs are corrected or cross-linked
## Scope
### Included
- Update `environment.md` § Test Execution to document `docker-compose.tests.yml` only and cite `scripts/run-tests.sh` as the canonical entry point
- Ripple fix: `README.md` and `AGENTS.md` integration-test command lines (same stale dual-compose pattern)
- No compose file or script behavior changes
### Excluded
- Historical task specs in `_docs/02_tasks/done/` (AZ-285 AC-2 records the old command — leave as historical artifact)
- Perf orchestration docs (already correct — uses `docker-compose.yml` + `docker-compose.perf.yml`)
- CI Woodpecker pipeline changes
## Acceptance Criteria
**AC-1: environment.md accuracy**
Given `environment.md` § Test Execution
When an operator reads the documented integration command
Then it matches `scripts/run-tests.sh` Step 2 (`docker compose -f docker-compose.tests.yml …`) and names the script as canonical
**AC-2: Agent-facing doc ripple**
Given `README.md` and `AGENTS.md` integration test sections
When an operator follows those instructions
Then they reference `docker-compose.tests.yml` only (or point to `./scripts/run-tests.sh` exclusively)
**AC-3: No runtime change**
Given the task is complete
When `./scripts/run-tests.sh --smoke` is run
Then behavior is unchanged (docs-only delta)
## Non-Functional Requirements
**Compatibility**
- Documented manual command must remain valid on darwin/arm64 without requiring `docker-compose.yml`
## Unit Tests
| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-3 | N/A (docs only) | Verified by grep + optional smoke run |
## Blackbox Tests
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|--------|------------------------|-------------|-------------------|----------------|
| AC-1 | `environment.md` on disk | Ripgrep for `docker-compose.yml -f docker-compose.tests.yml` | Zero matches in `environment.md` | — |
| AC-2 | `README.md`, `AGENTS.md` | Ripgrep for stale dual-compose integration command | Zero matches or explicit redirect to `run-tests.sh` | — |
## Constraints
- Documentation-only — no compose or script edits unless a comment in `run-tests.sh` is needed for cross-link clarity
## Risks & Mitigation
**Risk 1: Operators relied on dual-compose for host port mapping**
- *Mitigation*: `docker-compose.tests.yml` does not publish postgres to host; document that this is intentional (internal network only)
+72
View File
@@ -0,0 +1,72 @@
# Deploy Report — Cycle 13 (AZ-1126)
**Date**: 2026-06-26
**Cycle**: 13
**Scope**: Migrate UAV upload `capturedAt` to `DateTimeOffset` (F-AZ810-2 closure).
## What is shipping
### Code changes
| Area | Change |
|------|--------|
| `UavTileMetadata.CapturedAt` | `DateTime``DateTimeOffset` |
| `UtcOffsetRequiredDateTimeOffsetConverter` | **New** — rejects offset-less ISO-8601 at deserialization |
| `UavTileMetadataValidator` | Freshness rules use `UtcDateTime` without `DateTimeKind` branching |
| `UavTileQualityGate` / `UavTileUploadHandler` | UTC comparisons without manual kind normalization |
| `UavUploadValidationFilter` | Propagates converter `JsonException.Message` for metadata parse failures |
| Unit + integration tests | Offset-less rejection + backward-compatible `Z` / `+00:00` happy paths |
| `uav-tile-upload.md` | v1.2.0 → **v1.2.1** (offset requirement documented) |
### Database migrations
**None.**
### Configuration changes
| Setting | Change |
|---------|--------|
| New env vars | **None** |
| Container image | Rebuild only — same `aspnet:10.0` base; no Dockerfile changes |
| Consumer contracts | `uav-tile-upload.md` patch bump — wire shape unchanged for offset-aware clients |
### Contract changes (consumer-visible)
| Contract | Change | Consumer action |
|----------|--------|-----------------|
| `uav-tile-upload.md` v1.2.1 | `capturedAt` must include explicit UTC offset (`Z` or `+00:00`); offset-less strings → HTTP 400 | UAV upload clients sending `"2026-06-26T12:00:00"` without offset must add `Z` or `+00:00` |
| REST / gRPC wire shapes (other) | Unchanged | No action |
## Verification gates passed in this cycle
| Gate | Result | Evidence |
|------|--------|----------|
| Step 11 — Functional tests | **PASS** | Full suite via `./scripts/run-tests.sh` (mode=full) |
| Step 12 — Test-Spec Sync | **PASS** | Traceability AZ-1126 AC-1..AC-4 |
| Step 13 — Update Docs | **PASS** | `ripple_log_cycle13.md`, module + contract docs |
| Step 14 — Security Audit | **PASS** (delta) | `security_report_cycle13.md`; F-AZ810-2 **resolved** |
| Step 15 — Performance Test | **PASS** | `perf_2026-06-26_cycle13.md` — 11/11 thresholds |
## Security carry-overs (post-cycle-13)
| ID | Status |
|----|--------|
| F-AZ810-2 | **Resolved** (AZ-1126) |
| D-AZ795-1 | Open — FluentValidation 12.0.0 → 12.1.1 |
| D2-cy4 | Open — test SDK JWT advisory (test-runtime only) |
## Operator runbook
1. **Commit and push** cycle-13 changes to `origin/dev`; confirm Woodpecker `01-test` + `02-build-push` green.
2. **No migration** — deploy new API image only.
3. **Smoke-test** after deploy:
- POST `/api/satellite/upload` with valid JPEG + `capturedAt` lacking offset → HTTP 400 referencing `capturedAt`
- POST with `capturedAt` as ISO-8601 UTC (`...Z` or `...+00:00`) → HTTP 200 on otherwise valid payload
- Region, route, inventory, gRPC tile delivery happy paths unchanged
4. **Notify UAV upload consumers** (`gps-denied-onboard`, mission planner) of the offset requirement per contract v1.2.1.
## Release note
`/release` prerequisites (`scripts/deploy.sh`, `_docs/04_release/`) are **not present** in this repo — production promotion remains operator-driven (image build + compose on target host). Step 16.5 should be **skipped** unless release infrastructure is onboarded.
**Verdict**: Cleared for retrospective (Step 17). Release (16.5) skipped — no release execution harness.
@@ -0,0 +1,36 @@
# Dependency Scan (Cycle 13)
**Date**: 2026-06-26
**Mode**: Delta scan
**Scope**: Cycle-13 delta over cycle-10 baseline. Surface = AZ-1126 (`DateTimeOffset` migration — no package manifest changes).
**Method**: `dotnet list SatelliteProvider.sln package --vulnerable`.
## Cycle-13 Package Manifest Diff
| csproj | Cycle 10 baseline | Cycle 13 change |
|--------|-------------------|-----------------|
| All csproj | unchanged | **+0** packages added or bumped |
## Vulnerable Package Scan (2026-06-26)
| Project | Finding | Severity | Notes |
|---------|---------|----------|-------|
| `SatelliteProvider.Api` | none | — | Production runtime — clean |
| `SatelliteProvider.Common` | none | — | `UtcOffsetRequiredDateTimeOffsetConverter` is in-repo code |
| `SatelliteProvider.IntegrationTests` | transitive JWT 7.0.3 | Moderate | GHSA-59j7-ghrg-fj52 — test-runtime only (pre-existing) |
| `SatelliteProvider.TestSupport` | `System.IdentityModel.Tokens.Jwt` 7.0.3 | Moderate | test-runtime only — pre-existing |
## Cycle-13 Findings
**No new dependency CVEs.** AZ-1126 is a code-only DTO/converter change.
## Carry-overs
- **D-AZ795-1** (Low): FluentValidation 12.0.0 → 12.1.1 — still open
- **D2-cy4** (Medium, test-runtime): JWT test packages — still open
## Verdict
**PASS** (cycle-13 delta) — zero new CVEs.
Cumulative: **PASS_WITH_WARNINGS** — D2-cy4 + D-AZ795-1 carry-overs unchanged.
@@ -0,0 +1,13 @@
# Infrastructure & Configuration Review (Cycle 13)
**Date**: 2026-06-26
**Mode**: Delta scan
**Scope**: Cycle-13 infrastructure changes only.
| File | Change | Security relevance |
|------|--------|-------------------|
| All Docker / compose / CI / appsettings | **unchanged** | AZ-1126 is application-code + contract doc only |
## Verdict
**PASS** (cycle-13 delta) — no infrastructure surface change.
+28
View File
@@ -0,0 +1,28 @@
# OWASP Top 10 Review (Cycle 13)
**Date**: 2026-06-26
**Framework**: OWASP Top 10:2021
**Mode**: Delta review — AZ-1126 over cycle-10 baseline.
| Category | Cycle-10 status | Cycle-13 delta |
|----------|-----------------|----------------|
| A01 — Broken Access Control | PASS | No change |
| A02 — Cryptographic Failures | PASS | No change |
| A03 — Injection | PASS | No change |
| A04 — Insecure Design | PASS | No change |
| A05 — Security Misconfiguration | PASS | No change |
| A06 — Vulnerable Components | PASS_WITH_WARNINGS | No new packages; D-AZ795-1 + D2-cy4 carry-overs unchanged |
| A07 — Auth Failures | PASS | No change |
| A08 — Data Integrity Failures | PASS | Improved time-handling integrity on UAV upload metadata |
| A09 — Logging / Monitoring Failures | PASS_WITH_WARNINGS → **improved** | F-AZ810-2 **resolved**; F-AZ795-1/2 + F-AZ810-1 remain resolved |
| A10 — SSRF | N/A | No URL-fetch changes |
## A08 / A09 detail
AZ-1126 eliminates ambiguous `DateTimeKind.Unspecified` handling on the UAV upload metadata input path. Offset-less client timestamps now fail fast with HTTP 400 instead of being interpreted against host local timezone in dev environments.
## Verdict
**PASS** (cycle-13 delta).
Cumulative: **PASS_WITH_WARNINGS** — dependency carry-overs only (D-AZ795-1, D2-cy4).
@@ -0,0 +1,49 @@
# Security Audit Report (Cycle 13)
**Date**: 2026-06-26
**Scope**: Cycle-13 delta — AZ-1126 (`capturedAt` DateTimeOffset / F-AZ810-2 closure).
**Trigger**: `/autodev` Step 14 — user chose **A) Run security audit**.
**Verdict (cycle-13 delta)**: **PASS** — F-AZ810-2 resolved; 0 new Critical/High/Medium.
**Verdict (cumulative)**: **PASS_WITH_WARNINGS** — D-AZ795-1, D2-cy4 remain open.
## Summary
| Severity | Cycle 13 at audit | Cumulative open |
|----------|-------------------|-----------------|
| Critical | 0 | 0 |
| High | 0 | 0 |
| Medium | 0 | 1 (D2-cy4 test-runtime) |
| Low | 0 new | 1 (D-AZ795-1) |
## OWASP Top 10:2021 (cycle-13 delta)
See `owasp_review_cycle13.md` — A08/A09 improved; all other categories unchanged PASS/N/A.
## Findings
| # | Severity | Category | Location | Title | Status |
|---|----------|----------|----------|-------|--------|
| F-AZ810-2 | Low | Time-handling (A08/A09) | `UavTileMetadata.CapturedAt` | `DateTime` vs `DateTimeOffset` | **RESOLVED** (AZ-1126) |
## Carry-overs (still open)
- **D-AZ795-1** — FluentValidation 12.0.0 → 12.1.1
- **D2-cy4** — test SDK transitive JWT advisory (Moderate, test-runtime only)
## Recommendations
### Immediate
- None blocking cycle 13 ship.
### Short-term
- D-AZ795-1: bump FluentValidation when a coordinated package bump task lands.
### Long-term
- D2-cy4: pin JWT test packages when upstream resolves GHSA-59j7-ghrg-fj52 for 7.0.3 line.
## Artifacts
- `dependency_scan_cycle13.md`
- `static_analysis_cycle13.md`
- `owasp_review_cycle13.md`
- `infrastructure_review_cycle13.md`
@@ -0,0 +1,37 @@
# Static Analysis (Cycle 13)
**Date**: 2026-06-26
**Mode**: Delta scan
**Scope**: AZ-1126 `capturedAt``DateTimeOffset` + `UtcOffsetRequiredDateTimeOffsetConverter`. Cycle-10 baseline remains authoritative elsewhere.
**Files in scope**:
- `SatelliteProvider.Common/DTO/UavTileMetadata.cs`
- `SatelliteProvider.Common/Json/UtcOffsetRequiredDateTimeOffsetConverter.cs`
- `SatelliteProvider.Api/Validators/UavTileMetadataValidator.cs`
- `SatelliteProvider.Api/Validators/UavUploadValidationFilter.cs`
- `SatelliteProvider.Services.TileDownloader/UavTileQualityGate.cs`
- `SatelliteProvider.Services.TileDownloader/UavTileUploadHandler.cs`
- Unit + integration tests for offset-less rejection
**Method**: Read changed call sites; verify offset-less ISO strings rejected before persistence; confirm no new `ex.Message` echoes; grep for remaining `DateTimeKind` branching on upload path.
## Resolved findings (AZ-1126)
### F-AZ810-2 — `UavTileMetadata.CapturedAt` typed `DateTime` not `DateTimeOffset` (Low / Informational) — **RESOLVED**
- **Location**: `UavTileMetadata.cs`, validators, quality gate, upload handler.
- **Resolution**: `CapturedAt` is `DateTimeOffset` with `UtcOffsetRequiredDateTimeOffsetConverter` rejecting offset-less strings at deserialization. Freshness rules compare via `UtcDateTime`. Integration test `ItemCapturedAtOffsetLess_Returns400` binds the rejection path.
## Pass areas (cycle-13 delta)
| Area | Result |
|------|--------|
| SQL injection | N/A — no SQL changes |
| Hardcoded secrets | None introduced |
| Information disclosure (400 paths) | Unchanged from AZ-1113 — static strings preserved |
| New attack surface | Narrower — ambiguous timestamps rejected earlier |
| Inventory read path | `TileInventoryEntry.CapturedAt` remains `DateTime?` — intentional, out of scope |
## Verdict
**PASS** (cycle-13 delta) — F-AZ810-2 closed; zero new findings.
@@ -0,0 +1,37 @@
# Performance Report — Cycle 13
**Date**: 2026-06-26
**Cycle**: 13 (AZ-1126 capturedAt DateTimeOffset)
**Runner**: `scripts/run-performance-tests.sh` (default: `PERF_REPEAT_COUNT=20`, `PERF_UAV_BATCH_SIZE=10`, `PERF_PT10_SLOW_MS=50`)
**Stack**: `docker compose -f docker-compose.yml -f docker-compose.perf.yml up -d --build`
**API_URL**: `https://localhost:18980`
**Verdict**: **PASS** (11/11 thresholds; exit 0)
## Notes
First perf attempt failed at PT-01 (exit 7) because the perf stack was not running — resolved by starting the compose overlay before re-run.
## REST scenarios (PT-01..PT-08)
| Scenario | Result | Key metric |
|----------|--------|------------|
| PT-01 cold tile | Pass | 1519 ms |
| PT-02 cached tile | Pass | 230 ms |
| PT-03 region 200m | Pass | 2433 ms |
| PT-04 region 500m stitch | Pass | 2158 ms |
| PT-05 concurrent regions | Pass | 2378 ms |
| PT-06 route create | Pass | 203 ms |
| PT-07 cold/warm | Pass | warm p95 54 ms vs cold 2148 ms |
| PT-08 UAV batch | Pass | batch p95 225 ms |
AZ-1126 does not change perf probe payloads (PT-08 still uses offset-aware `capturedAt` from fixture generator).
## PT-10 (gRPC stream)
| Metric | p50 | p95 | Threshold | Verdict |
|--------|-----|-----|-----------|---------|
| first_batch_ms | 43 ms | 63 ms | ≤ 30000 ms | Pass |
| total_stream_ms | 43 ms | 64 ms | ≤ 120000 ms | Pass |
| slow-consumer | — | — | completes without DeliveryError | Pass |
Iteration 1 cold path ~3950 ms first batch (empty volume); iterations 220 warm cached.
@@ -0,0 +1,48 @@
# Retrospective — Cycle 13 (2026-06-26)
**Tasks**: AZ-1126 (capturedAt DateTimeOffset, 2 SP). **1 task, 2 SP, 1 batch.**
**Mode**: cycle-end. Step 16.5 (Release) **skipped** (no release harness).
**Previous retro**: `retro_2026-06-26_cycle12.md`
## Implementation Summary
| Metric | Cycle 13 | Δ vs cycle 12 |
|--------|----------|---------------|
| Tasks implemented | **1** | unchanged |
| Total complexity delivered | **2 SP** | -1 SP |
| Blocked tasks | **0** | unchanged |
| Auto-fix attempts | **1** (filter message propagation) | new |
## Quality
| Gate | Result |
|------|--------|
| Code review | PASS_WITH_WARNINGS (batch_01_cycle13) |
| Step 11 full suite | **PASS** (pre-verified in Step 10) |
| Step 14 security | **PASS** (delta) — F-AZ810-2 **resolved** |
| Step 15 perf | **PASS** 11/11 (first run failed: stack not up) |
| Step 16 deploy | **PASS**`deploy_cycle13.md` |
## Cycle 13 delta
- **F-AZ810-2 closed** — cycle 12 retro Action #1 shipped; `DateTimeOffset` + strict converter eliminates `DateTimeKind` ambiguity on UAV upload path.
- **Filter regression caught in batch** — generic `JsonException` handler initially masked converter diagnostics; fixed by propagating exception message.
- **Security finding → task → closure** — 2 SP dedicated cycle resolved a multi-cycle Low carry-over without scope creep.
## Comparison with cycle 12 retro actions
| Cycle 12 action | Cycle 13 outcome |
|-----------------|------------------|
| F-AZ810-2 DateTimeOffset (~1 SP) | **Done** (AZ-1126) |
| Align `environment.md` integration command | **Open** — not scheduled |
| PT-09 shell harness promotion | **Open** — optional |
## Top 3 Improvement Actions (cycle 14 candidates)
1. **D-AZ795-1** — bump FluentValidation 12.0.0 → 12.1.1 (~1 SP) — sole remaining Low prod dependency finding
2. **Align `environment.md` integration command** with `run-tests.sh` (`docker-compose.tests.yml` only) (~0.5 SP) — third-cycle carry-over
3. **Perf gate preflight** — document or script-check that perf compose stack is up before PT-01 (~0.5 SP) — cycle 13 first-run exit 7
## Cycle 13 Verdict
**Successful security-closure cycle** — AZ-1126 delivered a focused type migration with full gate coverage (security + perf + deploy). One auto-fix in the validation filter; no architecture drift.
@@ -0,0 +1,28 @@
# Structural Snapshot — 2026-06-26 (post-cycle 13, capturedAt DateTimeOffset)
Cycle 13 delta against `structure_2026-06-25_cycle10.md` (no cycle 11/12 structure snapshots on disk). Source: `_docs/02_document/module-layout.md` + on-disk `*.csproj` graph.
## Projects
| Layer | csproj | Cycle 13 delta |
|-------|--------|----------------|
| 2 (Common) | `SatelliteProvider.Common` | `UtcOffsetRequiredDateTimeOffsetConverter` + `UavTileMetadata.CapturedAt` type change |
| 4 (API) | `SatelliteProvider.Api` | Validator + `UavUploadValidationFilter` message propagation |
| 3 (Application) | `SatelliteProvider.Services.TileDownloader` | Quality gate + upload handler UTC comparisons |
| 6 (Tests) | `SatelliteProvider.Tests`, `SatelliteProvider.IntegrationTests` | Converter + UAV validation tests |
**Project count**: **10** (unchanged).
## Cross-Project Import Edges
**Total ProjectReference edges**: **23** (unchanged). **Import cycles**: 0.
## Contract coverage
| Surface | Contract | Cycle 13 delta |
|---------|----------|----------------|
| UAV upload metadata | `uav-tile-upload.md` v1.2.1 | patch — explicit UTC offset on `capturedAt` |
| gRPC `DeliverRouteTiles` | `tile_provision.proto` | unchanged |
| REST error envelope | `error-shape.md` v1.0.1 | unchanged |
**gRPC perf coverage**: PT-10 verified (cycle 12); cycle 13 did not regress.
+6 -4
View File
@@ -41,6 +41,12 @@ If the enum's wire string happens to match a member name case-insensitively (e.g
Source: _docs/06_metrics/perf_2026-06-26_cycle12.md
## Ring buffer (last 15 entries — newest at top)
- [2026-06-26] [process] Multi-cycle security carry-overs that name a concrete finding ID and fit ≤2 SP ship cleanly as a sole cycle theme — cycle 12 retro Action #1 → cycle 13 AZ-1126 closed F-AZ810-2 in one batch with full security + perf gate coverage.
Source: _docs/06_metrics/retro_2026-06-26_cycle13.md
- [2026-06-26] [testing] Custom `JsonConverter` exceptions must propagate through boundary filters — a generic metadata parse string in `UavUploadValidationFilter` masked `UtcOffsetRequiredDateTimeOffsetConverter` diagnostics until integration tests failed (cycle 13 AZ-1126 auto-fix).
Source: _docs/06_metrics/retro_2026-06-26_cycle13.md
- [2026-06-26] [tooling] Step 15 perf gate exit 7 on first run when the perf compose stack is not up — preflight with `docker compose -f docker-compose.yml -f docker-compose.perf.yml up -d` before `run-performance-tests.sh` or add a health check to the script (cycle 13).
Source: _docs/06_metrics/retro_2026-06-26_cycle13.md
- [2026-06-25] [testing] PT-07 cold-vs-warm region latency is sensitive to outlier cold p95 on a warm compose volume — the perf gate should drain the region queue before the warm pass and accept warm p50 < cold p50 when p95 is within noise (cycle 10: two marginal PT-07 FAILs before harness fix; AZ-1113 did not touch region paths).
Source: _docs/06_metrics/retro_2026-06-25_cycle10.md
- [2026-06-25] [process] Retrospective security recommendations that name concrete finding IDs (F-AZ795-1/2, F-AZ810-1) and fit ≤2 SP can ship as the sole cycle theme and close multi-cycle carry-overs in one batch — cycle 9 Action #3 → cycle 10 AZ-1113 resolved all three Low A09 findings.
@@ -65,7 +71,3 @@ If the enum's wire string happens to match a member name case-insensitively (e.g
Source: _docs/06_metrics/retro_2026-05-22_cycle7.md
- [2026-05-22] [testing] When a strict-validation layer ships (`JsonSerializerOptions.UnmappedMemberHandling.Disallow`, FluentValidation rules, explicit DTO `[JsonRequired]`), expect the project's own integration tests to surface latent bugs the prior lenient defaults had been masking — silent PascalCase fallback property names, out-of-range fixture coordinates, wrong-cased JSON keys; correct them in the same PR or the test suite goes red and the strict layer looks like a regression instead of the bug-finder it is (cycle 7: `IdempotentPostTests.RoutePoint` had been posting `{"Lat":...}` against a `[JsonPropertyName("lat")]` DTO for months; the new strict deserializer caught it and the 2-line payload fix landed alongside the strict layer; **cycle 8 instance**: `UavUploadTests.NextTestCoordinate` produced lat > 90°, caught by AZ-810 validator, 2-file clamp fix in batch 4).
Source: _docs/06_metrics/retro_2026-05-22_cycle7.md
- [2026-05-22] [architecture] Contract MAJOR bumps for projects with ≤2 known consumers should ship without a wire-format adapter — the cost of maintaining a dual-accepting endpoint outweighs the benefit when the operator runbook can coordinate the consumer cut-over directly; only invest in an adapter when consumer count or release-cadence asymmetry makes coordinated cut-over impractical (cycle 7: `tile-inventory.md` 1.0.0 → 2.0.0 renamed `tileZoom/tileX/tileY → z/x/y` and shipped breaking; the only consumer is `gps-denied-onboard` and the AZ-777 follow-up loop handled the coordination via the operator runbook in `deploy_cycle7.md`).
Source: _docs/06_metrics/retro_2026-05-22_cycle7.md
- [2026-05-12] [tooling] Kestrel `HttpProtocols.Http1AndHttp2` silently serves only HTTP/1.1 over a plaintext listener — ALPN requires TLS, so any "enable HTTP/2" task without TLS in its definition-of-done will downgrade transparently and the only log line is at INFO; tasks that mention HTTP/2 / h2 / multiplexing / ALPN MUST resolve the TLS-vs-h2c choice at spec-write time and the test gate MUST assert `HttpVersion == 2.0` not just a 200 (cycle 6: AZ-505 AC-5 first landed on h2c plaintext, required a post-merge TLS+ALPN pivot with dev-cert plumbing across compose/tests/perf/docs).
Source: _docs/06_metrics/retro_2026-05-12_cycle6.md
+9 -9
View File
@@ -2,23 +2,23 @@
## Current Step
flow: existing-code
step: 11
name: Run Tests
status: not_started
step: 10
name: Implement
status: in_progress
sub_step:
phase: 0
name: awaiting-invocation
phase: 1
name: parse
detail: ""
retry_count: 0
cycle: 13
cycle: 14
tracker: jira
auto_push: true
## Last Completed Cycle
cycle: 12
step_14_security: skipped
cycle: 13
step_14_security: completed
step_15_perf: completed
step_16_deploy: skipped
step_16_deploy: completed
step_16_5_release: skipped
step_17_retrospective: completed
verdict: cycle_complete