[AZ-838] SatelliteProviderRouteClient + seed_route.py CLI (E-AZ-835 C2)
ci/woodpecker/push/02-build-push Pipeline failed

Operator-side HTTP client + CLI that takes a RouteSpec from AZ-836
and onboards it via satellite-provider's POST /api/satellite/route:
pre-emptive AZ-809 validation, request submission, polling until
mapsReady, and POST /api/satellite/tiles/inventory verify.

Lives in c11_tile_manager (shared parent-suite HTTP/JWT plumbing,
shared BUILD_C11_TILE_MANAGER gate); error hierarchy split off
SatelliteProviderRouteError to keep the tile path and route path
independent. 30 unit tests + 1 RUN_E2E-gated integration test.

Pre-emptive validator tracks the actual AZ-809 server bounds
(points [2,500], zoom [0,22]) instead of the AZ-838 spec's narrower
client-only bounds; flagged as F1 in batch_107_cycle3_report.md
for user decision (accept-and-update-spec / revert-to-spec).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-23 13:29:45 +03:00
parent c7cd9b414d
commit c3a1ebc754
12 changed files with 2389 additions and 80 deletions
@@ -1,106 +0,0 @@
# SatelliteProviderRouteClient + seed_route.py CLI
**Task**: AZ-838_satellite_provider_route_client
**Name**: SatelliteProviderRouteClient + seed_route.py CLI: POST tlog-derived route to satellite-provider (AZ-835 C2)
**Description**: Second building block of Epic AZ-835. Consumer-side HTTP client + CLI wrapper that takes a `RouteSpec` (from AZ-836 / C1) and registers it with satellite-provider's `POST /api/satellite/route` endpoint, polls until `mapsReady=true`, and returns the inventory size for downstream consumption.
**Complexity**: 3 SP
**Dependencies**: AZ-836 (C1, RouteSpec dataclass + extractor — hard code dep); AZ-777 Phase 1 (existing satellite-provider HTTP plumbing patterns + JWT handling — done); AZ-809 (Route API validation — SOFT prereq, client pre-emptively validates so it's correct without it); AZ-835 (parent Epic)
**Component**: new `src/gps_denied_onboard/satellite_provider/route_client.py` + new CLI `tests/fixtures/derkachi_c6/seed_route.py`
**Tracker**: AZ-838 (https://denyspopov.atlassian.net/browse/AZ-838)
**Parent Epic**: AZ-835
Jira AZ-838 is the authoritative spec; this file is the in-workspace mirror.
## Public surface
```python
import uuid
from dataclasses import dataclass
from gps_denied_onboard.replay_input.tlog_route import RouteSpec # AZ-836
@dataclass(frozen=True, slots=True)
class RouteSeedResult:
route_id: uuid.UUID
terminal_status: str
maps_ready: bool
tile_count: int
elapsed_ms: int
submitted_payload_sha256: str
class SatelliteProviderRouteError(Exception): ...
class RouteValidationError(SatelliteProviderRouteError): ... # 4xx + ProblemDetails
class RouteTransientError(SatelliteProviderRouteError): ... # 5xx / network / timeout
class RouteTerminalFailureError(SatelliteProviderRouteError): ... # mapsReady never reached
class SatelliteProviderRouteClient:
def __init__(self, base_url: str, jwt: str, *, tls_insecure: bool = False,
request_timeout_s: float = 30.0, poll_interval_s: float = 5.0,
poll_max_attempts: int = 60): ...
def seed_route(self, spec: RouteSpec, *, name: str | None = None) -> RouteSeedResult: ...
```
## Wire shape
No formal Route API contract doc exists in `../satellite-provider/_docs/02_document/contracts/api/` as of 2026-05-22. DTOs are the source of truth:
- `../satellite-provider/SatelliteProvider.Common/DTO/CreateRouteRequest.cs` (top-level)
- `../satellite-provider/SatelliteProvider.Common/DTO/RoutePoint.cs` (`[JsonPropertyName("lat")] Latitude`, `[JsonPropertyName("lon")] Longitude` — input/output naming asymmetry flagged in AZ-809 AC-10; consume `lat`/`lon` in the JSON)
- `../satellite-provider/SatelliteProvider.Common/DTO/GeoPoint.cs` (nested geofence point)
Probe 2026-05-22: 2-point route + `requestMaps=true` completes end-to-end in ~15 s.
## Behaviour
- **Pre-emptive validation** against AZ-809 rules — surface as `RouteValidationError` BEFORE HTTP POST:
- `points` non-empty AND `len(points) <= 100`
- `id` non-zero Guid
- `regionSizeMeters > 0` AND `<= 10000`
- `zoomLevel` in 15..18 (per AZ-777 Phase 2 bbox config)
- Each point's `lat` in -90..90, `lon` in -180..180
- **Submit** `POST /api/satellite/route` with `requestMaps=true`, `createTilesZip=false`.
- **Poll** `GET /api/satellite/route/{id}` every `poll_interval_s` up to `poll_max_attempts` until `mapsReady=true` OR terminal failure. Log cadence at INFO.
- **Return** `RouteSeedResult`; `tile_count` from a final `POST /api/satellite/tiles/inventory` enumerating the route's tile coverage (computed locally from waypoints + `regionSizeMeters`).
- **Raise** `RouteTerminalFailureError` on terminal failure (`.detail` = SP response JSON).
- **Raise** `RouteTransientError` on 5xx / network / timeout (`__cause__` = underlying `httpx` exception).
- **Raise** `RouteValidationError` on 4xx; parse RFC 7807 `errors` dict into `field_errors`.
## CLI (`tests/fixtures/derkachi_c6/seed_route.py`)
Mirrors `seed_region.py` (AZ-777 Phase 2):
- Env: `SATELLITE_PROVIDER_URL`, `SATELLITE_PROVIDER_API_KEY`, `SATELLITE_PROVIDER_TLS_INSECURE`, optional `--auto-mint-jwt` (uses `scripts/mint_dev_jwt.py`)
- Required: `--tlog <path>` (delegates to AZ-836's `extract_route_from_tlog`)
- Optional: `--max-waypoints` (10), `--region-size-meters` (500), `--name`, `--output-summary <path>`, `--dry-run`
- Exit codes: 0 success, 71 config malformed, 72 missing env, 73 SP unreachable, 74 4xx, 75 5xx / terminal failure, 76 inventory verification mismatch
## Acceptance criteria
| # | Criterion |
|---|-----------|
| AC-1 | POSTs wire shape exactly per `CreateRouteRequest.cs` + `RoutePoint.cs` + `GeoPoint.cs` |
| AC-2 | Polls `GET /api/satellite/route/{id}` until `mapsReady=true` OR terminal failure; respects `poll_max_attempts` + `poll_interval_s` |
| AC-3 | 4xx + RFC 7807 ProblemDetails → `RouteValidationError`; `field_errors` populated from `errors` dict |
| AC-4 | 5xx / network / timeout → `RouteTransientError`; `__cause__` = underlying `httpx` exc |
| AC-5 | Terminal failure → `RouteTerminalFailureError`; `.detail` = SP response JSON |
| AC-6 | Pre-emptive validation rejects (BEFORE HTTP POST): empty `points`, >100 `points`, missing/zero `id`, missing/zero `regionSizeMeters`, OOR `zoomLevel`, OOR lat/lon |
| AC-7 | `seed_route.py --dry-run --tlog <derkachi.tlog>`: extracts route, prints planned payload + sha256, exit 0, no HTTP |
| AC-8 | `seed_route.py --tlog <derkachi.tlog>` against Jetson SP: exit 0, prints `RouteSeedResult`, optional summary JSON |
| AC-9 | Unit tests (mocked HTTPX): happy path, 400+ProblemDetails, 500 transient, terminal failure, timeout, dry-run, missing env, all pre-emptive validation cases |
| AC-10 | Integration test gated by `RUN_E2E=1` + `SATELLITE_PROVIDER_URL`: Derkachi route seeded, `tile_count > 0`, `maps_ready=True` |
## Out of scope
- FAISS index from seeded tiles (AZ-835 C3 / C5)
- C6 cache population (AZ-835 C3 — new `operator_pre_flight_setup` fixture)
- Modifying satellite-provider source (Route API consumed as-is)
- Multi-route batching (one RouteSpec → one POST)
- Authentication beyond existing JWT pattern (AZ-494)
## References
- Parent Epic: AZ-835 — https://denyspopov.atlassian.net/browse/AZ-835
- Sibling: AZ-836 (C1) — RouteSpec source
- Mirror CLI: `tests/fixtures/derkachi_c6/seed_region.py` (AZ-777 Phase 2)
- HTTP patterns: `src/gps_denied_onboard/components/c11_tile_manager/tile_downloader.py` (AZ-316/777 Phase 1)
- DTOs (in `../satellite-provider/`): `SatelliteProvider.Common/DTO/{CreateRouteRequest,RoutePoint,GeoPoint}.cs`
- Soft prereq: AZ-809 (Route API validation in satellite-provider)