From bdb57ae75ac9849f31d3a0937d7e0f4aae820376 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Mon, 31 Aug 2026 02:45:31 -0400 Subject: [PATCH] correlator: stop non-primary scenes inheriting the call doc's pin (#87) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _build_context fell back to call_doc.get("location_coords") whenever a scene passed no coordinates of its own. One radio call can be split into several scenes, but only the primary scene's geocode is ever written to the call doc — so every non-primary scene silently inherited the primary scene's pin. That fabricated location_proximity, the strongest accept signal the correlator has, for a scene that had no location at all, and drove it into the primary scene's incident on a pin it never had. Drop the fallback: coords = location_coords. A scene with no location is now correctly judged thin, cannot win the location path, cannot supply call_coords to _call_fits_incident, and cannot seed _find_cross_system_parent. recorrelation_sweep.py, the only other caller of correlate_call, was verified to already pass both location and location_coords explicitly from the call doc, so the fallback there was a no-op and this change is behavior-preserving for that path. Adds test_a_scene_with_no_location_does_not_inherit_the_call_docs_pin to test_incident_identity.py, pinning ctx["coords"] is None and ctx["is_thin_call"] is True when location=None but the call doc carries a location_coords. Ref: server-26#87 --- .../app/internal/incident_correlator.py | 9 ++++++- drb-c2-core/tests/test_incident_identity.py | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/drb-c2-core/app/internal/incident_correlator.py b/drb-c2-core/app/internal/incident_correlator.py index 00e5284..269a5f4 100644 --- a/drb-c2-core/app/internal/incident_correlator.py +++ b/drb-c2-core/app/internal/incident_correlator.py @@ -789,7 +789,14 @@ async def _build_context( location = clean_location(location) if location is None: location_coords = None - coords = location_coords or call_doc.get("location_coords") + # NOT `location_coords or call_doc.get("location_coords")` — server-26#87. + # A radio call can be split into several scenes, and only the primary + # scene's geocode is written to the call doc. Falling back to it here + # would hand every non-primary scene the primary scene's pin, fabricating + # location_proximity (the strongest accept signal) for a scene that has + # no location of its own and driving over-merges. If a scene passes no + # coords, it has none — it is judged thin and must win on its own signal. + coords = location_coords is_thin_call = _is_thin_call( call_units, call_vehicles, coords, tags, location, call_severity, reassignment ) diff --git a/drb-c2-core/tests/test_incident_identity.py b/drb-c2-core/tests/test_incident_identity.py index 38aa2fc..c97804c 100644 --- a/drb-c2-core/tests/test_incident_identity.py +++ b/drb-c2-core/tests/test_incident_identity.py @@ -229,6 +229,31 @@ async def test_a_bare_number_never_reaches_the_correlator(): assert ctx["location_coords"] is None +@pytest.mark.asyncio +async def test_a_scene_with_no_location_does_not_inherit_the_call_docs_pin(): + """ + server-26#87. One call can be split into several scenes, and only the + primary scene's geocode is written to the call doc. A non-primary scene + that passes no location of its own must not inherit that pin — doing so + fabricates location_proximity, the strongest accept signal, for a scene + that has none, and drives it into the primary scene's incident. + """ + with patch("app.internal.incident_correlator.fstore") as mock_fstore: + mock_fstore.doc_get = AsyncMock( + return_value={"location_coords": GRASSLANDS} + ) + mock_fstore.collection_list = AsyncMock(return_value=[]) + ctx = await _build_context( + call_id="call-scene-2", units=None, vehicles=None, cleared_units=None, + location_coords=None, reference_time=NOW, + system_id="sys-1", talkgroup_id=383, talkgroup_name=DISPATCH_TG, + tags=[], incident_type="police", location=None, + reassignment=False, create_if_new=True, + ) + assert ctx["coords"] is None + assert ctx["is_thin_call"] is True + + @pytest.mark.asyncio async def test_a_bare_number_never_becomes_an_incident_location_or_title(): inc = await _create(tags=["flames"], location="49", coords=None,