""" server-26#115 — two consensus-quality fixes. Fix 1 (routers/upload.py): when the cheap LLM says `orphan`, the rules engine says `new`, and the call is genuinely SUBSTANCELESS (routine severity, no vehicle/geocode/tag, and no incident already running on the same talkgroup), resolve to `orphan` and DO NOT pay for the smart tiebreaker. Radio housekeeping (unit check-ins, roll call, 10-8/10-98) was being promoted to incidents because the tiebreaker rubber-stamped the rules `new` ~21/21 of the time (CORRELATION_REVIEW_0907b.md). The substance test runs against `ctx` (fully populated at preview time), NOT against `rules_decision["corr_debug"]` — that dict is EMPTY at preview time for action=="new" (corr_path:"new" is written at APPLY time), so the first version of this gate fired on real events (a `major` "extinguishing fire", geocoded calls, pursuit updates). Fix 2 (incident_correlator.py): the `location` correlation path linked on a bare sub-`location_proximity_km` (0.5 km) distance alone, taking whichever incident came first in an unsorted `recent`. In a dense village two unrelated events routinely geocode that close. A `location` link now needs unit overlap with the candidate OR a distance under a tighter bar, and picks the NEAREST qualifying candidate. A unit-overlap location link is tagged `location_unit_overlap` so it does not merge into the fast path's bucket in the admin fit-signal histogram. """ from datetime import datetime, timedelta, timezone from unittest.mock import AsyncMock, patch import pytest from app.routers import upload from app.internal.incident_correlator import _run_decision, has_event_substance NOW = datetime(2026, 9, 7, 21, 30, 0, tzinfo=timezone.utc) # ───────────────────────────────────────────────────────────────────────────── # Fix 1 — the LLM-orphan gate in _correlate_with_consensus # ───────────────────────────────────────────────────────────────────────────── def _preview(action, corr_debug=None, ctx=None): base_ctx = {"call_id": "call-1"} if ctx: base_ctx.update(ctx) return { "decision": { "action": action, "matched_incident": None, "incident_type": "other" if action == "new" else None, "corr_debug": {} if corr_debug is None else dict(corr_debug), }, "ctx": base_ctx, } def _llm(action, reasoning="—"): md = {"incident_id": "inc-1"} if action == "link" else None return {"action": action, "matched_incident": md, "reasoning": reasoning} async def _run_consensus(preview, llm_decision): tiebreak_result = { "action": "new", "matched_incident": None, "incident_type": "other", "corr_debug": {}, "reasoning": "tb", } with patch("app.internal.incident_correlator.preview_correlation", new=AsyncMock(return_value=preview)), \ patch("app.internal.incident_correlator.apply_correlation", new=AsyncMock(return_value="incident-x")) as m_apply, \ patch("app.internal.llm_correlator.decide", new=AsyncMock(return_value=llm_decision)), \ patch("app.internal.llm_correlator.tiebreak", new=AsyncMock(return_value=tiebreak_result)) as m_tiebreak: await upload._correlate_with_consensus( call_id="call-1", node_id="n1", system_id="sys-1", talkgroup_id=9048, talkgroup_name="Dispatch", tags=[], incident_type=None, location=None, location_coords=None, ) return m_apply, m_tiebreak async def test_substanceless_no_recent_same_tg_incident_gates_without_tiebreak(): m_apply, m_tiebreak = await _run_consensus( _preview("new", {}), _llm("orphan", "unit check-in, not an incident"), ) m_tiebreak.assert_not_called() m_apply.assert_called_once() gated = m_apply.call_args[0][0]["decision"] assert gated["action"] == "orphan" dbg = gated["corr_debug"] assert dbg["corr_consensus"] == "llm_orphan_gate" assert dbg["corr_consensus"] != "tiebreak" assert dbg["corr_rules_action"] == "new" assert dbg["corr_llm_action"] == "orphan" assert dbg["corr_llm_reasoning"] == "unit check-in, not an incident" @pytest.mark.parametrize("severity", ["moderate", "major"]) async def test_moderate_or_major_severity_is_not_gated(severity): m_apply, m_tiebreak = await _run_consensus( _preview("new", {}, ctx={"call_severity": severity}), _llm("orphan"), ) m_tiebreak.assert_called_once() async def test_routine_severity_alone_still_gates(): m_apply, m_tiebreak = await _run_consensus( _preview("new", {}, ctx={"call_severity": "routine"}), _llm("orphan"), ) m_tiebreak.assert_not_called() assert m_apply.call_args[0][0]["decision"]["action"] == "orphan" async def test_call_with_coords_is_not_gated(): m_apply, m_tiebreak = await _run_consensus( _preview("new", {}, ctx={"coords": {"lat": 41.15, "lng": -73.86}}), _llm("orphan"), ) m_tiebreak.assert_called_once() async def test_call_with_tags_is_not_gated(): m_apply, m_tiebreak = await _run_consensus( _preview("new", {}, ctx={"tags": ["structure-fire"]}), _llm("orphan"), ) m_tiebreak.assert_called_once() async def test_call_with_vehicles_is_not_gated(): m_apply, m_tiebreak = await _run_consensus( _preview("new", {}, ctx={"call_vehicles": ["red sedan"]}), _llm("orphan"), ) m_tiebreak.assert_called_once() async def test_recent_incident_on_same_talkgroup_is_not_gated(): ctx = { "system_id": "sys-1", "talkgroup_id": 9048, "recent": [{ "incident_id": "inc-live", "system_ids": ["sys-1"], "talkgroup_ids": ["9048"], }], } m_apply, m_tiebreak = await _run_consensus( _preview("new", {}, ctx=ctx), _llm("orphan"), ) m_tiebreak.assert_called_once() async def test_recent_incident_on_a_different_talkgroup_still_gates(): ctx = { "system_id": "sys-1", "talkgroup_id": 9048, "recent": [{ "incident_id": "inc-other", "system_ids": ["sys-1"], "talkgroup_ids": ["1200"], }], } m_apply, m_tiebreak = await _run_consensus( _preview("new", {}, ctx=ctx), _llm("orphan"), ) m_tiebreak.assert_not_called() assert m_apply.call_args[0][0]["decision"]["action"] == "orphan" async def test_llm_link_vs_rules_new_still_escalates(): m_apply, m_tiebreak = await _run_consensus(_preview("new", {}), _llm("link", "same job")) m_tiebreak.assert_called_once() async def test_llm_orphan_vs_rules_link_still_escalates(): # Not the gate condition (gate needs rules=="new"); must fall through. m_apply, m_tiebreak = await _run_consensus(_preview("link", {}), _llm("orphan")) m_tiebreak.assert_called_once() def test_has_event_substance_predicate(): assert has_event_substance({"coords": {"lat": 1, "lng": 2}}) assert has_event_substance({"tags": ["fire"]}) assert has_event_substance({"call_vehicles": ["sedan"]}) assert not has_event_substance({}) assert not has_event_substance({"coords": None, "tags": [], "call_vehicles": []}) # units and location are NOT substance — nearly every transmission has them. assert not has_event_substance({"call_units": ["7-Adam"], "location": "Main St"}) # ───────────────────────────────────────────────────────────────────────────── # Fix 2 — tighten corr_path=location # ───────────────────────────────────────────────────────────────────────────── CALL_COORDS = {"lat": 41.150000, "lng": -73.860000} # ~0.39 km north of the call — inside location_proximity_km (0.5) but well # outside the tight bar (_LOCATION_TIGHT_PROXIMITY_KM, 0.2). FAR_INC_COORDS = {"lat": 41.153500, "lng": -73.860000} # ~0.13 km north of the call — inside the tight bar. NEAR_INC_COORDS = {"lat": 41.151200, "lng": -73.860000} # ~0.28 km north — inside the 0.5 radius, outside the 0.2 tight bar; used as a # second candidate that must lose the nearest-wins sort to NEAR_INC_COORDS. MID_INC_COORDS = {"lat": 41.152500, "lng": -73.860000} def _inc(incident_id, coords, units): return { "incident_id": incident_id, "system_ids": ["sys-1"], "talkgroup_ids": ["100"], # different TGID → fast path is a no-op "location_coords": coords, "units": units, "tags": [], "type": "police", "updated_at": (NOW - timedelta(minutes=6)).isoformat(), "started_at": (NOW - timedelta(minutes=20)).isoformat(), "status": "active", "call_ids": ["c0"], } def _loc_ctx(*, incidents, call_units): return { "call_id": "call-loc", "all_active": list(incidents), "recent": list(incidents), "call_doc": {}, "call_embedding": None, "call_units": call_units, "call_vehicles": [], "call_cleared": [], "call_severity": "routine", "coords": CALL_COORDS, "is_thin_call": False, "now": NOW, "system_id": "sys-1", "talkgroup_id": 999, # not in inc.talkgroup_ids "talkgroup_name": "Tactical", "tags": [], "incident_type": "police", "location": "Main St", "location_coords": CALL_COORDS, "reassignment": True, # suppress the unit-continuity path "create_if_new": True, } def test_location_path_in_radius_but_no_unit_overlap_no_tight_proximity_does_not_link(caplog): ctx = _loc_ctx( incidents=[_inc("inc-loc", FAR_INC_COORDS, ["7-Adam"])], call_units=["3-Boy"], ) with caplog.at_level("INFO", logger="drb-c2-core"): decision = _run_decision(ctx) # Reaches, and is rejected by, the new guard (not an earlier path). assert "location-path skipped" in caplog.text assert decision["action"] != "link" assert (decision.get("corr_debug") or {}).get("corr_path") != "location" def test_location_path_links_on_unit_overlap_with_distinct_fit_signal(): ctx = _loc_ctx( incidents=[_inc("inc-loc", FAR_INC_COORDS, ["5-Adam"])], call_units=["5-Adam"], ) decision = _run_decision(ctx) assert decision["action"] == "link" assert decision["corr_debug"]["corr_path"] == "location" # NOT "unit_overlap" — that value belongs to the fast path's histogram bucket. assert decision["corr_debug"]["corr_fit_signal"] == "location_unit_overlap" def test_location_path_links_on_tight_proximity_without_unit_overlap(): ctx = _loc_ctx( incidents=[_inc("inc-loc", NEAR_INC_COORDS, ["7-Adam"])], call_units=["3-Boy"], ) decision = _run_decision(ctx) assert decision["action"] == "link" assert decision["corr_debug"]["corr_path"] == "location" assert decision["corr_debug"]["corr_fit_signal"] == "location_proximity" def test_location_path_picks_nearest_in_radius_candidate(): # `recent` order puts the farther tight-proximity incident first; the guard # must still select the nearest one. ctx = _loc_ctx( incidents=[ _inc("inc-mid", MID_INC_COORDS, ["3-Boy"]), # ~0.28 km, tight-fail _inc("inc-near", NEAR_INC_COORDS, ["3-Boy"]), # ~0.13 km, tight-pass ], call_units=["3-Boy"], ) decision = _run_decision(ctx) assert decision["action"] == "link" assert decision["matched_incident"]["incident_id"] == "inc-near" assert decision["corr_debug"]["corr_path"] == "location"