mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 18:51:15 +00:00
[AZ-777] Phase 1 hotfix (z/x/y) + Phase 2 Derkachi seed + ops
Phase 1 hotfix:
- C11 HttpTileDownloader adapted to satellite-provider v2.0.0
z/x/y inventory contract (bulk POST keyed by slippy-map coords).
- Unit tests rewritten to exercise the new inventory schema.
- E2E smoke test updated to match the v2.0.0 wire.
Phase 2 (Derkachi seed + smoke-validated on Jetson):
- tests/fixtures/derkachi_c6/{README,bbox.yaml,seed_region.py}
drives POST /api/satellite/region against satellite-provider
with Google Maps as the imagery source. Smoke run produced
4 regions, 175 tiles, inventory 32/32.
- scripts/mint_dev_jwt.py + run-tests-jetson.sh auto-mint and
export SATELLITE_PROVIDER_API_KEY using JWT_SECRET / JWT_ISSUER
/ JWT_AUDIENCE env vars (no host port mappings; e2e-runner
reaches SP via internal docker network only).
Spec amendment: AZ-777 todo spec updated to record the
Google Maps imagery source decision and STOP-gate state.
AZ-777 Phase 3+ work is superseded by Epic AZ-835 (see next
commit).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -36,7 +36,6 @@ from gps_denied_onboard.components.c11_tile_manager import (
|
||||
request_hash,
|
||||
)
|
||||
|
||||
|
||||
_BASE_URL = "https://parent-suite.test"
|
||||
_INVENTORY_PATH = "/api/satellite/tiles/inventory"
|
||||
_TILES_PATH_PREFIX = "/tiles/"
|
||||
@@ -111,14 +110,10 @@ class _StubTileWriter:
|
||||
}
|
||||
)
|
||||
if call_index in self.rejected_indices:
|
||||
raise _StubFreshnessRejection(
|
||||
f"freshness rejected call_index={call_index}"
|
||||
)
|
||||
raise _StubFreshnessRejection(f"freshness rejected call_index={call_index}")
|
||||
return self.labels_by_index.get(call_index, "fresh")
|
||||
|
||||
def tile_already_present(
|
||||
self, *, zoom_level: int, lat: float, lon: float
|
||||
) -> bool:
|
||||
def tile_already_present(self, *, zoom_level: int, lat: float, lon: float) -> bool:
|
||||
self.exists_calls.append((zoom_level, lat, lon))
|
||||
return False
|
||||
|
||||
@@ -238,9 +233,9 @@ def _inventory_entry_for_coord(
|
||||
) -> dict[str, Any]:
|
||||
if not present:
|
||||
return {
|
||||
"tileZoom": int(zoom),
|
||||
"tileX": int(x),
|
||||
"tileY": int(y),
|
||||
"z": int(zoom),
|
||||
"x": int(x),
|
||||
"y": int(y),
|
||||
"locationHash": str(uuid4()),
|
||||
"present": False,
|
||||
"id": None,
|
||||
@@ -251,9 +246,9 @@ def _inventory_entry_for_coord(
|
||||
}
|
||||
captured = captured_at or datetime(2026, 5, 13, 0, 0, 0, tzinfo=timezone.utc)
|
||||
return {
|
||||
"tileZoom": int(zoom),
|
||||
"tileX": int(x),
|
||||
"tileY": int(y),
|
||||
"z": int(zoom),
|
||||
"x": int(x),
|
||||
"y": int(y),
|
||||
"locationHash": str(uuid4()),
|
||||
"present": True,
|
||||
"id": str(uuid4()),
|
||||
@@ -303,9 +298,9 @@ def _make_inventory_handler(
|
||||
resolution = 0.5
|
||||
results.append(
|
||||
_inventory_entry_for_coord(
|
||||
zoom=int(t["tileZoom"]),
|
||||
x=int(t["tileX"]),
|
||||
y=int(t["tileY"]),
|
||||
zoom=int(t["z"]),
|
||||
x=int(t["x"]),
|
||||
y=int(t["y"]),
|
||||
present=is_present,
|
||||
resolution_m_per_px=resolution,
|
||||
)
|
||||
@@ -331,12 +326,8 @@ def _make_inventory_handler(
|
||||
def test_ac1_100_tile_happy_path_writes_all(tmp_path: Path) -> None:
|
||||
# Arrange — bbox at zoom 14 produces N coord candidates; the
|
||||
# stub marks the first 100 as `present=true` and the rest absent.
|
||||
transport = httpx.MockTransport(
|
||||
_make_inventory_handler(present_count=100)
|
||||
)
|
||||
(downloader, _logs, writer, enforcer, _sleeps) = _build_downloader(
|
||||
transport=transport
|
||||
)
|
||||
transport = httpx.MockTransport(_make_inventory_handler(present_count=100))
|
||||
(downloader, _logs, writer, enforcer, _sleeps) = _build_downloader(transport=transport)
|
||||
request = _make_request(cache_root=tmp_path)
|
||||
|
||||
# Act
|
||||
@@ -366,9 +357,7 @@ def test_ac2_resolution_gate_rejects_sub_spec_tiles(tmp_path: Path) -> None:
|
||||
resolution_override_for_first_n=(10, 0.3),
|
||||
)
|
||||
)
|
||||
(downloader, log_records, writer, _enforcer, _sleeps) = _build_downloader(
|
||||
transport=transport
|
||||
)
|
||||
(downloader, log_records, writer, _enforcer, _sleeps) = _build_downloader(transport=transport)
|
||||
|
||||
# Act
|
||||
report = downloader.download_tiles_for_area(_make_request(cache_root=tmp_path))
|
||||
@@ -377,7 +366,9 @@ def test_ac2_resolution_gate_rejects_sub_spec_tiles(tmp_path: Path) -> None:
|
||||
assert report.tiles_rejected_resolution == 10
|
||||
assert report.tiles_downloaded == 40
|
||||
assert len(writer.write_calls) == 40
|
||||
res_warnings = [r for r in log_records if getattr(r, "kind", "") == "c11.download.resolution_rejected"]
|
||||
res_warnings = [
|
||||
r for r in log_records if getattr(r, "kind", "") == "c11.download.resolution_rejected"
|
||||
]
|
||||
assert len(res_warnings) == 10
|
||||
|
||||
|
||||
@@ -402,7 +393,9 @@ def test_ac3_freshness_rejections_counted_and_run_continues(tmp_path: Path) -> N
|
||||
assert report.tiles_downloaded == 5
|
||||
assert report.outcome == DownloadOutcome.SUCCESS
|
||||
summary_warns = [
|
||||
r for r in log_records if getattr(r, "kind", "") == "c11.download.freshness_rejected_summary"
|
||||
r
|
||||
for r in log_records
|
||||
if getattr(r, "kind", "") == "c11.download.freshness_rejected_summary"
|
||||
]
|
||||
assert len(summary_warns) == 1
|
||||
|
||||
@@ -483,9 +476,7 @@ def test_ac6_persistent_5xx_raises_satellite_provider_error(tmp_path: Path) -> N
|
||||
tile_response_factory=_factory,
|
||||
)
|
||||
)
|
||||
(downloader, _logs, _writer, _enforcer, _sleeps) = _build_downloader(
|
||||
transport=transport
|
||||
)
|
||||
(downloader, _logs, _writer, _enforcer, _sleeps) = _build_downloader(transport=transport)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(SatelliteProviderError):
|
||||
@@ -512,9 +503,7 @@ def test_ac7_401_fails_fast_no_retry(tmp_path: Path) -> None:
|
||||
tile_response_factory=_factory,
|
||||
)
|
||||
)
|
||||
(downloader, log_records, _writer, _enforcer, _sleeps) = _build_downloader(
|
||||
transport=transport
|
||||
)
|
||||
(downloader, _log_records, _writer, _enforcer, _sleeps) = _build_downloader(transport=transport)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(SatelliteProviderError):
|
||||
@@ -541,9 +530,7 @@ def test_ac8_idempotent_rerun_yields_no_op(tmp_path: Path) -> None:
|
||||
tile_response_factory=_factory,
|
||||
)
|
||||
)
|
||||
(downloader, _logs, _writer, _enforcer, _sleeps) = _build_downloader(
|
||||
transport=transport
|
||||
)
|
||||
(downloader, _logs, _writer, _enforcer, _sleeps) = _build_downloader(transport=transport)
|
||||
request = _make_request(cache_root=tmp_path)
|
||||
|
||||
# Act — first run
|
||||
@@ -578,10 +565,8 @@ def test_ac9_cache_budget_pre_check_aborts(tmp_path: Path) -> None:
|
||||
tile_response_factory=_factory,
|
||||
)
|
||||
)
|
||||
enforcer = _StubBudgetEnforcer(
|
||||
raise_on_call=CacheBudgetExceededError("no headroom")
|
||||
)
|
||||
(downloader, log_records, _writer, _enforcer, _sleeps) = _build_downloader(
|
||||
enforcer = _StubBudgetEnforcer(raise_on_call=CacheBudgetExceededError("no headroom"))
|
||||
(downloader, _log_records, _writer, _enforcer, _sleeps) = _build_downloader(
|
||||
transport=transport, budget_enforcer=enforcer
|
||||
)
|
||||
|
||||
@@ -606,16 +591,12 @@ def test_ac11_service_api_key_never_appears_in_logs(tmp_path: Path) -> None:
|
||||
inventory_response_override=httpx.Response(401),
|
||||
)
|
||||
)
|
||||
(downloader, log_records, _writer, _enforcer, _sleeps) = _build_downloader(
|
||||
transport=transport
|
||||
)
|
||||
(downloader, log_records, _writer, _enforcer, _sleeps) = _build_downloader(transport=transport)
|
||||
with pytest.raises(SatelliteProviderError):
|
||||
downloader.download_tiles_for_area(_make_request(cache_root=tmp_path))
|
||||
|
||||
# Assert
|
||||
flat = " ".join(
|
||||
r.getMessage() + json.dumps(getattr(r, "kv", {})) for r in log_records
|
||||
)
|
||||
flat = " ".join(r.getMessage() + json.dumps(getattr(r, "kv", {})) for r in log_records)
|
||||
assert _API_KEY not in flat
|
||||
assert "Bearer ***" in flat
|
||||
|
||||
@@ -642,9 +623,7 @@ def test_ac12_partial_journal_resumed_on_rerun(tmp_path: Path) -> None:
|
||||
tile_response_factory=_factory,
|
||||
)
|
||||
)
|
||||
(downloader, _logs, writer, _enforcer, _sleeps) = _build_downloader(
|
||||
transport=transport
|
||||
)
|
||||
(downloader, _logs, writer, _enforcer, _sleeps) = _build_downloader(transport=transport)
|
||||
request = _make_request(cache_root=tmp_path)
|
||||
|
||||
# First run — completes
|
||||
@@ -771,14 +750,10 @@ def test_nfr_throughput_1000_tiles_under_budget(tmp_path: Path) -> None:
|
||||
transport = httpx.MockTransport(
|
||||
_make_inventory_handler(
|
||||
present_count=1000,
|
||||
tile_response_factory=lambda r: httpx.Response(
|
||||
200, content=b"\xff\xd8tile"
|
||||
),
|
||||
tile_response_factory=lambda r: httpx.Response(200, content=b"\xff\xd8tile"),
|
||||
)
|
||||
)
|
||||
(downloader, _logs, _writer, _enforcer, _sleeps) = _build_downloader(
|
||||
transport=transport
|
||||
)
|
||||
(downloader, _logs, _writer, _enforcer, _sleeps) = _build_downloader(transport=transport)
|
||||
|
||||
# Big bbox at zoom 14: ~ 32x32 tile span on this latitude is enough.
|
||||
# 1° ≈ 45 tiles at zoom 14 in latitude → 0.75° gives ≈ 33 tiles → ~1089 tiles.
|
||||
|
||||
Reference in New Issue
Block a user