"""Phase 1 platform foundation tests — metrics, container quality, compare, gates, evidence.""" from __future__ import annotations import json from pathlib import Path import pytest from sensorflow.platform.metrics_engine import ( aggregate_container_rows, container_quality_metrics, delta_metrics, precision_recall_f1, verification_rates, ) from sensorflow.platform.container_quality import profile_from_counts from sensorflow.platform.levels import AggregateLevel, EvaluationScope, summarize_scope from sensorflow.platform.gates import ( DEFAULT_GATE_CONFIG, evaluate_multi_gates, list_gate_defs, load_gate_config, ) from sensorflow.platform.evidence import build_evidence_package, export_evidence_package from sensorflow.platform.compare import compare_models from sensorflow.platform.entities import EvidencePackage, Provenance # ------------------------------------------------------------------ metrics def test_precision_recall_f1_basic(): out = precision_recall_f1(tp=8, fp=2, fn=2) assert out["precision"] == 0.8 assert out["recall"] == 0.8 assert out["f1"] == 0.8 def test_precision_recall_empty(): out = precision_recall_f1(0, 0, 0) assert out["precision"] is None assert out["recall"] is None assert out["f1"] is None def test_verification_rates(): v = verification_rates( n_objects=100, verified=40, reviewed=55, disputed=5, auto_accepted=30, hitl=15, ) assert v["verified_rate"] == 0.4 assert v["hitl_rate"] == 0.15 assert v["disputed_rate"] == 0.05 assert v["auto_accept_rate"] == 0.3 assert v["unverified"] == 60 def test_container_quality_metrics_bundle(): m = container_quality_metrics( tp=10, fp=5, fn=5, sum_iou=7.0, n_objects=20, verified=8, reviewed=12, hitl=4, anomalies=2, ) assert m["precision"] == pytest.approx(10 / 15, rel=1e-3) assert m["mean_iou"] == 0.7 assert m["verification"]["verified_rate"] == 0.4 assert m["anomaly_rate"] == 0.1 def test_aggregate_container_rows(): rows = [ {"tp": 5, "fp": 1, "fn": 1, "sum_iou": 4.0, "n_objects": 10, "verified": 3, "reviewed": 4}, {"tp": 5, "fp": 1, "fn": 1, "sum_iou": 3.5, "n_objects": 10, "verified": 5, "reviewed": 6}, ] agg = aggregate_container_rows(rows) assert agg["tp"] == 10 assert agg["verification"]["verified"] == 8 def test_delta_metrics(): deltas = delta_metrics( {"precision": 0.9, "recall": 0.8}, {"precision": 0.88, "recall": 0.85}, keys=["precision", "recall"], ) assert deltas[0]["delta"] == pytest.approx(-0.02) assert deltas[1]["delta"] == pytest.approx(0.05) def test_profile_from_counts(): p = profile_from_counts( container_id="c-1", tp=4, fp=1, fn=1, sum_iou=3.2, n_objects=10, verified=2, reviewed=5, hitl=3, ) assert p["container_id"] == "c-1" assert p["f1"] is not None assert p["verification"]["hitl"] == 3 # ------------------------------------------------------------------ levels def test_aggregate_ladder(): ladder = AggregateLevel.ladder() assert ladder[0] == AggregateLevel.FRAME assert ladder[-1] == AggregateLevel.POPULATION assert AggregateLevel.CONTAINER.parent() == AggregateLevel.DATASET assert AggregateLevel.FRAME.child() is None def test_evaluation_scope_summary(): scope = EvaluationScope( level=AggregateLevel.CONTAINER, run_id="run-1", mega_container_id=42, model_version="model-v1", ) s = summarize_scope(scope) assert s["backend"] == "megaeval" assert s["refs"]["mega_container_id"] == 42 # ------------------------------------------------------------------ gates def test_gate_defs_include_multi_gate_skeleton(tmp_path: Path): cfg_path = tmp_path / "gates.json" cfg = load_gate_config(cfg_path) assert cfg_path.exists() defs = list_gate_defs(cfg) types = {d.gate_type for d in defs} for required in ("scenario", "coverage", "regression", "safety", "release", "quality", "launch"): assert required in types def test_evaluate_multi_gates_skeleton(tmp_path: Path, monkeypatch): cfg_path = tmp_path / "gates.json" monkeypatch.setattr( "sensorflow.platform.gates.DEFAULT_GATE_CONFIG_PATH", cfg_path ) # rewrite load to use tmp from sensorflow.platform import gates as gates_mod monkeypatch.setattr(gates_mod, "DEFAULT_GATE_CONFIG_PATH", cfg_path) out = gates_mod.evaluate_multi_gates() assert out["summary"]["unwired_count"] >= 3 assert out["summary"]["release_ready"] is False types = [g["gate_type"] for g in out["gates"]] assert "scenario" in types and "safety" in types skeleton = [g for g in out["gates"] if g["gate_type"] == "safety"][0] assert skeleton["ready"] is False assert skeleton["passed"] is None def test_gate_config_thresholds_are_config_driven(): assert "map_3d" in DEFAULT_GATE_CONFIG["gates"]["quality"]["thresholds"] assert DEFAULT_GATE_CONFIG["gates"]["safety"]["ready"] is False # ------------------------------------------------------------------ evidence def test_evidence_package_stub(tmp_path: Path, monkeypatch): monkeypatch.setattr( "sensorflow.platform.evidence.EVIDENCE_DIR", tmp_path ) pkg = build_evidence_package(evaluation_id="eval-test") assert isinstance(pkg, EvidencePackage) assert pkg.evaluation_id == "eval-test" assert "model_versions" in pkg.placeholders or pkg.model_versions path = export_evidence_package(pkg, tmp_path / f"{pkg.package_id}.json") assert path.exists() raw = json.loads(path.read_text()) assert raw["evaluation_id"] == "eval-test" assert "gate_results" in raw assert "compute_usage" in raw assert "provenance" in raw # append-only: second export with same path gets a revision file path2 = export_evidence_package(pkg, path) assert path2 != path assert path2.exists() # ------------------------------------------------------------------ compare (unit, no mega store) def test_compare_models_requires_two_runs(): with pytest.raises(ValueError): compare_models(["only-one"]) class _FakeRun: def __init__(self, run_id, model_version, headline, status="published"): self.run_id = run_id self.model_version = model_version self.headline = headline self.status = status self.population_id = "pop-1" self.label_version = "labels-v1" class _FakeStore: def __init__(self, runs): self.runs = {r.run_id: r for r in runs} def artifacts(self, run_id): raise NotImplementedError def test_compare_models_matrix(monkeypatch): a = _FakeRun("run-a", "model-A", { "precision": 0.9, "recall": 0.8, "f1": 0.85, "mean_iou": 0.7, "safety_recall": 0.75, "anomaly_rate": 0.1, }) b = _FakeRun("run-b", "model-B", { "precision": 0.88, "recall": 0.82, "f1": 0.85, "mean_iou": 0.71, "safety_recall": 0.74, "anomaly_rate": 0.09, }) c = _FakeRun("run-c", "model-C", { "precision": 0.91, "recall": 0.79, "f1": 0.845, "mean_iou": 0.69, "safety_recall": 0.78, "anomaly_rate": 0.11, }) store = _FakeStore([a, b, c]) def fake_compare(store, cand, base, policy=None): return { "recommendation": "PROMOTE", "blockers": [], "headline_deltas": [], "per_class": [], "regressions": [], } monkeypatch.setattr("sensorflow.megaeval.analysis.compare_runs", fake_compare) out = compare_models( ["run-a", "run-b", "run-c"], baseline_run_id="run-a", store=store, ) assert len(out["models"]) == 3 assert len(out["pairwise"]) == 2 assert len(out["metric_matrix"]) >= 3 recall_row = next(r for r in out["metric_matrix"] if r["metric"] == "recall") assert recall_row["model-B"] == 0.82 assert recall_row["deltas_vs_baseline"]["model-B"] == pytest.approx(0.02) # ------------------------------------------------------------------ API smoke (optional FastAPI) def test_platform_router_mounts(): from fastapi.testclient import TestClient from sensorflow.platform.api import router from fastapi import FastAPI app = FastAPI() app.include_router(router) client = TestClient(app) levels = client.get("/api/evaluations/levels") assert levels.status_code == 200 body = levels.json() assert "population" in body["levels"] assert body["backends"]["container"] == "megaeval" gates = client.get("/api/gates") assert gates.status_code == 200 assert any(g["gate_type"] == "scenario" for g in gates.json()["gates"]) status = client.get("/api/gates/status") assert status.status_code == 200 assert status.json()["summary"]["release_ready"] is False evidence = client.post("/api/evaluations/evidence", json={ "evaluation_id": "api-eval-1", "persist": False, }) assert evidence.status_code == 200 assert evidence.json()["package"]["evaluation_id"] == "api-eval-1" bad = client.post("/api/models/compare", json={"run_ids": ["x"]}) assert bad.status_code == 422 or bad.status_code == 400