Files
Oleksandr Bezdieniezhnykh 5fe67023b2 [AZ-329] [AZ-330] [AZ-523] [AZ-524] Batch 44 atomic refactor
Implements two new C12 services and rebalances the C11/C12 boundary
in one atomic commit:

* AZ-329 PostLandingUploadOrchestrator — gates C11 upload on the
  `flight_footer` FDR record's `clean_shutdown` field; 4 refusal
  modes; new FdrFooterReader Protocol + LocalFdrFooterReader.
* AZ-330 OperatorReLocService — AC-3.4 visual-loss re-localization
  hint; reuses shared LatLonAlt; OperatorCommandTransport Protocol
  cut (E-C8 owns the future pymavlink concrete); new FDR record
  kind `c12.reloc.requested`; log redaction (lat/lon 5 decimals,
  reason 200 chars).
* AZ-523 C11 internal flight-state gate removed (SRP refactor):
  `confirm_flight_state` / `FlightStateSignal` use /
  `FlightStateNotOnGroundError` deleted from C11; TileUploader
  contract bumped to v2.0.0 (frozen) with migration note; AZ-317
  superseded.
* AZ-524 Package rename `c12_operator_tooling` →
  `c12_operator_orchestrator` across source, tests, pyproject,
  CMake, Dockerfile, compose, CI, runtime-root services class
  (`OperatorOrchestratorServices`) + factory function
  (`build_operator_orchestrator`), logger namespaces, config slug,
  docs, and the E-C12 epic title.

Tests: 1543 passed, 80 skipped (all environment gates). Targeted
AC suite (AZ-329 + AZ-330 + FdrFooterReader): 37 passed. Cold-start
NFR-perf still ≤ 500 ms p99.

Tracker: AZ-317 → Done (superseded); AZ-319 v2.0.0 contract bump
comment; AZ-329/AZ-330 → In Testing; AZ-253 epic renamed; AZ-523
+ AZ-524 created and closed as audit-trail tickets.

See `_docs/03_implementation/batch_44_cycle1_report.md`.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 19:42:46 +03:00

143 lines
4.9 KiB
Python

"""C11 protocol conformance — uploader (AZ-319 AC-12) + downloader (AZ-316 AC-10).
Smoke-tests that each concrete impl exposes every method its Protocol
requires (positive cases) and that partial fakes omitting one of the
required methods are correctly rejected (negative cases).
"""
from __future__ import annotations
import logging
import httpx
from gps_denied_onboard.clock.wall_clock import WallClock
from gps_denied_onboard.components.c11_tile_manager import (
C11Config,
C11RetryConfig,
HttpTileDownloader,
HttpTileUploader,
IdempotentRetryTileUploader,
)
from gps_denied_onboard.components.c11_tile_manager.interface import (
TileDownloader,
TileUploader,
)
from gps_denied_onboard.fdr_client.fakes import FakeFdrSink
_PRODUCER_ID = "c11_tile_manager.tile_uploader"
class _NullSleep:
def __call__(self, _seconds: float) -> None:
return None
class _PartialFakeMissingEnumerate:
"""Conformance counterexample: missing ``enumerate_pending_tiles``."""
def upload_pending_tiles(self, request: object) -> object: # noqa: ARG002
return None
class _PartialDownloaderMissingEnumerate:
"""Conformance counterexample: missing ``enumerate_remote_coverage``."""
def download_tiles_for_area(self, request: object) -> object: # noqa: ARG002
return None
def test_ac12_concrete_uploader_satisfies_protocol() -> None:
# Arrange — supply minimal-yet-valid dependencies; the Protocol
# check only inspects method names, not their behaviour.
cfg = C11Config(
satellite_provider_ingest_url="https://parent-suite.test",
upload_batch_size=10,
upload_http_timeout_s=5.0,
upload_max_retry_after_s=600,
companion_id="conformance",
)
transport = httpx.MockTransport(lambda r: httpx.Response(202))
uploader = HttpTileUploader(
http_client=httpx.Client(transport=transport),
tile_store=object(), # type: ignore[arg-type]
tile_metadata_store=object(), # type: ignore[arg-type]
key_manager=object(), # type: ignore[arg-type]
fdr_client=FakeFdrSink(_PRODUCER_ID), # type: ignore[arg-type]
logger=logging.getLogger("test_az319_conformance"),
config=cfg,
sleep=_NullSleep(),
)
# Assert
assert isinstance(uploader, TileUploader)
def test_ac12_partial_fake_is_not_protocol_conformant() -> None:
# Assert
assert not isinstance(_PartialFakeMissingEnumerate(), TileUploader)
def test_ac10_concrete_downloader_satisfies_protocol() -> None:
# Arrange
cfg = C11Config(
satellite_provider_url="https://parent-suite.test",
service_api_key="conformance-test-key",
download_http_timeout_s=5.0,
download_max_5xx_retries=4,
download_max_retry_after_s=600,
download_resolution_floor_m_per_px=0.5,
)
transport = httpx.MockTransport(lambda r: httpx.Response(200, json={"tiles": []}))
downloader = HttpTileDownloader(
http_client=httpx.Client(transport=transport, base_url="https://parent-suite.test"),
tile_writer=object(), # type: ignore[arg-type]
budget_enforcer=object(), # type: ignore[arg-type]
logger=logging.getLogger("test_az316_conformance"),
config=cfg,
sleep=_NullSleep(),
)
# Assert
assert isinstance(downloader, TileDownloader)
def test_ac10_partial_downloader_is_not_protocol_conformant() -> None:
# Assert
assert not isinstance(_PartialDownloaderMissingEnumerate(), TileDownloader)
def test_ac9_idempotent_retry_decorator_satisfies_uploader_protocol() -> None:
# Arrange — wrap a Protocol-conformant inner uploader; the decorator
# must itself satisfy ``TileUploader`` so the composition root can
# bind it transparently in place of ``HttpTileUploader``.
cfg = C11Config(
satellite_provider_ingest_url="https://parent-suite.test",
upload_batch_size=10,
upload_http_timeout_s=5.0,
upload_max_retry_after_s=600,
companion_id="conformance",
)
transport = httpx.MockTransport(lambda r: httpx.Response(202))
inner = HttpTileUploader(
http_client=httpx.Client(transport=transport),
tile_store=object(), # type: ignore[arg-type]
tile_metadata_store=object(), # type: ignore[arg-type]
key_manager=object(), # type: ignore[arg-type]
fdr_client=FakeFdrSink(_PRODUCER_ID), # type: ignore[arg-type]
logger=logging.getLogger("test_az320_inner"),
config=cfg,
sleep=_NullSleep(),
)
decorator = IdempotentRetryTileUploader(
inner=inner,
tile_metadata_store=object(), # type: ignore[arg-type]
fdr_client=FakeFdrSink("c11_tile_manager.idempotent_retry"), # type: ignore[arg-type]
logger=logging.getLogger("test_az320_decorator"),
clock=WallClock(),
config=C11RetryConfig(),
)
# Assert
assert isinstance(decorator, TileUploader)