Files
Oleksandr Bezdieniezhnykh 5def1a3eb3 [AZ-422] Add FT-P-17 + FT-N-06 mid-flight tile blackbox tests
Implement the AC-8.4 and AC-NEW-6 blackbox scenarios for mid-flight
tile generation, dedup, landing-time upload, and freshness gating.

Helpers:
- runner/helpers/mid_flight_tile_evaluator.py — pure-logic evaluators
  for tile generation rate, Mode B Fact #105 schema check, footprint+
  GSD dedup (via geo.distance_m), upload-audit reconciliation, and
  the AC-5/AC-6 capture_utc + freshness-gate checks.
- runner/helpers/mock_suite_sat_audit.py — httpx wrapper for the
  mock-suite-sat-service /tiles/audit endpoint with strict response-
  shape validation.

Scenarios:
- tests/positive/test_ft_p_17_mid_flight_tiles.py
- tests/negative/test_ft_n_06_mid_flight_freshness.py

Both skip when sitl_replay_ready is false and fail loudly when fixture
records are missing (tests-as-gates discipline). 52 new unit tests
(41 evaluator + 11 audit client) cover every helper branch.

Review: PASS_WITH_WARNINGS (2 Low — duplicate haversine carry-over,
upstream production dependency surface).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 15:28:39 +03:00

77 lines
2.7 KiB
Python

"""HTTP client for the mock-suite-sat-service audit log.
Thin wrapper over ``GET /tiles/audit`` (and its alias ``GET /mock/audit``)
that the FT-P-17 scenario uses to verify every generated tile was
accepted with HTTP 202 at landing-time upload.
Reading the audit log is a one-shot, end-of-run operation, so the
helper is synchronous httpx — no streaming, no retries (the service is
co-located in the compose harness and a failure to reach it is itself
a test failure, not something to paper over).
"""
from __future__ import annotations
from typing import Any
import httpx
DEFAULT_TIMEOUT_S = 10.0
DEFAULT_AUDIT_PATH = "/tiles/audit"
def fetch_audit(
base_url: str,
run_id: str,
*,
timeout_s: float = DEFAULT_TIMEOUT_S,
audit_path: str = DEFAULT_AUDIT_PATH,
transport: httpx.BaseTransport | None = None,
) -> list[dict[str, Any]]:
"""Return the ``entries`` list from the mock-suite-sat-service audit log.
The endpoint returns ``{"run_id": str, "entries": [...]}``; this
helper unwraps the list. An empty list is a legal response (the
SUT may not have uploaded anything yet).
Raises ``RuntimeError`` on non-2xx HTTP status or a malformed
response shape — the scenario test wants those to fail loudly.
``transport`` is a unit-test seam: pass an
``httpx.MockTransport`` to feed canned responses without spinning
up the real service.
"""
if not base_url:
raise RuntimeError("fetch_audit: base_url must be a non-empty string")
if not run_id:
raise RuntimeError("fetch_audit: run_id must be a non-empty string")
url = base_url.rstrip("/") + audit_path
client_kwargs: dict[str, Any] = {"timeout": timeout_s}
if transport is not None:
client_kwargs["transport"] = transport
with httpx.Client(**client_kwargs) as client:
resp = client.get(url, params={"run_id": run_id})
if resp.status_code >= 300:
raise RuntimeError(
f"fetch_audit: {url}?run_id={run_id} returned HTTP "
f"{resp.status_code}: body={resp.text[:200]!r}"
)
try:
body = resp.json()
except ValueError as exc:
raise RuntimeError(
f"fetch_audit: {url}?run_id={run_id} body is not valid JSON: {exc}"
) from exc
if not isinstance(body, dict):
raise RuntimeError(
f"fetch_audit: {url}?run_id={run_id} body is not a JSON object: "
f"got {type(body).__name__}"
)
entries = body.get("entries")
if not isinstance(entries, list):
raise RuntimeError(
f"fetch_audit: {url}?run_id={run_id} body missing 'entries' list: "
f"keys={list(body.keys())}"
)
return entries