mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 11:01:13 +00:00
[AZ-620] Phase B: build_pre_constructed seeds c6_descriptor_index + c6_tile_store
Second of six subtasks of AZ-618. Extends airborne_bootstrap.build_pre_constructed(config) additively with the two C6 storage entries on top of AZ-619's c13_fdr + clock contract: - c6_descriptor_index: via storage_factory.build_descriptor_index - c6_tile_store: via storage_factory.build_tile_store When BUILD_FAISS_INDEX=OFF, the lower-level RuntimeNotAvailableError from the descriptor index factory is translated into an AirborneBootstrapError that names the missing key (c6_descriptor_index), the gating flag (BUILD_FAISS_INDEX), and the consuming component slug(s) drawn from AIRBORNE_REQUIRED_PRE_CONSTRUCTED_KEYS. The original error is preserved as __cause__ so operators still see the upstream reason. Tests: 3 new unit tests cover AC-620.1 + AC-620.2 (twice, with and without a configured consumer, so the bootstrap fails loudly in either branch). AZ-619 tests updated to add an autouse stub for the Phase B builders (keeps them focused on Phase A keys) and to relax the "exactly two keys" assertion to "AZ-619 keys remain present under AZ-620 additivity" per the original test's own forward-pointer. Bonus: ruff --fix removed 12 pre-existing UP037 quoted-annotation warnings in airborne_bootstrap.py (covered by `from __future__ import annotations`). All in modified-area scope per quality-gates.mdc. Run: pytest tests/unit/runtime_root/ -q -> 15/15 passed in 1.06s. Spec moved to _docs/02_tasks/done/ in the previous commit (audit-trail backfill of batch_90 also landed there). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""AZ-619 — Phase A of AZ-618: ``build_pre_constructed`` seeds c13_fdr + clock.
|
||||
|
||||
Verifies the contract at
|
||||
``_docs/02_tasks/todo/AZ-619_pre_constructed_phase_a_c13_fdr_clock.md``:
|
||||
``_docs/02_tasks/done/AZ-619_pre_constructed_phase_a_c13_fdr_clock.md``:
|
||||
|
||||
* AC-619.1: ``build_pre_constructed(default_config)`` returns a dict
|
||||
containing keys ``c13_fdr`` (FdrClient instance) and ``clock``
|
||||
@@ -12,6 +12,17 @@ Verifies the contract at
|
||||
|
||||
AC-619.3 (this file exists with the above tests) is satisfied by the
|
||||
existence of this module.
|
||||
|
||||
AZ-620 amendment: ``build_pre_constructed`` now also calls the C6
|
||||
storage factories (``build_descriptor_index`` + ``build_tile_store``)
|
||||
to populate ``c6_descriptor_index`` + ``c6_tile_store``. Those
|
||||
factories require ``config.components["c6_tile_cache"]`` to be present
|
||||
and (for the descriptor index) ``BUILD_FAISS_INDEX=ON``. The AZ-619
|
||||
tests use bare ``Config()`` — empty ``components`` — so we stub the C6
|
||||
builders with sentinel returns. This keeps the AZ-619 contract
|
||||
assertions focused on Phase A keys without entangling them with the
|
||||
heavier Phase B integration paths (those are covered by
|
||||
``test_az620_pre_constructed_phase_b.py``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,6 +35,7 @@ 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_MAIN_PRODUCER_ID,
|
||||
build_pre_constructed,
|
||||
@@ -40,6 +52,21 @@ def _isolated_fdr_cache() -> Iterator[None]:
|
||||
fdr_client_module._reset_for_tests()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _stub_c6_builders(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange: stub the AZ-620 Phase B C6 storage builders so the AZ-619
|
||||
# tests stay focused on the Phase A contract. Without this the bare
|
||||
# Config() used below would hit KeyError inside storage_factory's
|
||||
# config.components["c6_tile_cache"] lookup. Sentinel objects are
|
||||
# opaque on purpose — the AZ-619 assertions never inspect them.
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap, "_build_c6_descriptor_index", lambda _config: object()
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap, "_build_c6_tile_store", lambda _config: object()
|
||||
)
|
||||
|
||||
|
||||
def test_ac_619_1_default_config_seeds_c13_fdr_and_clock() -> None:
|
||||
# Arrange
|
||||
config = Config()
|
||||
@@ -92,12 +119,14 @@ def test_ac_619_2_clock_is_independent_instance_per_call() -> None:
|
||||
assert isinstance(second["clock"], WallClock)
|
||||
|
||||
|
||||
def test_phase_a_only_seeds_two_keys() -> None:
|
||||
"""Phase A scope discipline: exactly the two documented keys.
|
||||
def test_phase_a_keys_remain_present_under_az620_additivity() -> None:
|
||||
"""Phase A's keys must survive AZ-620's additive extension.
|
||||
|
||||
Phases B..F (AZ-620..AZ-624) will add more keys; this test will be
|
||||
relaxed at that point. For now it pins the AZ-619 contract precisely so
|
||||
a regression that adds keys here without updating the spec is caught.
|
||||
AZ-620 (Phase B) added ``c6_descriptor_index`` + ``c6_tile_store`` and
|
||||
the spec mandates additivity ("MUST be additive on top of AZ-619" —
|
||||
AZ-620 Constraints). The AZ-619 contract is therefore: ``c13_fdr`` and
|
||||
``clock`` MUST always be present in the dict, regardless of how many
|
||||
later phases extend it.
|
||||
"""
|
||||
# Arrange
|
||||
config = Config()
|
||||
@@ -105,5 +134,9 @@ def test_phase_a_only_seeds_two_keys() -> None:
|
||||
# Act
|
||||
pre_constructed = build_pre_constructed(config)
|
||||
|
||||
# Assert
|
||||
assert set(pre_constructed.keys()) == {"c13_fdr", "clock"}
|
||||
# Assert: AZ-619 keys still there and correctly typed (the additivity
|
||||
# contract). Other keys are allowed and validated by Phase B/C/D/E/F
|
||||
# tests.
|
||||
assert {"c13_fdr", "clock"}.issubset(pre_constructed.keys())
|
||||
assert isinstance(pre_constructed["c13_fdr"], FdrClient)
|
||||
assert isinstance(pre_constructed["clock"], WallClock)
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
"""AZ-620 — Phase B of AZ-618: ``build_pre_constructed`` seeds c6_descriptor_index + c6_tile_store.
|
||||
|
||||
Verifies the contract at
|
||||
``_docs/02_tasks/todo/AZ-620_pre_constructed_phase_b_c6_storage.md``:
|
||||
|
||||
* AC-620.1: with a default airborne ``Config`` (and ``BUILD_FAISS_INDEX=ON``),
|
||||
``build_pre_constructed(config)`` additionally contains
|
||||
``c6_descriptor_index`` (DescriptorIndex instance) and ``c6_tile_store``
|
||||
(TileStore instance) on top of AZ-619's contract.
|
||||
* AC-620.2: when ``BUILD_FAISS_INDEX=OFF`` AND a config selects
|
||||
``c2_vpr.strategy="net_vlad"`` (a strategy that requires
|
||||
``c6_descriptor_index``), ``build_pre_constructed`` raises
|
||||
``AirborneBootstrapError`` whose message names ``c6_descriptor_index``
|
||||
(the missing key), ``BUILD_FAISS_INDEX`` (the gating flag), and
|
||||
``c2_vpr`` (the consuming component slug).
|
||||
|
||||
AC-620.3 (this file exists with the above tests) is satisfied by the
|
||||
existence of this module.
|
||||
|
||||
The tests stub the heavy C6 factories (`build_descriptor_index`,
|
||||
`build_tile_store`) at the airborne_bootstrap import boundary. The C6
|
||||
factories themselves have their own unit tests under
|
||||
``tests/unit/c6_tile_cache/`` covering Postgres + FAISS internals; here
|
||||
we only validate the bootstrap-layer wiring and error translation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from gps_denied_onboard.config import Config
|
||||
from gps_denied_onboard.fdr_client import client as fdr_client_module
|
||||
from gps_denied_onboard.runtime_root import airborne_bootstrap
|
||||
from gps_denied_onboard.runtime_root.airborne_bootstrap import (
|
||||
FAISS_BUILD_FLAG,
|
||||
AirborneBootstrapError,
|
||||
build_pre_constructed,
|
||||
)
|
||||
from gps_denied_onboard.runtime_root.errors import RuntimeNotAvailableError
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _C2VprBlock:
|
||||
"""Minimal c2_vpr config block — only the field the test needs."""
|
||||
|
||||
strategy: str = "net_vlad"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolated_fdr_cache() -> Iterator[None]:
|
||||
# Arrange: every test starts with an empty FdrClient cache so the
|
||||
# bootstrap's make_fdr_client call doesn't accidentally pick up a
|
||||
# stale instance from a prior test in the same process.
|
||||
fdr_client_module._reset_for_tests()
|
||||
yield
|
||||
fdr_client_module._reset_for_tests()
|
||||
|
||||
|
||||
def test_ac_620_1_adds_c6_descriptor_index_and_c6_tile_store(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Arrange: stub the c6 factories to return identifiable sentinels so we
|
||||
# can assert wiring without standing up real FAISS + Postgres.
|
||||
descriptor_index_sentinel = MagicMock(name="DescriptorIndex")
|
||||
tile_store_sentinel = MagicMock(name="TileStore")
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"build_descriptor_index",
|
||||
lambda _config: descriptor_index_sentinel,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"build_tile_store",
|
||||
lambda _config: tile_store_sentinel,
|
||||
)
|
||||
config = Config()
|
||||
|
||||
# Act
|
||||
pre_constructed = build_pre_constructed(config)
|
||||
|
||||
# Assert: AZ-620 keys are present and reference the exact factory
|
||||
# returns. AZ-619 keys are still present (additivity contract).
|
||||
assert "c6_descriptor_index" in pre_constructed
|
||||
assert "c6_tile_store" in pre_constructed
|
||||
assert pre_constructed["c6_descriptor_index"] is descriptor_index_sentinel
|
||||
assert pre_constructed["c6_tile_store"] is tile_store_sentinel
|
||||
assert "c13_fdr" in pre_constructed
|
||||
assert "clock" in pre_constructed
|
||||
|
||||
|
||||
def test_ac_620_2_build_flag_off_with_configured_c2_vpr_raises_named_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# Arrange: simulate BUILD_FAISS_INDEX=OFF by making the c6 descriptor
|
||||
# factory raise RuntimeNotAvailableError the same way storage_factory
|
||||
# does in production. Tile store stays stubbed so the test is focused
|
||||
# on the descriptor-index translation path.
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"build_descriptor_index",
|
||||
_raise_faiss_disabled,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"build_tile_store",
|
||||
lambda _config: MagicMock(name="TileStore"),
|
||||
)
|
||||
config = Config.with_blocks(c2_vpr=_C2VprBlock(strategy="net_vlad"))
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(AirborneBootstrapError) as excinfo:
|
||||
build_pre_constructed(config)
|
||||
|
||||
message = str(excinfo.value)
|
||||
assert "c6_descriptor_index" in message
|
||||
assert FAISS_BUILD_FLAG in message
|
||||
assert "c2_vpr" in message
|
||||
# The original factory error is preserved as the cause chain so the
|
||||
# operator can still see the upstream reason.
|
||||
assert isinstance(excinfo.value.__cause__, RuntimeNotAvailableError)
|
||||
|
||||
|
||||
def test_ac_620_2_no_configured_consumer_still_raises_with_full_set(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Defence-in-depth: even with no consumer configured, AZ-620 still
|
||||
fails loudly when BUILD_FAISS_INDEX is OFF.
|
||||
|
||||
Rationale: ``build_pre_constructed`` is the airborne contract's single
|
||||
seam — silently returning a dict without ``c6_descriptor_index`` would
|
||||
let a later AZ-621..AZ-624 phase or a downstream caller misread the
|
||||
state. The error message in this case lists the FULL theoretical
|
||||
consumer set so the operator still gets actionable information.
|
||||
"""
|
||||
# Arrange
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"build_descriptor_index",
|
||||
_raise_faiss_disabled,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
airborne_bootstrap,
|
||||
"build_tile_store",
|
||||
lambda _config: MagicMock(name="TileStore"),
|
||||
)
|
||||
config = Config() # empty components
|
||||
|
||||
# Act + Assert
|
||||
with pytest.raises(AirborneBootstrapError) as excinfo:
|
||||
build_pre_constructed(config)
|
||||
|
||||
message = str(excinfo.value)
|
||||
assert "c6_descriptor_index" in message
|
||||
assert FAISS_BUILD_FLAG in message
|
||||
# When no consumer is configured, ALL theoretical consumers
|
||||
# of c6_descriptor_index are surfaced — currently {c2_vpr}.
|
||||
assert "c2_vpr" in message
|
||||
|
||||
|
||||
def _raise_faiss_disabled(_config: Config) -> None:
|
||||
raise RuntimeNotAvailableError(
|
||||
f"DescriptorIndex runtime 'faiss_hnsw' requires "
|
||||
f"{FAISS_BUILD_FLAG}=ON in this binary; the flag is OFF."
|
||||
)
|
||||
Reference in New Issue
Block a user