"""FT-P-15 — Tile cache manifest schema + resolution floor (AZ-421 / AC-8.1). The full scenario: 1. SUT cold-starts against the bind-mounted ``tile-cache-fixture`` and emits a one-time ``cache-self-check`` FDR record carrying every manifest entry it loaded (CRS, tile_matrix, dimension, m_per_px, capture_date, source, compression). 2. The SUT additionally emits ``tile-load-rejected`` FDR records for any entry the freshness/floor gate rejected at load time. 3. The test parses the FDR archive, evaluates the manifest schema contract (AC-1: every required field present; AC-2: every entry either ≥ 0.5 m/px or rejected), and asserts the report passes. AC-1: every required field present per entry — ``MANIFEST_REQUIRED_FIELDS``. AC-2: m/px ≥ 0.5 OR rejected by FDR ``tile-load-rejected``. AC-3 of FT-P-15-spec maps to AC-6 of the task (parameterisation). Gated on: * ``runner.helpers.fdr_reader`` — owned by AZ-594; present. * ``runner.helpers.tile_cache_inspector.evaluate_manifest_schema`` — pure-logic evaluator covered by ``e2e/_unit_tests/helpers/test_tile_cache_inspector.py``. * ``sitl_replay_ready`` — skip-gates the scenario when no FDR archive is present locally. """ from __future__ import annotations from pathlib import Path import pytest from runner.helpers import tile_cache_inspector as tci @pytest.mark.traces_to("AC-8.1,AC-1,AC-2,AC-6") def test_ft_p_15_cache_schema( 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-15 scenario (AC-8.1).""" if not sitl_replay_ready: pytest.skip( "FT-P-15 requires `E2E_SITL_REPLAY_DIR` to point at a SITL replay " "fixture that includes the FDR `cache-self-check` record + any " "`tile-load-rejected` records (AZ-595 + AZ-421 fixture builder). " "Pure-logic AC-8.1 coverage lives in " "e2e/_unit_tests/helpers/test_tile_cache_inspector.py." ) from runner.helpers import fdr_reader fdr_root = Path(evidence_dir).parent / f"run-{run_id}" / "fdr" manifest_entries: list[dict] = [] rejected_ids: list[str] = [] for rec in fdr_reader.iter_records(fdr_root): if rec.record_type == tci.CACHE_SELF_CHECK_FDR_KIND: raw_entries = rec.payload.get("entries") if isinstance(raw_entries, list): for entry in raw_entries: if isinstance(entry, dict): manifest_entries.append(entry) elif rec.record_type == tci.TILE_LOAD_REJECTED_FDR_KIND: entry_id = rec.payload.get("id") or rec.payload.get("tile_id") if isinstance(entry_id, str) and entry_id: rejected_ids.append(entry_id) if not manifest_entries: pytest.fail( f"FT-P-15: no `{tci.CACHE_SELF_CHECK_FDR_KIND}` FDR record with " f"manifest entries found under {fdr_root}. The fixture builder " "must emit one at cold start." ) report = tci.evaluate_manifest_schema( manifest_entries, tile_load_rejected_ids=rejected_ids, ) nfr_recorder.record_metric( "ft_p_15.manifest_entries", float(report.total_entries), ac_id="AC-8.1" ) nfr_recorder.record_metric( "ft_p_15.entries_missing_fields", float(len(report.entries_with_missing_fields)), ac_id="AC-1", ) nfr_recorder.record_metric( "ft_p_15.entries_below_floor", float(len(report.entries_below_floor)), ac_id="AC-2", ) assert report.passes, ( "AC-8.1 (manifest schema + ≥0.5 m/px floor) failed: " f"total={report.total_entries}, " f"missing_fields={[(e.entry_id, e.missing_fields) for e in report.entries_with_missing_fields]}, " f"below_floor_not_rejected=" f"{[e.entry_id for e in report.entries_below_floor if e.entry_id not in report.rejected_below_floor_ids]}" )