"""FT-P-16 — Offline-only operation (AZ-421 / AC-8.3, RESTRICT-SAT-1). The full scenario: 1. The SUT runs against the local tile-cache mount only. 2. The Docker compose harness attaches the SUT container to ``e2e-net`` with ``Internal: true`` — Docker itself blocks egress to anything outside that network (AZ-406 owns the compose wiring). 3. A 60 s Derkachi replay generates load; during the replay the scenario reads ``docker network inspect e2e-net`` and ``docker inspect `` and asserts: - ``e2e-net.Internal == true`` - The SUT container is attached to ``e2e-net`` only. The "0 packets to non-e2e-net destinations" semantic of AC-8.3 is enforced structurally — there is no other network the SUT can reach, so the packet count is provably 0 without per-packet counters. Gated on: * ``sitl_replay_ready`` — full replay needs the SITL fixture (skip cleanly otherwise). * ``DOCKER_NETWORK_INSPECT_PATH`` / ``DOCKER_CONTAINER_INSPECT_PATH`` env vars — point at JSON files produced by the fixture builder ahead of test invocation. When unset, the scenario skips with a clear reason (the docker CLI is not available inside the runner container without volume-mounting the docker socket; the fixture builder snapshots the inspect output instead). * ``runner.helpers.tile_cache_inspector.evaluate_offline_mode`` — pure-logic evaluator covered by ``e2e/_unit_tests/helpers/test_tile_cache_inspector.py``. """ from __future__ import annotations import json import os from pathlib import Path import pytest from runner.helpers import tile_cache_inspector as tci DOCKER_NETWORK_INSPECT_ENV = "DOCKER_NETWORK_INSPECT_PATH" DOCKER_CONTAINER_INSPECT_ENV = "DOCKER_CONTAINER_INSPECT_PATH" @pytest.mark.traces_to("AC-8.3,AC-3,AC-6,RESTRICT-SAT-1") def test_ft_p_16_offline_only( fc_adapter: str, vio_strategy: str, evidence_dir, # type: ignore[no-untyped-def] run_id: str, nfr_recorder, # type: ignore[no-untyped-def] sitl_replay_ready: bool, ) -> None: """Full FT-P-16 scenario (AC-8.3 / RESTRICT-SAT-1).""" if not sitl_replay_ready: pytest.skip( "FT-P-16 needs `E2E_SITL_REPLAY_DIR` to point at a SITL replay " "fixture (AZ-595). Pure-logic AC-8.3 coverage lives in " "e2e/_unit_tests/helpers/test_tile_cache_inspector.py." ) net_path = os.environ.get(DOCKER_NETWORK_INSPECT_ENV) ctr_path = os.environ.get(DOCKER_CONTAINER_INSPECT_ENV) if not net_path or not ctr_path: pytest.skip( f"FT-P-16 needs `{DOCKER_NETWORK_INSPECT_ENV}` and " f"`{DOCKER_CONTAINER_INSPECT_ENV}` env vars set to JSON files " "produced by the compose harness (`docker network inspect " "e2e-net` + `docker inspect gps-denied-onboard`). The fixture " "builder snapshots both before the test runs." ) net_inspect = _load_docker_inspect_object(Path(net_path), kind="network") ctr_inspect = _load_docker_inspect_object(Path(ctr_path), kind="container") report = tci.evaluate_offline_mode(net_inspect, ctr_inspect) nfr_recorder.record_metric( "ft_p_16.network_internal", 1.0 if report.network_internal else 0.0, ac_id="AC-8.3" ) nfr_recorder.record_metric( "ft_p_16.container_network_count", float(len(report.container_networks)), ac_id="AC-3" ) assert report.passes, ( "AC-8.3 (offline-only operation) failed: " f"network_internal={report.network_internal}, " f"container_networks={report.container_networks}, " f"expected_network={report.expected_network}" ) def _load_docker_inspect_object(path: Path, *, kind: str) -> dict: """Load a single inspect object from a JSON file. ``docker inspect`` returns a JSON array. The scenario expects either the wrapped array OR an unwrapped single-object payload — accept both shapes for forwards-compatibility with fixture builders that pre-unwrap. """ if not path.exists(): pytest.fail(f"FT-P-16: {kind} inspect JSON not found at {path}") raw = json.loads(path.read_text(encoding="utf-8")) if isinstance(raw, list): if not raw: pytest.fail(f"FT-P-16: {kind} inspect JSON at {path} is an empty array") if not isinstance(raw[0], dict): pytest.fail( f"FT-P-16: {kind} inspect JSON at {path} array element is not an object" ) return raw[0] if isinstance(raw, dict): return raw pytest.fail( f"FT-P-16: {kind} inspect JSON at {path} is neither object nor array: " f"type={type(raw).__name__}" ) return {} # unreachable; pytest.fail raises