From 9bdc868dfd4163821d1c7c3a4af1736bfea21504 Mon Sep 17 00:00:00 2001 From: Oleksandr Bezdieniezhnykh Date: Tue, 19 May 2026 12:22:03 +0300 Subject: [PATCH] [AZ-687] Guard build_pre_constructed seeds in replay mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...Z-687_pre_constructed_replay_mode_guard.md | 0 .../reviews/batch_97_review.md | 79 ++++++ _docs/_autodev_state.md | 4 +- .../runtime_root/airborne_bootstrap.py | 98 +++++-- .../test_az687_pre_constructed_replay_mode.py | 265 ++++++++++++++++++ 5 files changed, 428 insertions(+), 18 deletions(-) rename _docs/02_tasks/{todo => done}/AZ-687_pre_constructed_replay_mode_guard.md (100%) create mode 100644 _docs/03_implementation/reviews/batch_97_review.md create mode 100644 tests/unit/runtime_root/test_az687_pre_constructed_replay_mode.py diff --git a/_docs/02_tasks/todo/AZ-687_pre_constructed_replay_mode_guard.md b/_docs/02_tasks/done/AZ-687_pre_constructed_replay_mode_guard.md similarity index 100% rename from _docs/02_tasks/todo/AZ-687_pre_constructed_replay_mode_guard.md rename to _docs/02_tasks/done/AZ-687_pre_constructed_replay_mode_guard.md diff --git a/_docs/03_implementation/reviews/batch_97_review.md b/_docs/03_implementation/reviews/batch_97_review.md new file mode 100644 index 0000000..42c9e5d --- /dev/null +++ b/_docs/03_implementation/reviews/batch_97_review.md @@ -0,0 +1,79 @@ +# Code Review Report + +**Batch**: 97 — AZ-687 (`build_pre_constructed` replay-mode guard for c6 descriptor index) +**Date**: 2026-05-19 +**Verdict**: PASS + +## Inputs + +- Task spec: `_docs/02_tasks/todo/AZ-687_pre_constructed_replay_mode_guard.md` +- Changed files: + - `src/gps_denied_onboard/runtime_root/airborne_bootstrap.py` (modified — helper added, three guarded conditionals around the existing seeds) + - `tests/unit/runtime_root/test_az687_pre_constructed_replay_mode.py` (new — three tests covering AC-687-1 + inverse + AC-687-2) +- Project restrictions: `_docs/00_problem/restrictions.md` (re-read) +- Solution overview: `_docs/01_solution/solution.md` (re-read) + +## Findings + +None. + +## Phase Walkthrough + +### Phase 1: Context Loading + +Spec read: AZ-687 is a 2-point cross-cutting task surfacing the `KeyError: 'c6_tile_cache'` regression from the AZ-618 Jetson Tier-2 e2e run on 2026-05-19. The guard belongs at the BUILD-PRE-CONSTRUCTED layer, not at `_c6_config` (the existing docstring rejects silent fallback there). Scope is precisely: `runtime_root/airborne_bootstrap.py::build_pre_constructed` plus a new unit test under `tests/unit/runtime_root/`. + +### Phase 2: Spec Compliance + +| AC | Status | Test | +|----|--------|------| +| AC-687-1 (replay-mode without `c6_tile_cache` block does not raise; skipped slots absent from dict) | Satisfied | `test_ac_687_1_replay_mode_without_c6_block_does_not_raise_keyerror` (asserts no KeyError; asserts the five guarded keys are absent; asserts builders not invoked) + `test_ac_687_1_replay_mode_with_c6_block_present_still_builds_c6_seeds` (inverse — guard is conditional on block absence, not just on mode) | +| AC-687-2 (live-mode regression: every key in `set.union(*AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS.values())` still seeded; no key None) | Satisfied | `test_ac_687_2_live_mode_seeds_every_required_key` | +| AC-687-3 (Jetson Tier-2 e2e replay crosses `replay.compose_root.ready` AND `replay.input.frame_emitted` on AC-1/AC-2/AC-5/AC-6) | Pending — out-of-band hardware run required. Will be collected by the user via `scripts/run-tests-jetson.sh tests/e2e/replay/test_derkachi_1min.py`; evidence destination `_docs/03_implementation/jetson_runs/2026-05-19_az687_tier2_run.txt`. | +| AC-687-4 (full Tier-1 pytest suite green) | Satisfied — `pytest tests/unit/` reports 2153 passed, 85 skipped, 1 deselected (pre-existing `test_cold_start_under_500ms_p99` perf flake — operator-orchestrator CLI subprocess startup latency on busy dev macOS; not related to AZ-687). | + +Constraint compliance: + +- `_c6_config` untouched — silent fallback explicitly avoided. +- No new `BUILD_*` env flags introduced. +- No component factory signatures changed. +- No per-component config schema changes. + +Scope discipline: implementation is exactly two files; no scope creep, no opportunistic refactors. + +### Phase 3: Code Quality + +- **SRP**: `_replay_omits_component_block` has one reason to change (the predicate's truth table). `build_pre_constructed` retains its existing responsibility; the conditionals are inline, not factored into a new function. +- **Error handling**: no silent suppression. `KeyError` from `_c6_config` is still surfaced in any environment where the block is expected (live mode or replay-with-block). +- **Naming**: `_replay_omits_component_block` reads as a complete sentence at the call site (`if not _replay_omits_component_block(config, "c6_tile_cache"): ...`). The single-line docstrings on each conditional block name the invariant the skip preserves. +- **Complexity**: helper is 7 LOC (excluding docstring). `build_pre_constructed` grew by 3 `if` branches; cyclomatic complexity well under 10. +- **DRY**: the predicate is shared across the three skip sites; the conditionals stay inline (a higher abstraction would obscure the per-seed semantics). +- **Test quality**: tests assert specific behavior (which builders fired, which keys are present/absent), not just "no exception". `_stub_guarded_builders` returns a mutable invocation map so the assertions are precise. +- **Dead code**: none. +- **Comment discipline**: the inline comments document the WHY (why the skip is safe — wrappers gated on `config.components`), not the WHAT. + +### Phase 4: Security Quick-Scan + +No SQL, no subprocess, no `eval`/`exec`, no secrets, no new external inputs. Helper inspects `config.mode` (typed `Literal["live", "replay"]`) and `config.components` (typed `Mapping[str, Any]`); both are already validated by `Config.__post_init__`. + +### Phase 5: Performance Scan + +Helper is O(1): one attribute read + one dict membership check. No hot-path impact (bootstrap runs once per process). No N+1 risk, no unbounded fetch, no async-blocking. + +### Phase 6: Cross-Task Consistency + +Single-task batch; N/A. + +### Phase 7: Architecture Compliance + +- **Layer direction**: `runtime_root` is the composition root (ADR-001/ADR-009) — it MAY import concrete strategies across components. The change touches only the composition root and adds no new cross-component imports. +- **Public API respect**: no new imports introduced. +- **No new cyclic dependencies**: `_replay_omits_component_block` reads `Config` only via `getattr` + dict membership; no module-level imports added. +- **Duplicate symbols**: `_replay_omits_component_block` is unique to `airborne_bootstrap.py`. +- **Cross-cutting concerns not locally re-implemented**: the mode + block-presence predicate is a composition-root concern (the composition root is the only layer that owns the `config.mode` branch); placing it in `airborne_bootstrap.py` is correct per `_docs/02_document/module-layout.md` shared/runtime_root entry. + +Baseline delta: no `_docs/02_document/architecture_compliance_baseline.md` exists yet, so no delta is computed (baseline mode has not been run). + +## Verdict Justification + +Zero Critical / High findings (no Spec-Gap, no Bug, no Security, no Architecture violation). Zero Medium / Low findings. Verdict: **PASS** — no action required before commit. diff --git a/_docs/_autodev_state.md b/_docs/_autodev_state.md index de71b48..8d24388 100644 --- a/_docs/_autodev_state.md +++ b/_docs/_autodev_state.md @@ -8,8 +8,8 @@ status: in_progress sub_step: phase: 16 name: batch-loop - detail: "AZ-687 (2pt) filed under AZ-602 (blocks AZ-618); spec in todo/, Jetson Tier-2 evidence in _docs/03_implementation/jetson_runs/2026-05-19_az618_tier2_run.txt; resume batch loop in new session" + detail: "Batch 97 landed: AZ-687 (2pt) replay-mode guard in build_pre_constructed; spec moved to done/, code-review PASS (batch_97_review.md), Tier-1 suite green (2153 pass). AZ-687 transitioned to In Testing pending out-of-band Jetson Tier-2 e2e re-run (AC-687-3 evidence: _docs/03_implementation/jetson_runs/2026-05-19_az687_tier2_run.txt — to be collected by user)." retry_count: 0 cycle: 1 tracker: jira -last_completed_batch: 96 +last_completed_batch: 97 diff --git a/src/gps_denied_onboard/runtime_root/airborne_bootstrap.py b/src/gps_denied_onboard/runtime_root/airborne_bootstrap.py index e012c4e..a976b4d 100644 --- a/src/gps_denied_onboard/runtime_root/airborne_bootstrap.py +++ b/src/gps_denied_onboard/runtime_root/airborne_bootstrap.py @@ -1135,6 +1135,43 @@ def _build_c3_feature_extractor(config: Config) -> FeatureExtractor: return OpenCvOrbExtractor() +def _replay_omits_component_block(config: Config, block_name: str) -> bool: + """True iff replay-mode :class:`Config` has no ``components[block_name]`` entry. + + AZ-687: the replay CLI (``gps-denied-replay``) synthesizes a minimal + :class:`Config` carrying only ``mode == "replay"`` plus the + ``replay`` sub-block. The strategy packages that register their own + component config blocks via :func:`register_component_block` (e.g. + :mod:`gps_denied_onboard.components.c6_tile_cache`, + :mod:`gps_denied_onboard.components.c7_inference`, + :mod:`gps_denied_onboard.components.c5_state`) are not imported on + that path, so :func:`gps_denied_onboard.config.loader.load_config` + produces an empty ``components`` mapping. + + The :func:`build_pre_constructed` seeds that depend on those blocks + (``c6_descriptor_index``, ``c6_tile_store``, ``c7_inference``, + ``c3_lightglue_runtime``, the C5 ``(estimator, handle)`` pair) are + only consumed by :func:`compose_root`'s per-component wrappers, + which only execute for slugs actually present in + ``config.components``. With an empty components mapping, no wrapper + asks for those slots and skipping their bootstrap is safe. + + The fix lives at this layer (BUILD-PRE-CONSTRUCTED), NOT at + :func:`storage_factory._c6_config` / :func:`inference_factory._c7_config`: + those helpers deliberately raise :class:`KeyError` on missing blocks + (per their docstrings, silent fallback would mask a missing import). + + Returns ``False`` outside replay mode regardless of block presence — + live-mode contracts (AC-687-2) MUST keep seeding every key. + """ + if config.mode != "replay": + return False + components = getattr(config, "components", None) or {} + if not isinstance(components, Mapping): + return False + return block_name not in components + + def build_pre_constructed(config: Config) -> dict[str, Any]: """Build the airborne ``pre_constructed`` dict for :func:`compose_root`. @@ -1191,6 +1228,17 @@ def build_pre_constructed(config: Config) -> dict[str, Any]: the replay branch's :class:`TlogDerivedClock`. That's intentional and matches the contract in :func:`compose_root`'s docstring. + Replay-mode guard (AZ-687): when ``config.mode == "replay"`` and the + minimal replay Config omits a strategy-component block + (``c6_tile_cache``, ``c7_inference``, ``c5_state``), the bootstrap + skips the seeds that would otherwise call the corresponding + ``_cN_config`` helper and raise :class:`KeyError`. The skipped slots + are absent from the returned dict; ``compose_root``'s per-component + wrappers only run for slugs in ``config.components`` and never read + the skipped slots. Live mode is unaffected — every documented key in + :data:`AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS` is still seeded. See + :func:`_replay_omits_component_block` for the precise predicate. + Raises: AirborneBootstrapError: if ``BUILD_FAISS_INDEX`` is OFF and any configured consumer (per @@ -1216,27 +1264,45 @@ def build_pre_constructed(config: Config) -> dict[str, Any]: constructed: dict[str, Any] = {} constructed["c13_fdr"] = make_fdr_client(AIRBORNE_MAIN_PRODUCER_ID, config) constructed["clock"] = WallClock() - constructed["c6_descriptor_index"] = _build_c6_descriptor_index(config) - constructed["c6_tile_store"] = _build_c6_tile_store(config) - constructed["c7_inference"] = _build_c7_inference(config) - constructed["c3_lightglue_runtime"] = _build_c3_lightglue_runtime( - config, inference_runtime=constructed["c7_inference"] - ) + # AZ-687: a minimal replay Config carries no `c6_tile_cache` block, + # so `_build_c6_*` would KeyError inside `_c6_config`. The skip is + # safe because the only wrappers that read these slots + # (c2_vpr / c2_5_rerank / c5_state) require their own component + # entries in `config.components`, which the minimal replay Config + # also omits. + if not _replay_omits_component_block(config, "c6_tile_cache"): + constructed["c6_descriptor_index"] = _build_c6_descriptor_index(config) + constructed["c6_tile_store"] = _build_c6_tile_store(config) + # AZ-687: c3_lightglue_runtime cascades on `constructed["c7_inference"]`, + # so it's gated under the same `c7_inference` block guard. Replay + # mode without the c7_inference block also drops the c3_matcher / + # c2_5_rerank wrappers that would have consumed the runtime. + if not _replay_omits_component_block(config, "c7_inference"): + constructed["c7_inference"] = _build_c7_inference(config) + constructed["c3_lightglue_runtime"] = _build_c3_lightglue_runtime( + config, inference_runtime=constructed["c7_inference"] + ) constructed["c3_feature_extractor"] = _build_c3_feature_extractor(config) constructed["c282_ransac_filter"] = _build_c282_ransac_filter(config) constructed["c5_imu_preintegrator"] = _build_c5_imu_preintegrator(config) constructed["c5_se3_utils"] = _build_c5_se3_utils(config) constructed["c5_wgs_converter"] = _build_c5_wgs_converter(config) - estimator, handle = _build_c5_state_estimator_pair( - config, - imu_preintegrator=constructed["c5_imu_preintegrator"], - se3_utils=constructed["c5_se3_utils"], - wgs_converter=constructed["c5_wgs_converter"], - fdr_client=constructed["c13_fdr"], - tile_store=constructed["c6_tile_store"], - ) - constructed["c5_isam2_graph_handle"] = handle - constructed[_C5_PREBUILT_ESTIMATOR_KEY] = estimator + # AZ-687: the eager (estimator, handle) build registers the gtsam-bound + # state factory and reads `config.components["c5_state"]`. When the + # block is absent in replay mode the c5_state wrapper itself will not + # run, so the handle / prebuilt-estimator slots are unread; skip the + # build to avoid forcing the gtsam import on the replay binary. + if not _replay_omits_component_block(config, "c5_state"): + estimator, handle = _build_c5_state_estimator_pair( + config, + imu_preintegrator=constructed["c5_imu_preintegrator"], + se3_utils=constructed["c5_se3_utils"], + wgs_converter=constructed["c5_wgs_converter"], + fdr_client=constructed["c13_fdr"], + tile_store=constructed.get("c6_tile_store"), + ) + constructed["c5_isam2_graph_handle"] = handle + constructed[_C5_PREBUILT_ESTIMATOR_KEY] = estimator return constructed diff --git a/tests/unit/runtime_root/test_az687_pre_constructed_replay_mode.py b/tests/unit/runtime_root/test_az687_pre_constructed_replay_mode.py new file mode 100644 index 0000000..d845516 --- /dev/null +++ b/tests/unit/runtime_root/test_az687_pre_constructed_replay_mode.py @@ -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" + )