from pathlib import Path import sys PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from target_handoff import ( HandoffDecision, compute_fast_handoff_hits, evaluate_stale_lock, pick_guidance_override_box, should_override_guidance, update_guidance_override_latch, ) def test_evaluate_stale_lock_requires_fresh_candidate_and_weak_anchor(): decision = evaluate_stale_lock( confirmed=True, have_fresh_candidate=True, target_has_fresh_update=False, candidate_dist_px=420.0, pred_diag_px=70.0, miss_streak=8, klt_valid=False, klt_quality=0.18, klt_iou=0.12, candidate_motion_ok=True, target_motion_ok=False, stale_break_dist_diag=3.5, stale_break_min_miss=2, stale_break_klt_quality=0.35, stale_break_klt_iou=0.25, ) assert decision.stale_lock_active is True assert "target_not_fresh" in decision.reasons assert "weak_klt_quality" in decision.reasons def test_evaluate_stale_lock_rejects_close_candidate_when_anchor_is_healthy(): decision = evaluate_stale_lock( confirmed=True, have_fresh_candidate=True, target_has_fresh_update=True, candidate_dist_px=90.0, pred_diag_px=70.0, miss_streak=0, klt_valid=True, klt_quality=0.92, klt_iou=0.71, candidate_motion_ok=True, target_motion_ok=True, stale_break_dist_diag=3.5, stale_break_min_miss=2, stale_break_klt_quality=0.35, stale_break_klt_iou=0.25, ) assert decision.stale_lock_active is False assert decision.reasons == [] def test_compute_fast_handoff_hits_reduces_confirmation_under_stale_lock(): hits = compute_fast_handoff_hits( stale_lock_active=True, motion_switch_mode=False, approach_extra_hits=2, fast_maneuver_extra_hits=2, default_switch_hits=4, fast_handoff_hits=2, motion_switch_hits=6, ) assert hits == 2 def test_handoff_decision_repr_is_stable_for_logging(): decision = HandoffDecision( stale_lock_active=True, allow_fast_handoff=True, required_switch_hits=2, disable_prediction_hold=True, reasons=["target_not_fresh", "weak_klt_quality"], ) assert decision.reason_text() == "target_not_fresh|weak_klt_quality" def test_should_override_guidance_when_target_is_missing_but_fresh_candidate_exists(): assert should_override_guidance( confirmed=True, target_track_missing=True, have_fresh_candidate=True, candidate_matches_target=False, miss_streak=10, override_miss_ge=2, ) def test_should_not_override_guidance_before_miss_threshold(): assert not should_override_guidance( confirmed=True, target_track_missing=True, have_fresh_candidate=True, candidate_matches_target=False, miss_streak=1, override_miss_ge=2, ) def test_pick_guidance_override_box_falls_back_to_raw_yolo_box(): yolo_box = [100.0, 120.0, 180.0, 200.0] picked = pick_guidance_override_box( track_candidate_box=None, raw_yolo_box=yolo_box, ) assert picked == yolo_box def test_update_guidance_override_latch_keeps_recent_box_when_new_candidate_is_missing(): prev_box = [100.0, 120.0, 180.0, 200.0] latched_box, ttl = update_guidance_override_latch( new_box=None, prev_box=prev_box, prev_ttl=2, max_ttl=2, ) assert latched_box == prev_box assert ttl == 1 def test_update_guidance_override_latch_replaces_memory_when_new_candidate_arrives(): new_box = [300.0, 320.0, 380.0, 400.0] latched_box, ttl = update_guidance_override_latch( new_box=new_box, prev_box=None, prev_ttl=0, max_ttl=2, ) assert latched_box == new_box assert ttl == 2