[AZ-402] Replay — gps-denied-replay console-script + shared main(config)

Implements the replay-mode CLI dispatcher per ADR-011 (replay-as-
configuration):

- src/gps_denied_onboard/cli/replay.py: argparse with all 6 required
  args (--video, --tlog, --output, --camera-calibration, --config,
  --mavlink-signing-key) plus --pace and --time-offset-ms; path
  validation, calibration JSON schema-validation, config mutation
  (mode='replay' + replay sub-block + signing-key hex on dev_static
  field), dispatch into runtime_root.main(config).
- runtime_root.main() now accepts an optional Config (additive,
  backward-compat). Adds dedicated catch for ReplayInputAdapterError
  mapping to EXIT_FDR_OPEN_FAILURE (2) so the CLI's exit-code matrix
  holds end-to-end (AC-9 + epic AZ-265 AC-8).
- Signing-key contents stored as hex; redacted in startup banner.
- Top-level except logs full traceback via logger.exception + stderr
  print and exits 1.

The CLI does NOT call compose_root directly — it builds a Config and
hands it to the shared airborne main, which calls compose_root, which
branches on config.mode (AZ-401 / replay protocol Invariant 11).

Tests: 22 unit tests covering AC-1..AC-10 + extras (signing-key
redaction, file-not-dir validation, dev_static propagation, unhandled
exception traceback). Full regression: 2085 passed (+22) green; no
new flaky tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-14 20:04:37 +03:00
parent 17a0d074af
commit 2c31cc094f
6 changed files with 990 additions and 11 deletions
@@ -618,10 +618,39 @@ def _read_flight_root(config: Config) -> str:
return str(path) if path is not None else "<unknown>"
def main() -> int: # pragma: no cover — guarded entrypoint
def main(config: Config | None = None) -> int:
"""Shared airborne-binary entrypoint.
Both the live ``gps-denied-onboard`` console-script and the replay
``gps-denied-replay`` console-script (AZ-402) dispatch here. When
``config`` is ``None`` the live binary's behaviour is preserved: load
from environment + default paths and compose. When a pre-built
``Config`` is supplied (replay CLI), it is used directly so the CLI
can mutate ``config.mode = "replay"`` + populate the replay sub-block
before the airborne main runs.
Per ADR-011 there is one composition root, ``compose_root``, which
branches on ``config.mode``. The CLI MUST NOT call ``compose_root``
directly (replay protocol Invariant 11).
Exit codes:
* ``0`` — success.
* ``EXIT_FDR_OPEN_FAILURE`` (``2``) — operator-visible startup hard-fail:
FDR cannot open OR replay auto-sync impossible (AZ-405 AC-8 / epic
AZ-265 AC-8). Both share the code because both demand operator
action before the binary can run.
* ``EXIT_GENERIC_FAILURE`` (``1``) — any other error.
"""
from gps_denied_onboard.replay_input import ReplayInputAdapterError
try:
config = load_config(env=os.environ, paths=())
if config is None:
config = load_config(env=os.environ, paths=())
compose_root(config)
except ReplayInputAdapterError as exc:
print(f"runtime_root: replay sync impossible: {exc}", file=sys.stderr)
return EXIT_FDR_OPEN_FAILURE
except (ConfigurationError, StrategyNotLinkedError, RuntimeError) as exc:
print(f"runtime_root: {exc}", file=sys.stderr)
return EXIT_GENERIC_FAILURE