mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 12:51:12 +00:00
[AZ-687] Guard build_pre_constructed seeds in replay mode
Replay CLI synthesizes a minimal Config whose `components` mapping omits the strategy-component blocks (`c6_tile_cache`, `c7_inference`, `c5_state`) the airborne bootstrap historically read unconditionally. Add `_replay_omits_component_block` and gate the c6 seeds, the c7 + c3_lightglue_runtime pair, and the c5 (estimator, handle) eager build on `config.mode == "replay" AND block absent`. Live mode and any replay config that DOES populate the blocks remain unchanged — the guard is conditional, not blanket. The skip is safe because compose_root's per-component wrappers only run for slugs in `config.components`; absent blocks mean absent wrappers, so the seeded slots would never be read. Fix lives at the BUILD-PRE-CONSTRUCTED layer per the spec's explicit "no silent fallback in `_c6_config`" constraint. Covers AC-687-1 / AC-687-2 / AC-687-4. AC-687-3 (Jetson Tier-2 e2e replay) requires an out-of-band hardware re-run; evidence destination documented in autodev state. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
"""AZ-687 — ``build_pre_constructed`` replay-mode guard.
|
||||
|
||||
Pins the contract at
|
||||
``_docs/02_tasks/todo/AZ-687_pre_constructed_replay_mode_guard.md``:
|
||||
|
||||
* AC-687-1: a replay-mode :class:`Config` whose ``components`` mapping
|
||||
omits ``c6_tile_cache`` must NOT raise ``KeyError`` inside
|
||||
:func:`build_pre_constructed`. The skipped slots
|
||||
(``c6_descriptor_index`` / ``c6_tile_store`` and, under the same
|
||||
predicate, ``c7_inference`` / ``c3_lightglue_runtime`` / the C5
|
||||
``(estimator, handle)`` pair) are absent from the returned dict
|
||||
instead of mapping to a stand-in.
|
||||
* AC-687-2: live-mode (and any replay config that DOES populate the
|
||||
blocks) continues to seed every key documented in
|
||||
:data:`AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS` — the guard is
|
||||
conditional, not unconditional.
|
||||
|
||||
The Jetson Tier-2 e2e gate (AC-687-3) is verified out-of-band; the
|
||||
full Tier-1 pytest suite (AC-687-4) is run by the implement skill's
|
||||
final test step.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from gps_denied_onboard.clock.wall_clock import WallClock
|
||||
from gps_denied_onboard.config import Config
|
||||
from gps_denied_onboard.fdr_client import client as fdr_client_module
|
||||
from gps_denied_onboard.fdr_client.client import FdrClient
|
||||
from gps_denied_onboard.runtime_root import airborne_bootstrap
|
||||
from gps_denied_onboard.runtime_root.airborne_bootstrap import (
|
||||
AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS,
|
||||
build_pre_constructed,
|
||||
clear_imu_preintegrator_cache,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolated_caches() -> Iterator[None]:
|
||||
fdr_client_module._reset_for_tests()
|
||||
clear_imu_preintegrator_cache()
|
||||
yield
|
||||
fdr_client_module._reset_for_tests()
|
||||
clear_imu_preintegrator_cache()
|
||||
|
||||
|
||||
def _stub_unconditional_helpers(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Stub the always-built helper seeds so the test focuses on the guard.
|
||||
|
||||
The guard's correctness lives in the conditional builds for
|
||||
``c6_*``, ``c7_inference``, ``c3_lightglue_runtime``, and the C5
|
||||
estimator pair. The other six seeds (``c13_fdr``, ``clock``,
|
||||
``c3_feature_extractor``, ``c282_ransac_filter``,
|
||||
``c5_imu_preintegrator``, ``c5_se3_utils``, ``c5_wgs_converter``)
|
||||
are unconditional; stubbing them keeps the test free of OpenCV +
|
||||
camera-calibration filesystem requirements.
|
||||
"""
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c3_feature_extractor",
|
||||
lambda _config: MagicMock(name="FeatureExtractor"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c282_ransac_filter",
|
||||
lambda _config: MagicMock(name="RansacFilter"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c5_imu_preintegrator",
|
||||
lambda _config: MagicMock(name="ImuPreintegrator"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c5_se3_utils",
|
||||
lambda _config: MagicMock(name="Se3Utils"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"_build_c5_wgs_converter",
|
||||
lambda _config: MagicMock(name="WgsConverter"),
|
||||
)
|
||||
|
||||
|
||||
def _stub_guarded_builders(monkeypatch: pytest.MonkeyPatch) -> dict[str, list[bool]]:
|
||||
"""Stub the guarded builders and track whether each was invoked.
|
||||
|
||||
Returns a dict mapping each builder name to a single-element list
|
||||
whose contents flip to ``True`` when the builder runs. Tests assert
|
||||
on those flags to prove the guard fired or didn't.
|
||||
"""
|
||||
invoked: dict[str, list[bool]] = {
|
||||
"_build_c6_descriptor_index": [False],
|
||||
"_build_c6_tile_store": [False],
|
||||
"_build_c7_inference": [False],
|
||||
"_build_c3_lightglue_runtime": [False],
|
||||
"_build_c5_state_estimator_pair": [False],
|
||||
}
|
||||
|
||||
def _track_c6_index(_config: Config) -> MagicMock:
|
||||
invoked["_build_c6_descriptor_index"][0] = True
|
||||
return MagicMock(name="DescriptorIndex")
|
||||
|
||||
def _track_c6_store(_config: Config) -> MagicMock:
|
||||
invoked["_build_c6_tile_store"][0] = True
|
||||
return MagicMock(name="TileStore")
|
||||
|
||||
def _track_c7(_config: Config) -> MagicMock:
|
||||
invoked["_build_c7_inference"][0] = True
|
||||
return MagicMock(name="InferenceRuntime")
|
||||
|
||||
def _track_c3_lightglue(_config: Config, *, inference_runtime: object) -> MagicMock:
|
||||
invoked["_build_c3_lightglue_runtime"][0] = True
|
||||
del inference_runtime
|
||||
return MagicMock(name="LightGlueRuntime")
|
||||
|
||||
def _track_c5_pair(*_args: object, **_kwargs: object) -> tuple[MagicMock, MagicMock]:
|
||||
invoked["_build_c5_state_estimator_pair"][0] = True
|
||||
return (MagicMock(name="StateEstimator"), MagicMock(name="ISam2GraphHandle"))
|
||||
|
||||
monkeypatch.setattr(airborne_bootstrap, "_build_c6_descriptor_index", _track_c6_index)
|
||||
monkeypatch.setattr(airborne_bootstrap, "_build_c6_tile_store", _track_c6_store)
|
||||
monkeypatch.setattr(airborne_bootstrap, "_build_c7_inference", _track_c7)
|
||||
monkeypatch.setattr(airborne_bootstrap, "_build_c3_lightglue_runtime", _track_c3_lightglue)
|
||||
monkeypatch.setattr(airborne_bootstrap, "_build_c5_state_estimator_pair", _track_c5_pair)
|
||||
return invoked
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# AC-687-1: replay-mode without strategy blocks
|
||||
|
||||
|
||||
def test_ac_687_1_replay_mode_without_c6_block_does_not_raise_keyerror(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Arrange: replay-mode Config with empty components (no c6_tile_cache,
|
||||
# no c7_inference, no c5_state block) — exactly the shape the
|
||||
# `gps-denied-replay` CLI produces from the minimal Derkachi yaml
|
||||
# (mode: replay + replay: pace/target_fc_dialect).
|
||||
_stub_unconditional_helpers(monkeypatch)
|
||||
invoked = _stub_guarded_builders(monkeypatch)
|
||||
config = Config(mode="replay")
|
||||
|
||||
# Act
|
||||
pre_constructed = build_pre_constructed(config)
|
||||
|
||||
# Assert: the guarded keys are absent (not built, not sentinel'd).
|
||||
for skipped_key in (
|
||||
"c6_descriptor_index",
|
||||
"c6_tile_store",
|
||||
"c7_inference",
|
||||
"c3_lightglue_runtime",
|
||||
"c5_isam2_graph_handle",
|
||||
):
|
||||
assert skipped_key not in pre_constructed, (
|
||||
f"AC-687-1: {skipped_key!r} must be omitted in replay mode "
|
||||
f"when the corresponding config block is absent"
|
||||
)
|
||||
for skipped_builder in (
|
||||
"_build_c6_descriptor_index",
|
||||
"_build_c6_tile_store",
|
||||
"_build_c7_inference",
|
||||
"_build_c3_lightglue_runtime",
|
||||
"_build_c5_state_estimator_pair",
|
||||
):
|
||||
assert not invoked[skipped_builder][0], (
|
||||
f"AC-687-1: {skipped_builder} must not run when the guarded "
|
||||
f"config block is absent in replay mode"
|
||||
)
|
||||
# The unconditional keys still seed — these are needed even in a
|
||||
# minimal replay run (c13_fdr for FDR side-channel writes; clock
|
||||
# is replaced by compose_root's replay-branch override).
|
||||
assert isinstance(pre_constructed["c13_fdr"], FdrClient)
|
||||
assert isinstance(pre_constructed["clock"], WallClock)
|
||||
|
||||
|
||||
def test_ac_687_1_replay_mode_with_c6_block_present_still_builds_c6_seeds(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Arrange: replay-mode Config that DOES populate c6_tile_cache /
|
||||
# c7_inference / c5_state blocks (e.g. a replay scenario that
|
||||
# exercises satellite-anchor verification). The guard must NOT
|
||||
# fire — those seeds remain authoritative when their blocks are
|
||||
# registered.
|
||||
_stub_unconditional_helpers(monkeypatch)
|
||||
invoked = _stub_guarded_builders(monkeypatch)
|
||||
config = Config.with_blocks(
|
||||
mode="replay",
|
||||
c6_tile_cache={"strategy": "postgres_filesystem"},
|
||||
c7_inference={"runtime": "pytorch_fp16"},
|
||||
c5_state={"strategy": "gtsam_isam2"},
|
||||
)
|
||||
|
||||
# Act
|
||||
pre_constructed = build_pre_constructed(config)
|
||||
|
||||
# Assert: every guarded builder ran, every guarded key seeded.
|
||||
for builder_name in (
|
||||
"_build_c6_descriptor_index",
|
||||
"_build_c6_tile_store",
|
||||
"_build_c7_inference",
|
||||
"_build_c3_lightglue_runtime",
|
||||
"_build_c5_state_estimator_pair",
|
||||
):
|
||||
assert invoked[builder_name][0], (
|
||||
f"AC-687-1 inverse: {builder_name} must run when the gating "
|
||||
f"config block IS present (replay mode is not a blanket "
|
||||
f"opt-out — only a missing block triggers the skip)"
|
||||
)
|
||||
for seeded_key in (
|
||||
"c6_descriptor_index",
|
||||
"c6_tile_store",
|
||||
"c7_inference",
|
||||
"c3_lightglue_runtime",
|
||||
"c5_isam2_graph_handle",
|
||||
):
|
||||
assert seeded_key in pre_constructed
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# AC-687-2: live-mode regression
|
||||
|
||||
|
||||
def test_ac_687_2_live_mode_seeds_every_required_key(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Arrange: live-mode Config (default) with empty components — the
|
||||
# guard MUST NOT fire because the mode predicate is false. Stub
|
||||
# heavy builders to keep the test independent of FAISS / TensorRT /
|
||||
# gtsam build flags.
|
||||
_stub_unconditional_helpers(monkeypatch)
|
||||
invoked = _stub_guarded_builders(monkeypatch)
|
||||
config = Config() # mode defaults to "live"
|
||||
expected_keys: set[str] = set().union(*AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS.values())
|
||||
|
||||
# Act
|
||||
pre_constructed = build_pre_constructed(config)
|
||||
|
||||
# Assert: every guarded builder ran (live-mode unchanged) and every
|
||||
# documented required key is present and non-None.
|
||||
for builder_name in (
|
||||
"_build_c6_descriptor_index",
|
||||
"_build_c6_tile_store",
|
||||
"_build_c7_inference",
|
||||
"_build_c3_lightglue_runtime",
|
||||
"_build_c5_state_estimator_pair",
|
||||
):
|
||||
assert invoked[builder_name][0], (
|
||||
f"AC-687-2: live-mode regression — {builder_name} was "
|
||||
f"skipped, but the AZ-687 guard MUST only fire in replay mode"
|
||||
)
|
||||
missing = expected_keys - pre_constructed.keys()
|
||||
assert not missing, (
|
||||
f"AC-687-2: live-mode regression — missing keys after the "
|
||||
f"AZ-687 guard landed: {sorted(missing)}"
|
||||
)
|
||||
for key in expected_keys:
|
||||
assert pre_constructed[key] is not None, (
|
||||
f"AC-687-2: pre_constructed[{key!r}] must map to a real "
|
||||
f"instance, not None, in live mode"
|
||||
)
|
||||
Reference in New Issue
Block a user