from __future__ import annotations import json from pathlib import Path from k1link.perception.contracts import LocalObstacleMap, SourceAccounting, ThreatAssessment from k1link.perception.graph_contracts import ( REFERENCE_GRAPH_ID_V2, DeliveredFrame, GraphRunMode, GraphState, TerminalOutcome, TerminalOutcomeType, build_graph_run_result_v2, ) from k1link.perception.reference_graph_parity import ( compare_reference_graph_to_accepted_ledgers, ) def _result(threats: tuple[ThreatAssessment, ...] = ()): obstacle_map = LocalObstacleMap( source_id="RAVNOVES00", session_id="20260720T065719Z_viewer_live", frame_id="frame-000000", graph_id=REFERENCE_GRAPH_ID_V2, generated_monotonic_ns=1, output_age_ns=0, occupied=(), unknown=(), camera_uncertainty=(), accounting=SourceAccounting(1, 1, 0, 0), ) return build_graph_run_result_v2( graph_id=REFERENCE_GRAPH_ID_V2, source_profile_id="m4-ravnoves00-recorded-realtime/v1", run_mode=GraphRunMode.LOSSLESS_REPLAY, state=GraphState.STOPPED, admitted_count=1, outcomes=( TerminalOutcome( source_id="RAVNOVES00", session_id="20260720T065719Z_viewer_live", frame_id="frame-000000", sequence=0, outcome=TerminalOutcomeType.DELIVERED, stage_id="threat", reason="object-payload-delivered", ), ), deliveries=(DeliveredFrame(0, obstacle_map, threats),), queue_high_watermarks=(("detector", 1),), ) def _write_ledgers(root: Path, *, threat_assessments: list[dict[str, object]]) -> tuple[Path, Path]: temporal = root / "temporal.jsonl" threat = root / "threat.jsonl" temporal.write_text( json.dumps( { "sequence": 0, "frame_id": "frame-000000", "current": [], "rolling_retained": [], "held": [], "expired": [], } ) + "\n", "utf-8", ) threat.write_text( json.dumps( { "sequence": 0, "frame_id": "frame-000000", "camera_proposals": [], "assessments": threat_assessments, } ) + "\n", "utf-8", ) return temporal, threat def test_reference_graph_parity_accepts_exact_frame_semantics(tmp_path: Path) -> None: temporal, threat = _write_ledgers(tmp_path, threat_assessments=[]) report = compare_reference_graph_to_accepted_ledgers( _result(), temporal_frames_path=temporal, threat_frames_path=threat, expected_frames=1, ) assert report.accepted is True assert dict(report.mismatch_counts) == { "camera_uncertainty": 0, "current": 0, "expired": 0, "held": 0, "rolling_retained": 0, "source_binding": 0, "threat_assessments": 0, } def test_reference_graph_parity_reports_threat_drift(tmp_path: Path) -> None: temporal, threat = _write_ledgers(tmp_path, threat_assessments=[{"component_id": "x"}]) report = compare_reference_graph_to_accepted_ledgers( _result(), temporal_frames_path=temporal, threat_frames_path=threat, expected_frames=1, ) assert report.accepted is False assert dict(report.mismatch_counts)["threat_assessments"] == 1 def test_reference_graph_parity_treats_assessment_order_as_nonsemantic( tmp_path: Path, ) -> None: first = { "schema_version": "missioncore.threat-assessment/v1", "assessment_id": "threat-first", "component_id": "component-first", "rig_profile_id": "rig-test", "corridor_profile_id": "corridor-test", "qualification": "unqualified", "relative_speed_mps": None, "closest_approach_m": None, "ttc_seconds": None, "corridor_intersection": "unknown", "decision": "unknown", "reason_codes": ["test-unavailable"], "authority": "replay-simulated", "physical_collision_accepted": False, "actuation_allowed": False, } second = { **first, "assessment_id": "threat-second", "component_id": "component-second", } temporal, threat = _write_ledgers( tmp_path, threat_assessments=[second, first], ) report = compare_reference_graph_to_accepted_ledgers( _result((ThreatAssessment.from_dict(first), ThreatAssessment.from_dict(second))), temporal_frames_path=temporal, threat_frames_path=threat, expected_frames=1, ) assert report.accepted is True