From 80ef5608f1f0fb174efa01d932682d01baa7412b Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Fri, 26 Jun 2026 16:13:37 +0300 Subject: [PATCH] chore: WIP pre-implement cycle 14 baseline Co-authored-by: Cursor --- .woodpecker/01-test.yml | 3 + .woodpecker/02-build-push.yml | 9 +- AGENTS.md | 18 +++- SatelliteProvider.Api/Dockerfile | 4 + SatelliteProvider.IntegrationTests/Dockerfile | 8 +- _docs/02_document/00_discovery.md | 7 +- .../03_tile_downloader/description.md | 2 +- .../02_document/deployment/ci_cd_pipeline.md | 47 +++++++--- .../deployment/containerization.md | 3 + _docs/02_document/modules/api_program.md | 4 +- _docs/02_document/modules/common_dtos.md | 2 +- .../02_document/modules/tests_integration.md | 4 +- _docs/02_document/modules/tests_unit.md | 3 +- _docs/02_document/ripple_log_cycle13.md | 13 +++ _docs/02_document/tests/blackbox-tests.md | 19 ++++ _docs/02_document/tests/environment.md | 23 +++++ .../02_document/tests/traceability-matrix.md | 7 +- _docs/02_tasks/_dependencies_table.md | 7 ++ ...1131_environment_md_integration_command.md | 86 +++++++++++++++++++ _docs/03_implementation/deploy_cycle13.md | 72 ++++++++++++++++ _docs/05_security/dependency_scan_cycle13.md | 36 ++++++++ .../infrastructure_review_cycle13.md | 13 +++ _docs/05_security/owasp_review_cycle13.md | 28 ++++++ _docs/05_security/security_report_cycle13.md | 49 +++++++++++ _docs/05_security/static_analysis_cycle13.md | 37 ++++++++ _docs/06_metrics/perf_2026-06-26_cycle13.md | 37 ++++++++ _docs/06_metrics/retro_2026-06-26_cycle13.md | 48 +++++++++++ .../structure_2026-06-26_cycle13.md | 28 ++++++ _docs/LESSONS.md | 10 ++- _docs/_autodev_state.md | 18 ++-- docker-compose.tests.yml | 7 +- docker-compose.yml | 1 - scripts/run-tests.sh | 13 +++ 33 files changed, 619 insertions(+), 47 deletions(-) create mode 100644 _docs/02_document/ripple_log_cycle13.md create mode 100644 _docs/02_tasks/todo/AZ-1131_environment_md_integration_command.md create mode 100644 _docs/03_implementation/deploy_cycle13.md create mode 100644 _docs/05_security/dependency_scan_cycle13.md create mode 100644 _docs/05_security/infrastructure_review_cycle13.md create mode 100644 _docs/05_security/owasp_review_cycle13.md create mode 100644 _docs/05_security/security_report_cycle13.md create mode 100644 _docs/05_security/static_analysis_cycle13.md create mode 100644 _docs/06_metrics/perf_2026-06-26_cycle13.md create mode 100644 _docs/06_metrics/retro_2026-06-26_cycle13.md create mode 100644 _docs/06_metrics/structure_2026-06-26_cycle13.md diff --git a/.woodpecker/01-test.yml b/.woodpecker/01-test.yml index 56cb259..64e389e 100644 --- a/.woodpecker/01-test.yml +++ b/.woodpecker/01-test.yml @@ -2,6 +2,9 @@ when: event: [push, pull_request, manual] branch: [dev, stage, main] +# Unit tests are architecture-neutral — run once on the colocated arm64 agent +# (suite/_infra/ci/README.md: unit lanes stay arm64-only to avoid burning the +# remote amd64 agent). Build-push fans out to arm64 + amd64 via matrix. labels: platform: arm64 diff --git a/.woodpecker/02-build-push.yml b/.woodpecker/02-build-push.yml index b11e377..7bacd34 100644 --- a/.woodpecker/02-build-push.yml +++ b/.woodpecker/02-build-push.yml @@ -5,14 +5,15 @@ when: depends_on: - 01-test -# Multi-arch matrix. Adding amd64 = uncommenting the second entry once an -# amd64 agent is online. +# Multi-arch matrix — matches suite/_infra/ci/README.md § Build-push step. +# arm64 agent (colocated Jetson) → {branch}-arm +# amd64 agent (remote host) → {branch}-amd64 (production deploy tag) matrix: include: - PLATFORM: arm64 TAG_SUFFIX: arm - # - PLATFORM: amd64 - # TAG_SUFFIX: amd + - PLATFORM: amd64 + TAG_SUFFIX: amd64 labels: platform: ${PLATFORM} diff --git a/AGENTS.md b/AGENTS.md index 488cf95..560d108 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -156,11 +156,27 @@ Configuration sections in appsettings.json: docker-compose up --build ``` -**Run tests:** +**Run tests** (Apple Silicon Mac — native `linux/arm64`, no Rosetta): +```bash +./scripts/run-tests.sh +``` + +**Run tests** (Docker Compose with tests): ```bash docker-compose -f docker-compose.yml -f docker-compose.tests.yml up --build --abort-on-container-exit ``` +### CI / Deploy platforms + +| Context | Architecture | Image tag | +|---------|--------------|-----------| +| Woodpecker unit tests | `linux/arm64` agent | — | +| Woodpecker build (arm64 agent) | `linux/arm64` | `{branch}-arm` | +| Woodpecker build (amd64 agent) | `linux/amd64` | `{branch}-amd64` | +| Production deploy | amd64 host | `${BRANCH}-amd64` (Watchtower) | + +See `_docs/02_document/deployment/ci_cd_pipeline.md` and suite `_infra/ci/README.md`. + ### Important Notes 1. **DO NOT run `dotnet build` or `dotnet restore` via terminal tools** - these commands hang. Ask user to run manually. diff --git a/SatelliteProvider.Api/Dockerfile b/SatelliteProvider.Api/Dockerfile index f695e86..f1f7239 100644 --- a/SatelliteProvider.Api/Dockerfile +++ b/SatelliteProvider.Api/Dockerfile @@ -5,6 +5,10 @@ EXPOSE 8081 FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends protobuf-compiler \ + && rm -rf /var/lib/apt/lists/* +ENV PROTOBUF_PROTOC=/usr/bin/protoc COPY ["SatelliteProvider.Api/SatelliteProvider.Api.csproj", "SatelliteProvider.Api/"] COPY ["SatelliteProvider.GrpcContracts/SatelliteProvider.GrpcContracts.csproj", "SatelliteProvider.GrpcContracts/"] COPY ["SatelliteProvider.Common/SatelliteProvider.Common.csproj", "SatelliteProvider.Common/"] diff --git a/SatelliteProvider.IntegrationTests/Dockerfile b/SatelliteProvider.IntegrationTests/Dockerfile index 3cdc684..a370a3b 100644 --- a/SatelliteProvider.IntegrationTests/Dockerfile +++ b/SatelliteProvider.IntegrationTests/Dockerfile @@ -1,5 +1,9 @@ -FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:10.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends protobuf-compiler \ + && rm -rf /var/lib/apt/lists/* +ENV PROTOBUF_PROTOC=/usr/bin/protoc COPY ["SatelliteProvider.IntegrationTests/SatelliteProvider.IntegrationTests.csproj", "SatelliteProvider.IntegrationTests/"] COPY ["SatelliteProvider.GrpcContracts/SatelliteProvider.GrpcContracts.csproj", "SatelliteProvider.GrpcContracts/"] COPY ["SatelliteProvider.TestSupport/SatelliteProvider.TestSupport.csproj", "SatelliteProvider.TestSupport/"] @@ -12,7 +16,7 @@ RUN dotnet build "SatelliteProvider.IntegrationTests.csproj" -c Release -o /app/ FROM build AS publish RUN dotnet publish "SatelliteProvider.IntegrationTests.csproj" -c Release -o /app/publish /p:UseAppHost=false -FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/aspnet:10.0 AS final +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "SatelliteProvider.IntegrationTests.dll"] diff --git a/_docs/02_document/00_discovery.md b/_docs/02_document/00_discovery.md index 21a64e3..43b307d 100644 --- a/_docs/02_document/00_discovery.md +++ b/_docs/02_document/00_discovery.md @@ -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 diff --git a/_docs/02_document/components/03_tile_downloader/description.md b/_docs/02_document/components/03_tile_downloader/description.md index 5e8355d..e14dc7f 100644 --- a/_docs/02_document/components/03_tile_downloader/description.md +++ b/_docs/02_document/components/03_tile_downloader/description.md @@ -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 | diff --git a/_docs/02_document/deployment/ci_cd_pipeline.md b/_docs/02_document/deployment/ci_cd_pipeline.md index d8e4634..c2434f0 100644 --- a/_docs/02_document/deployment/ci_cd_pipeline.md +++ b/_docs/02_document/deployment/ci_cd_pipeline.md @@ -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`) | diff --git a/_docs/02_document/deployment/containerization.md b/_docs/02_document/deployment/containerization.md index 2833a4f..8ec43cd 100644 --- a/_docs/02_document/deployment/containerization.md +++ b/_docs/02_document/deployment/containerization.md @@ -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) diff --git a/_docs/02_document/modules/api_program.md b/_docs/02_document/modules/api_program.md index ff800e4..b8c66b1 100644 --- a/_docs/02_document/modules/api_program.md +++ b/_docs/02_document/modules/api_program.md @@ -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()`, 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()` 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()`: 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 diff --git a/_docs/02_document/modules/common_dtos.md b/_docs/02_document/modules/common_dtos.md index 2dc0c62..0fcd43a 100644 --- a/_docs/02_document/modules/common_dtos.md +++ b/_docs/02_document/modules/common_dtos.md @@ -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) diff --git a/_docs/02_document/modules/tests_integration.md b/_docs/02_document/modules/tests_integration.md index e4e6df0..0357414 100644 --- a/_docs/02_document/modules/tests_integration.md +++ b/_docs/02_document/modules/tests_integration.md @@ -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` + `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` / `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` / `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 diff --git a/_docs/02_document/modules/tests_unit.md b/_docs/02_document/modules/tests_unit.md index 9b499d8..4e52b56 100644 --- a/_docs/02_document/modules/tests_unit.md +++ b/_docs/02_document/modules/tests_unit.md @@ -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) diff --git a/_docs/02_document/ripple_log_cycle13.md b/_docs/02_document/ripple_log_cycle13.md new file mode 100644 index 0000000..71ed4f1 --- /dev/null +++ b/_docs/02_document/ripple_log_cycle13.md @@ -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). diff --git a/_docs/02_document/tests/blackbox-tests.md b/_docs/02_document/tests/blackbox-tests.md index ca93b66..e3f8da7 100644 --- a/_docs/02_document/tests/blackbox-tests.md +++ b/_docs/02_document/tests/blackbox-tests.md @@ -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. + diff --git a/_docs/02_document/tests/environment.md b/_docs/02_document/tests/environment.md index c2b9f23..96e3232 100644 --- a/_docs/02_document/tests/environment.md +++ b/_docs/02_document/tests/environment.md @@ -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 | diff --git a/_docs/02_document/tests/traceability-matrix.md b/_docs/02_document/tests/traceability-matrix.md index 396319d..14fe198 100644 --- a/_docs/02_document/tests/traceability-matrix.md +++ b/_docs/02_document/tests/traceability-matrix.md @@ -164,6 +164,10 @@ | AZ-812 AC-4 | `curl` probe with `{"id":"","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. diff --git a/_docs/02_tasks/_dependencies_table.md b/_docs/02_tasks/_dependencies_table.md index dfbff06..d4115ea 100644 --- a/_docs/02_tasks/_dependencies_table.md +++ b/_docs/02_tasks/_dependencies_table.md @@ -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 9–11 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) diff --git a/_docs/02_tasks/todo/AZ-1131_environment_md_integration_command.md b/_docs/02_tasks/todo/AZ-1131_environment_md_integration_command.md new file mode 100644 index 0000000..982ef46 --- /dev/null +++ b/_docs/02_tasks/todo/AZ-1131_environment_md_integration_command.md @@ -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) diff --git a/_docs/03_implementation/deploy_cycle13.md b/_docs/03_implementation/deploy_cycle13.md new file mode 100644 index 0000000..5234960 --- /dev/null +++ b/_docs/03_implementation/deploy_cycle13.md @@ -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. diff --git a/_docs/05_security/dependency_scan_cycle13.md b/_docs/05_security/dependency_scan_cycle13.md new file mode 100644 index 0000000..b1e5304 --- /dev/null +++ b/_docs/05_security/dependency_scan_cycle13.md @@ -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. diff --git a/_docs/05_security/infrastructure_review_cycle13.md b/_docs/05_security/infrastructure_review_cycle13.md new file mode 100644 index 0000000..adfecab --- /dev/null +++ b/_docs/05_security/infrastructure_review_cycle13.md @@ -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. diff --git a/_docs/05_security/owasp_review_cycle13.md b/_docs/05_security/owasp_review_cycle13.md new file mode 100644 index 0000000..5feb51d --- /dev/null +++ b/_docs/05_security/owasp_review_cycle13.md @@ -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). diff --git a/_docs/05_security/security_report_cycle13.md b/_docs/05_security/security_report_cycle13.md new file mode 100644 index 0000000..849802b --- /dev/null +++ b/_docs/05_security/security_report_cycle13.md @@ -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` diff --git a/_docs/05_security/static_analysis_cycle13.md b/_docs/05_security/static_analysis_cycle13.md new file mode 100644 index 0000000..9e9e6e3 --- /dev/null +++ b/_docs/05_security/static_analysis_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. diff --git a/_docs/06_metrics/perf_2026-06-26_cycle13.md b/_docs/06_metrics/perf_2026-06-26_cycle13.md new file mode 100644 index 0000000..caefb25 --- /dev/null +++ b/_docs/06_metrics/perf_2026-06-26_cycle13.md @@ -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 2–20 warm cached. diff --git a/_docs/06_metrics/retro_2026-06-26_cycle13.md b/_docs/06_metrics/retro_2026-06-26_cycle13.md new file mode 100644 index 0000000..6cb91cf --- /dev/null +++ b/_docs/06_metrics/retro_2026-06-26_cycle13.md @@ -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. diff --git a/_docs/06_metrics/structure_2026-06-26_cycle13.md b/_docs/06_metrics/structure_2026-06-26_cycle13.md new file mode 100644 index 0000000..3c0d139 --- /dev/null +++ b/_docs/06_metrics/structure_2026-06-26_cycle13.md @@ -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. diff --git a/_docs/LESSONS.md b/_docs/LESSONS.md index 97bb6ed..f2a4e71 100644 --- a/_docs/LESSONS.md +++ b/_docs/LESSONS.md @@ -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 diff --git a/_docs/_autodev_state.md b/_docs/_autodev_state.md index b431a6d..10edc32 100644 --- a/_docs/_autodev_state.md +++ b/_docs/_autodev_state.md @@ -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 diff --git a/docker-compose.tests.yml b/docker-compose.tests.yml index 550a017..803fd25 100644 --- a/docker-compose.tests.yml +++ b/docker-compose.tests.yml @@ -1,3 +1,9 @@ +# Integration test stack. +# Platform: native host arch — linux/arm64 on Apple Silicon Mac (M1+), linux/amd64 +# on an amd64 Linux host. No platform: pin (Mac must not force linux/amd64 / Rosetta). +# CI: unit tests arm64-only (.woodpecker/01-test.yml); image builds arm64 + amd64 +# (.woodpecker/02-build-push.yml matrix). See tests/environment.md § Platform. + services: postgres: image: postgres:16 @@ -13,7 +19,6 @@ services: retries: 5 api: - platform: linux/amd64 build: context: . dockerfile: SatelliteProvider.Api/Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml index 8783d19..bc1284c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,6 @@ services: retries: 5 api: - platform: linux/amd64 build: context: . dockerfile: SatelliteProvider.Api/Dockerfile diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 4d690ac..814d29d 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -4,6 +4,12 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +case "$(uname -m)" in + arm64|aarch64) + export DOCKER_DEFAULT_PLATFORM=linux/arm64 + ;; +esac + cleanup() { docker compose -f "$PROJECT_ROOT/docker-compose.tests.yml" down --remove-orphans || true } @@ -80,6 +86,13 @@ Environment: the API container validates. May be a DEV-ONLY value for local runs. JWT_AUDIENCE Required for any integration test mode (AZ-494). Same contract as JWT_ISSUER — must match what the API container validates. + +Platform: + Mac M1+ (darwin/arm64): scripts/run-tests.sh sets DOCKER_DEFAULT_PLATFORM=linux/arm64. + Do not pin platform: linux/amd64 locally — forces Rosetta/QEMU emulation. + Woodpecker CI: unit tests on arm64 agent only; image builds fan out to arm64 + ({branch}-arm) and amd64 ({branch}-amd64) agents. Production deploy pulls + *-amd64 (suite/_infra/deploy/satellite-provider/). EOF }