[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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-13 19:42:46 +03:00
parent 2d88d3d674
commit 5fe67023b2
112 changed files with 3409 additions and 1311 deletions
@@ -1,12 +1,14 @@
"""AZ-319 ``HttpTileUploader`` unit tests.
Covers AC-1 .. AC-14 and the upload-throughput NFR from
``_docs/02_tasks/todo/AZ-319_c11_tile_uploader.md``.
Covers AC-1, AC-3 .. AC-14 and the upload-throughput NFR from
``_docs/02_tasks/done/AZ-319_c11_tile_uploader.md``. AC-2 (the legacy
ON_GROUND gate) was removed in batch 44 — gating is now C12's
``PostLandingUploadOrchestrator`` responsibility.
Uses :class:`httpx.MockTransport` for deterministic HTTP responses,
:class:`FakeFdrSink` for FDR capture, a list-backed ``logging.Handler``
for log capture, and stub C6 stores / gate / key manager so this
suite never drags in AZ-303 / AZ-305 / AZ-317 / AZ-318 internals.
for log capture, and stub C6 stores / key manager so this suite never
drags in AZ-303 / AZ-305 / AZ-318 internals.
"""
from __future__ import annotations
@@ -25,8 +27,6 @@ import pytest
from gps_denied_onboard.components.c11_tile_manager import (
C11Config,
FlightStateNotOnGroundError,
FlightStateSignal,
HttpTileUploader,
IngestStatus,
PerFlightKeyManager,
@@ -37,9 +37,6 @@ from gps_denied_onboard.components.c11_tile_manager import (
UploadRequest,
canonical_payload_bytes,
)
from gps_denied_onboard.components.c11_tile_manager.flight_state_gate import (
FlightStateGate,
)
from gps_denied_onboard.fdr_client import FdrRecord
from gps_denied_onboard.fdr_client.fakes import FakeFdrSink
@@ -125,25 +122,6 @@ class _FakeMetadataStore:
self.mark_calls.append((tile_id, uploaded_at))
class _StubGate:
"""Stand-in for AZ-317 ``FlightStateGate``."""
def __init__(
self, signal: FlightStateSignal = FlightStateSignal.ON_GROUND
) -> None:
self._signal = signal
self.confirm_calls: int = 0
def confirm_on_ground(self) -> FlightStateSignal:
self.confirm_calls += 1
if self._signal != FlightStateSignal.ON_GROUND:
raise FlightStateNotOnGroundError(
self._signal,
datetime.now(timezone.utc),
)
return self._signal
class _StubKeyManager:
"""Stand-in for AZ-318 ``PerFlightKeyManager``.
@@ -222,7 +200,6 @@ def _build_uploader(
transport: httpx.MockTransport,
pending: list[_FakeTile] | None = None,
blobs: dict[str, bytes] | None = None,
gate_signal: FlightStateSignal = FlightStateSignal.ON_GROUND,
fingerprint_hex: str = "0123456789abcdef",
config: C11Config | None = None,
sleep_recorder: list[float] | None = None,
@@ -230,7 +207,6 @@ def _build_uploader(
HttpTileUploader,
FakeFdrSink,
list[logging.LogRecord],
_StubGate,
_StubKeyManager,
_FakeTileStore,
_FakeMetadataStore,
@@ -249,7 +225,6 @@ def _build_uploader(
logger.setLevel(logging.DEBUG)
logger.propagate = False
gate = _StubGate(signal=gate_signal)
key_manager = _StubKeyManager(fingerprint_hex=fingerprint_hex)
tile_store = _FakeTileStore(blobs=blobs)
metadata_store = _FakeMetadataStore(pending=pending)
@@ -272,14 +247,13 @@ def _build_uploader(
http_client=client,
tile_store=tile_store,
tile_metadata_store=metadata_store,
flight_state_gate=gate, # type: ignore[arg-type]
key_manager=key_manager, # type: ignore[arg-type]
fdr_client=fdr, # type: ignore[arg-type]
logger=logger,
config=cfg,
sleep=_sleep,
)
return uploader, fdr, log_records, gate, key_manager, tile_store, metadata_store, sleeps
return uploader, fdr, log_records, key_manager, tile_store, metadata_store, sleeps
def _make_request(*, batch_size: int = 10, flight_id: UUID | None = None) -> UploadRequest:
@@ -361,7 +335,6 @@ def test_ac1_50_tile_happy_path_marks_all_uploaded() -> None:
uploader,
fdr,
_logs,
_gate,
key_manager,
_tile_store,
metadata_store,
@@ -385,48 +358,6 @@ def test_ac1_50_tile_happy_path_marks_all_uploaded() -> None:
assert key_manager.end_calls == 1
# ----------------------------------------------------------------------
# AC-2: gate blocks before any read or POST
# ----------------------------------------------------------------------
def test_ac2_gate_blocks_before_any_read_or_post() -> None:
# Arrange
pending = [_make_tile()]
posted: list[httpx.Request] = []
def _handler(request: httpx.Request) -> httpx.Response:
posted.append(request)
return httpx.Response(202, json={"batch_uuid": str(uuid4()), "per_tile_status": []})
transport = httpx.MockTransport(_handler)
(
uploader,
_fdr,
_logs,
gate,
key_manager,
tile_store,
metadata_store,
_sleeps,
) = _build_uploader(
transport=transport,
pending=pending,
gate_signal=FlightStateSignal.IN_FLIGHT,
)
# Act / Assert
with pytest.raises(FlightStateNotOnGroundError):
uploader.upload_pending_tiles(_make_request())
assert gate.confirm_calls == 1
assert metadata_store.pending_calls == 0
assert tile_store.read_calls == []
assert key_manager.start_calls == []
assert key_manager.end_calls == 0
assert posted == []
# ----------------------------------------------------------------------
# AC-3: signature rejection — record + skip mark_uploaded; outcome=partial
# ----------------------------------------------------------------------
@@ -457,7 +388,6 @@ def test_ac3_signature_rejection_records_and_keeps_pending() -> None:
uploader,
fdr,
_logs,
_gate,
key_manager,
_tile_store,
metadata_store,
@@ -504,7 +434,6 @@ def test_ac4_duplicate_and_superseded_are_success() -> None:
uploader,
_fdr,
_logs,
_gate,
_key_manager,
_tile_store,
metadata_store,
@@ -536,7 +465,6 @@ def test_ac5_signing_key_zeroised_on_success() -> None:
uploader,
_fdr,
_logs,
_gate,
key_manager,
_tile_store,
_metadata_store,
@@ -570,7 +498,6 @@ def test_ac6_signing_key_zeroised_on_failure() -> None:
uploader,
_fdr,
_logs,
_gate,
key_manager,
_tile_store,
metadata_store,
@@ -605,7 +532,6 @@ def test_ac7_public_key_fdr_precedes_tile_fdr() -> None:
uploader,
fdr,
_logs,
_gate,
_key_manager,
_tile_store,
_metadata_store,
@@ -661,7 +587,6 @@ def test_ac8_429_honours_retry_after_seconds() -> None:
uploader,
_fdr,
_logs,
_gate,
_key_manager,
_tile_store,
_metadata_store,
@@ -697,7 +622,6 @@ def test_ac9_persistent_5xx_raises_satellite_provider_error() -> None:
uploader,
_fdr,
_logs,
_gate,
key_manager,
_tile_store,
_metadata_store,
@@ -731,7 +655,6 @@ def test_ac10_401_fails_fast_no_retry() -> None:
uploader,
_fdr,
log_records,
_gate,
_key_manager,
_tile_store,
_metadata_store,
@@ -766,7 +689,6 @@ def test_ac11_empty_pending_set_is_success_no_posts() -> None:
uploader,
fdr,
_logs,
_gate,
key_manager,
_tile_store,
_metadata_store,
@@ -868,7 +790,6 @@ def test_ac14_partial_success_batch_does_not_raise() -> None:
uploader,
_fdr,
_logs,
_gate,
_key_manager,
_tile_store,
metadata_store,
@@ -910,7 +831,6 @@ def test_429_budget_exhaustion_raises_rate_limited_error() -> None:
uploader,
_fdr,
_logs,
_gate,
key_manager,
_tile_store,
_metadata_store,
@@ -951,7 +871,7 @@ def test_nfr_throughput_1000_tiles_under_budget() -> None:
)
transport = httpx.MockTransport(_handler)
(uploader, _fdr, _logs, _gate, _km, _ts, _ms, _sleeps) = _build_uploader(
(uploader, _fdr, _logs, _km, _ts, _ms, _sleeps) = _build_uploader(
transport=transport, pending=pending
)