212 lines
7.1 KiB
Python
212 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import ast
|
|
import copy
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from k1link.perception.baseline import (
|
|
BaselineContractError,
|
|
load_m4_baseline,
|
|
validate_reuse_inventory,
|
|
verify_m4_baseline,
|
|
verify_m4_execution_source,
|
|
)
|
|
|
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
|
PERCEPTION_ROOT = REPOSITORY_ROOT / "src" / "k1link" / "perception"
|
|
BASELINE_PATH = REPOSITORY_ROOT / "config" / "perception" / "m4-recorded-realtime-baseline-v1.json"
|
|
REUSE_PATH = REPOSITORY_ROOT / "config" / "perception" / "m4-reuse-inventory-v1.json"
|
|
DETECTOR_RUNTIME_MODULES = (
|
|
"__init__.py",
|
|
"baseline.py",
|
|
"contracts.py",
|
|
"detector.py",
|
|
"detector_replay.py",
|
|
"detector_replay_cli.py",
|
|
"detector_replay_contracts.py",
|
|
"detector_replay_result.py",
|
|
"detector_replay_validation.py",
|
|
"providers.py",
|
|
"recorded_source.py",
|
|
"yolox_object_detector.py",
|
|
)
|
|
|
|
|
|
def _imports(path: Path) -> set[str]:
|
|
tree = ast.parse(path.read_text("utf-8"), filename=str(path))
|
|
modules: set[str] = set()
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.Import):
|
|
modules.update(alias.name for alias in node.names)
|
|
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
modules.add(node.module)
|
|
return modules
|
|
|
|
|
|
def test_m4_baseline_is_exact_and_every_local_evidence_digest_resolves() -> None:
|
|
profile = load_m4_baseline(BASELINE_PATH)
|
|
verification = verify_m4_baseline(REPOSITORY_ROOT, profile)
|
|
assert verification.source_id == "RAVNOVES00"
|
|
assert verification.session_id == "20260720T065719Z_viewer_live"
|
|
assert len(verification.verified_paths) == 10
|
|
|
|
|
|
def test_worker_execution_source_verification_needs_no_historical_lab_tree() -> None:
|
|
profile = load_m4_baseline(BASELINE_PATH)
|
|
camera_root = (
|
|
REPOSITORY_ROOT
|
|
/ ".runtime/compute-jobs/recorded-camera-602ac89026ed12978619801d"
|
|
/ "input/camera/sensor.camera.right/epoch-1"
|
|
)
|
|
pack_root = (
|
|
REPOSITORY_ROOT
|
|
/ ".runtime/compute-experiments/e10/lidar-packs"
|
|
/ "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
|
|
)
|
|
|
|
verified = verify_m4_execution_source(
|
|
profile,
|
|
camera_summary_path=camera_root / "summary.json",
|
|
camera_index_path=camera_root / "index.jsonl",
|
|
source_pack_manifest_path=pack_root / "manifest.json",
|
|
source_pack_path=pack_root / "lidar-pack.npz",
|
|
)
|
|
|
|
assert tuple(path.name for path in verified) == (
|
|
"summary.json",
|
|
"index.jsonl",
|
|
"manifest.json",
|
|
"lidar-pack.npz",
|
|
)
|
|
|
|
|
|
def test_worker_execution_source_rejects_rewritten_manifest_bytes(tmp_path: Path) -> None:
|
|
profile = load_m4_baseline(BASELINE_PATH)
|
|
camera_root = (
|
|
REPOSITORY_ROOT
|
|
/ ".runtime/compute-jobs/recorded-camera-602ac89026ed12978619801d"
|
|
/ "input/camera/sensor.camera.right/epoch-1"
|
|
)
|
|
pack_root = (
|
|
REPOSITORY_ROOT
|
|
/ ".runtime/compute-experiments/e10/lidar-packs"
|
|
/ "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b"
|
|
)
|
|
rewritten_manifest = tmp_path / "manifest.json"
|
|
document = json.loads((pack_root / "manifest.json").read_text("utf-8"))
|
|
rewritten_manifest.write_text(json.dumps(document), "utf-8")
|
|
|
|
with pytest.raises(BaselineContractError, match="manifest file digest"):
|
|
verify_m4_execution_source(
|
|
profile,
|
|
camera_summary_path=camera_root / "summary.json",
|
|
camera_index_path=camera_root / "index.jsonl",
|
|
source_pack_manifest_path=rewritten_manifest,
|
|
source_pack_path=pack_root / "lidar-pack.npz",
|
|
)
|
|
|
|
|
|
def test_m4_baseline_cannot_silently_select_another_source(tmp_path: Path) -> None:
|
|
document = json.loads(BASELINE_PATH.read_text("utf-8"))
|
|
incompatible = copy.deepcopy(document)
|
|
incompatible["source"]["source_id"] = "RAVNOVES01"
|
|
path = tmp_path / "baseline.json"
|
|
path.write_text(json.dumps(incompatible), "utf-8")
|
|
with pytest.raises(BaselineContractError, match="RAVNOVES00"):
|
|
load_m4_baseline(path)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("section", "key", "value", "message"),
|
|
(
|
|
("calibration", "valid_fov_mask_sha256", "0" * 64, "calibration"),
|
|
("detector", "minimum_score", 0.51, "detector"),
|
|
("rollback", "worker_node", "worker-007", "rollback"),
|
|
),
|
|
)
|
|
def test_m4_baseline_cannot_silently_tune_frozen_execution_identity(
|
|
tmp_path: Path,
|
|
section: str,
|
|
key: str,
|
|
value: object,
|
|
message: str,
|
|
) -> None:
|
|
document = json.loads(BASELINE_PATH.read_text("utf-8"))
|
|
document[section][key] = value
|
|
path = tmp_path / "baseline.json"
|
|
path.write_text(json.dumps(document), "utf-8")
|
|
with pytest.raises(BaselineContractError, match=message):
|
|
load_m4_baseline(path)
|
|
|
|
|
|
def test_reuse_inventory_separates_primitives_from_historical_wrappers() -> None:
|
|
document = validate_reuse_inventory(REUSE_PATH)
|
|
assert document["rules"]["bulk_legacy_migration_required"] is False
|
|
|
|
|
|
def test_perception_contracts_import_no_compute_lab_graph_or_web_module() -> None:
|
|
imports = _imports(PERCEPTION_ROOT / "contracts.py")
|
|
forbidden = {
|
|
module
|
|
for module in imports
|
|
if module.startswith(
|
|
(
|
|
"k1link.compute",
|
|
"k1link.laboratory",
|
|
"k1link.web",
|
|
"k1link.perception.providers",
|
|
"k1link.perception.graph",
|
|
)
|
|
)
|
|
}
|
|
assert forbidden == set()
|
|
|
|
|
|
def test_perception_package_initializer_is_side_effect_free() -> None:
|
|
assert _imports(PERCEPTION_ROOT / "__init__.py") == set()
|
|
|
|
|
|
def test_detector_runtime_closure_imports_no_legacy_compute_package() -> None:
|
|
violations = {
|
|
name: sorted(
|
|
module
|
|
for module in _imports(PERCEPTION_ROOT / name)
|
|
if module.startswith("k1link.compute")
|
|
)
|
|
for name in DETECTOR_RUNTIME_MODULES
|
|
}
|
|
assert {name: modules for name, modules in violations.items() if modules} == {}
|
|
|
|
|
|
def test_new_perception_boundary_has_no_experiment_specific_imports() -> None:
|
|
violations: dict[str, str] = {}
|
|
for path in PERCEPTION_ROOT.glob("*.py"):
|
|
for module in _imports(path):
|
|
leaf = module.rsplit(".", 1)[-1]
|
|
if module.startswith("k1link.compute") and (
|
|
leaf.startswith("e") or leaf.startswith("l")
|
|
):
|
|
violations[path.name] = module
|
|
assert violations == {}
|
|
|
|
|
|
def test_product_perception_imports_only_admitted_compute_primitives() -> None:
|
|
inventory = validate_reuse_inventory(REUSE_PATH)
|
|
admitted = {
|
|
item["module"]
|
|
for item in inventory["reusable_primitives"]
|
|
if isinstance(item, dict) and isinstance(item.get("module"), str)
|
|
}
|
|
violations: dict[str, set[str]] = {}
|
|
for path in PERCEPTION_ROOT.glob("*.py"):
|
|
compute_imports = {
|
|
module for module in _imports(path) if module.startswith("k1link.compute")
|
|
}
|
|
unadmitted = compute_imports - admitted
|
|
if unadmitted:
|
|
violations[path.name] = unadmitted
|
|
assert violations == {}
|