"""AZ-301 — ``EngineGate`` acceptance tests. AC-1..AC-7 + NFR-reliability-no-write cover the five-step deterministic refusal pipeline plus the happy path. AC-8 (``read_host_tuple`` on a real Jetson) is Tier-2-only and lives in the integration suite — it needs an actual NVML-readable GPU + ``/etc/nv_tegra_release``. """ from __future__ import annotations import hashlib import json import logging import os from pathlib import Path import pytest from gps_denied_onboard._types.inference import EngineCacheEntry, PrecisionMode from gps_denied_onboard.components.c7_inference import ( DeploymentManifest, EngineGate, EngineHashMismatchError, EngineSchemaMismatchError, EngineSidecarMissingError, HostTuple, ManifestReader, ) _TIER2_HOST = HostTuple(sm=87, jp="6.2", trt="10.3", precision=PrecisionMode.FP16) def _write_engine(tmp_path: Path, name: str, payload: bytes = b"fake-engine") -> Path: engine_path = tmp_path / name engine_path.write_bytes(payload) return engine_path def _write_sidecar(engine_path: Path, hex_digest: str | None = None) -> str: if hex_digest is None: hex_digest = hashlib.sha256(engine_path.read_bytes()).hexdigest() sidecar = Path(str(engine_path) + ".sha256") sidecar.write_text(hex_digest, encoding="utf-8") return hex_digest def _entry_for(engine_path: Path) -> EngineCacheEntry: return EngineCacheEntry( engine_path=engine_path, sha256_hex=hashlib.sha256(engine_path.read_bytes()).hexdigest(), sm=_TIER2_HOST.sm, jp=_TIER2_HOST.jp, trt=_TIER2_HOST.trt, precision=PrecisionMode.FP16, extras={}, ) def _manifest_with(engine_path: Path, sha_hex: str) -> DeploymentManifest: relative = engine_path.relative_to(engine_path.parent) return DeploymentManifest( root=engine_path.parent, entries={relative.as_posix(): sha_hex}, ) # ---------------------------------------------------------------------- # AC-1: filename-schema parse failure refused at parse time. def test_ac1_parse_failure_refused_at_parse_time(tmp_path: Path) -> None: # Arrange bogus = _write_engine(tmp_path, "bogus_name.engine") entry = _entry_for(bogus) gate = EngineGate() # Act / Assert with pytest.raises(EngineSchemaMismatchError, match="AZ-281 schema"): gate.validate(entry, _TIER2_HOST, _manifest_with(bogus, "0" * 64)) assert not Path(str(bogus) + ".sha256").exists(), ( "Gate must NOT have created a sidecar during refusal" ) # ---------------------------------------------------------------------- # AC-2: filename-schema tuple mismatch refused at parse time. def test_ac2_schema_tuple_mismatch(tmp_path: Path) -> None: # Arrange — engine compiled for sm=86 but host is sm=87. engine = _write_engine(tmp_path, "ultravpr__sm86_jp6.2_trt10.3_fp16.engine") entry = _entry_for(engine) gate = EngineGate() # Act / Assert with pytest.raises(EngineSchemaMismatchError) as exc_info: gate.validate(entry, _TIER2_HOST, _manifest_with(engine, "0" * 64)) msg = str(exc_info.value) assert "sm=86" in msg assert "sm=87" in msg # ---------------------------------------------------------------------- # AC-3: missing sidecar refused before manifest lookup. def test_ac3_missing_sidecar_refused_before_manifest(tmp_path: Path) -> None: # Arrange engine = _write_engine(tmp_path, "ultravpr__sm87_jp6.2_trt10.3_fp16.engine") entry = _entry_for(engine) manifest_calls: list[Path] = [] class _TrackingManifest: def read(self) -> DeploymentManifest: manifest_calls.append(engine) return _manifest_with(engine, "0" * 64) gate = EngineGate() # Act / Assert with pytest.raises(EngineSidecarMissingError, match=r"\.sha256"): gate.validate(entry, _TIER2_HOST, _TrackingManifest()) # Manifest may be read at step 0 (validate prologue) but the missing-sidecar # check refuses BEFORE manifest lookup; no manifest *__getitem__* call. # ---------------------------------------------------------------------- # AC-4: sidecar trust failure. def test_ac4_sidecar_hash_mismatches_file(tmp_path: Path) -> None: # Arrange engine = _write_engine(tmp_path, "ultravpr__sm87_jp6.2_trt10.3_fp16.engine") _write_sidecar(engine, hex_digest="d" * 64) # wrong hash entry = _entry_for(engine) gate = EngineGate() # Act / Assert with pytest.raises(EngineHashMismatchError, match="sidecar"): gate.validate(entry, _TIER2_HOST, _manifest_with(engine, "d" * 64)) # ---------------------------------------------------------------------- # AC-5: manifest mismatch. def test_ac5_manifest_hash_mismatches_sidecar(tmp_path: Path) -> None: # Arrange engine = _write_engine(tmp_path, "ultravpr__sm87_jp6.2_trt10.3_fp16.engine") true_hash = _write_sidecar(engine) bogus_manifest = _manifest_with(engine, "f" * 64) entry = _entry_for(engine) gate = EngineGate() # Act / Assert with pytest.raises(EngineHashMismatchError, match="manifest hash"): gate.validate(entry, _TIER2_HOST, bogus_manifest) assert "f" * 64 in str(true_hash) or true_hash != "f" * 64 # ---------------------------------------------------------------------- # AC-6: full-success path returns silently and logs INFO. def test_ac6_full_success_returns_silently_and_logs_pass( tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: # Arrange engine = _write_engine(tmp_path, "ultravpr__sm87_jp6.2_trt10.3_fp16.engine") sha = _write_sidecar(engine) manifest = _manifest_with(engine, sha) entry = _entry_for(engine) gate = EngineGate() # Act with caplog.at_level(logging.INFO, logger="c7_inference.gate"): result = gate.validate(entry, _TIER2_HOST, manifest) # Assert assert result is None pass_records = [ r for r in caplog.records if getattr(r, "kind", None) == "c7.gate.pass" ] assert len(pass_records) == 1, ( f"expected exactly one c7.gate.pass record; got {len(pass_records)} " f"of {[r.message for r in caplog.records]}" ) # ---------------------------------------------------------------------- # AC-7: refusal order is deterministic — schema wins over sidecar-missing. def test_ac7_schema_error_wins_over_sidecar_missing(tmp_path: Path) -> None: # Arrange — both broken: schema mismatch AND no sidecar on disk. engine = _write_engine(tmp_path, "bogus.engine") entry = _entry_for(engine) gate = EngineGate() # Act / Assert with pytest.raises(EngineSchemaMismatchError): gate.validate(entry, _TIER2_HOST, _manifest_with(engine, "0" * 64)) # ---------------------------------------------------------------------- # Manifest reader coverage. def test_manifest_reader_round_trip(tmp_path: Path) -> None: manifest_path = tmp_path / "manifest.json" payload = { "root": str(tmp_path), "entries": { "ultravpr__sm87_jp6.2_trt10.3_fp16.engine": "a" * 64, "lightglue__sm87_jp6.2_trt10.3_int8.engine": "b" * 64, }, } manifest_path.write_text(json.dumps(payload), encoding="utf-8") manifest = ManifestReader(manifest_path).read() assert manifest.root == tmp_path assert manifest["ultravpr__sm87_jp6.2_trt10.3_fp16.engine"] == "a" * 64 assert "lightglue__sm87_jp6.2_trt10.3_int8.engine" in manifest def test_manifest_missing_entry_raises_hash_mismatch() -> None: """Risk 2 — missing manifest entry MUST raise EngineHashMismatchError, NOT KeyError.""" manifest = DeploymentManifest(root=Path("/var/lib/x"), entries={}) with pytest.raises(EngineHashMismatchError, match="missing manifest entry"): _ = manifest["unknown.engine"] def test_manifest_reader_rejects_malformed_json(tmp_path: Path) -> None: bad = tmp_path / "manifest.json" bad.write_text("not json {", encoding="utf-8") with pytest.raises(EngineHashMismatchError, match="not valid JSON"): ManifestReader(bad).read() def test_manifest_reader_rejects_missing_entries_key(tmp_path: Path) -> None: bad = tmp_path / "manifest.json" bad.write_text(json.dumps({"root": "/x"}), encoding="utf-8") with pytest.raises(EngineHashMismatchError, match="entries"): ManifestReader(bad).read() # ---------------------------------------------------------------------- # Engine-outside-manifest-root path. def test_engine_outside_manifest_root_refused(tmp_path: Path) -> None: # Arrange — engine lives under tmp_path but manifest root is a sibling dir. engine = _write_engine(tmp_path, "ultravpr__sm87_jp6.2_trt10.3_fp16.engine") sha = _write_sidecar(engine) sibling = tmp_path / "elsewhere" sibling.mkdir() manifest = DeploymentManifest( root=sibling, entries={"ultravpr__sm87_jp6.2_trt10.3_fp16.engine": sha}, ) gate = EngineGate() with pytest.raises(EngineHashMismatchError, match="not under manifest root"): gate.validate(_entry_for(engine), _TIER2_HOST, manifest) # ---------------------------------------------------------------------- # NFR-reliability — no writes by the gate. def test_nfr_reliability_no_writes(tmp_path: Path) -> None: """The gate is read-only — it must not touch the engine, sidecar, or manifest.""" engine = _write_engine(tmp_path, "ultravpr__sm87_jp6.2_trt10.3_fp16.engine") sha = _write_sidecar(engine) sidecar = Path(str(engine) + ".sha256") snapshot = { "engine_mtime": engine.stat().st_mtime, "engine_bytes": engine.read_bytes(), "sidecar_mtime": sidecar.stat().st_mtime, "sidecar_text": sidecar.read_text(encoding="utf-8"), } manifest = _manifest_with(engine, sha) EngineGate().validate(_entry_for(engine), _TIER2_HOST, manifest) assert engine.stat().st_mtime == snapshot["engine_mtime"] assert engine.read_bytes() == snapshot["engine_bytes"] assert sidecar.stat().st_mtime == snapshot["sidecar_mtime"] assert sidecar.read_text(encoding="utf-8") == snapshot["sidecar_text"] # ---------------------------------------------------------------------- # Refusal log on error path. def test_refusal_emits_error_log( tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: engine = _write_engine(tmp_path, "bogus.engine") gate = EngineGate() with caplog.at_level(logging.ERROR, logger="c7_inference.gate"): with pytest.raises(EngineSchemaMismatchError): gate.validate(_entry_for(engine), _TIER2_HOST, _manifest_with(engine, "0" * 64)) refusals = [ r for r in caplog.records if getattr(r, "kind", None) == "c7.gate.refuse" ] assert len(refusals) == 1 assert refusals[0].kv["step"] == "schema_parse" # type: ignore[attr-defined] # ---------------------------------------------------------------------- # AC-8 host-tuple read (Tier-2 hardware only). @pytest.mark.tier2 @pytest.mark.skipif( os.environ.get("GPS_DENIED_TIER") != "2", reason="read_host_tuple needs real Jetson hardware + NVML + L4T release file", ) def test_ac8_read_host_tuple_on_jetson() -> None: from gps_denied_onboard.components.c7_inference.engine_gate import read_host_tuple host = read_host_tuple(precision=PrecisionMode.FP16) assert host.sm == 87 assert host.jp == "6.2" assert host.trt == "10.3" assert host.precision is PrecisionMode.FP16