Files
server-26/drb-c2-core/tests/test_geocode_named_region.py
T
Logan CusanoandClaude Opus 5.5 6bf072973c Honest pins and paths: geocode precision, type-gated paths, impossible-speed stops (#236)
Backend: _geocode_location now returns {lat, lng, location_type}, so the
precision rides onto the call doc and, unchanged via _resolve_location_pair,
onto the incident pin. Display-only: every correlator read indexes lat/lng.

Frontend: pins styled by precision (rooftop / interpolated ring /
GEOMETRIC_CENTER dashed uncertainty circle labelled with the spoken place /
unverified pre-#23 pins ghosted). liveWall.pathStops() is shared by the map
and the call spine: only police/accident incidents with a pursuit tag draw a
numbered path; others get unnumbered call dots. Segments over >10 min gaps or
talkgroup changes are dashed, stops implying >8 km/min are ghosted off the
path, paths >20 km get a "may be merged" badge. New optional MapView prop
pulseIncidentIds (wired by #238).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 03:28:00 -04:00

186 lines
7.5 KiB
Python

"""
server-26#159: a citywide/patched feed can be received far from its own
coverage area — "New York City - NYPD Citywide 2 Patch" was ~56km from the
receiving node, well past geocode_max_km (40km). Real, correctly-geocoded
addresses on that talkgroup were rejected by intelligence._geocode_location's
node-distance sanity check every time, so location_coords never populated for
the whole system: location_proximity correlation was permanently dead there,
and the same real event reported at two nearby addresses two minutes apart
became two separate incidents instead of one.
`trust_named_region` fixes this narrowly: the node-distance check is a proxy
for "is this plausible" that only makes sense when the node's own position is
the best guess we have at the area. It must not apply when the query already
names a different region on its own terms (operator-set area_context, or a
municipality parsed straight from the talkgroup's own name) — and it must
never touch the anchor path, whose own radius is always authoritative.
"""
from unittest.mock import patch
import pytest
import app.internal.intelligence as intel
from app.config import settings
def _maps_result(lat: float, lng: float, location_type: str = "ROOFTOP"):
payload = {
"status": "OK",
"results": [{
"geometry": {
"location": {"lat": lat, "lng": lng},
"location_type": location_type,
},
}],
}
class _Resp:
def raise_for_status(self): pass
def json(self): return payload
class _Client:
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
async def get(self, *a, **k): return _Resp()
return patch("httpx.AsyncClient", lambda *a, **k: _Client())
@pytest.fixture(autouse=True)
def _api_key():
# intelligence.py imports settings locally per-function (`from app.config
# import settings`), which binds the same cached singleton — patching the
# module-level object here reaches it, but `intel.settings` itself does
# not exist as an attribute.
with patch.object(settings, "google_maps_api_key", "test-key"):
yield
# Node at (0, 0); result at (1, 0) is ~111km away — well past the 40km default.
NODE_LAT, NODE_LON = 0.0, 0.0
FAR_LAT, FAR_LNG = 1.0, 0.0
@pytest.mark.asyncio
async def test_named_region_geocode_accepted_beyond_node_distance():
with _maps_result(FAR_LAT, FAR_LNG):
coords = await intel._geocode_location(
"1108 Jackson Avenue, New York City - NYPD Citywide 2 Patch",
node_lat=NODE_LAT, node_lon=NODE_LON,
trust_named_region=True,
)
assert coords == {"lat": FAR_LAT, "lng": FAR_LNG, "location_type": "ROOFTOP"}
@pytest.mark.asyncio
async def test_local_geocode_still_rejected_beyond_node_distance_without_named_region():
"""Regression guard: a bare street name with no named region still uses
the node as its only plausibility check, exactly as before this fix."""
with _maps_result(FAR_LAT, FAR_LNG):
coords = await intel._geocode_location(
"Main Street",
node_lat=NODE_LAT, node_lon=NODE_LON,
trust_named_region=False,
)
assert coords is None
@pytest.mark.asyncio
async def test_anchor_path_ignores_trust_named_region():
"""The anchor's own radius is always authoritative — trust_named_region
is only a statement about the node fallback, never a way to widen an
anchor that was itself deliberately sized to discriminate."""
anchor = {"lat": NODE_LAT, "lng": NODE_LON, "radius_km": 10.0}
with _maps_result(FAR_LAT, FAR_LNG):
coords = await intel._geocode_location(
"1108 Jackson Avenue, New York City - NYPD Citywide 2 Patch",
node_lat=NODE_LAT, node_lon=NODE_LON,
anchor=anchor, trust_named_region=True,
)
assert coords is None
@pytest.mark.asyncio
async def test_named_region_geocode_within_node_distance_is_unaffected():
"""A close result is accepted the same way regardless of the flag."""
near_lat, near_lng = 0.05, 0.0 # ~5.5km from the node
with _maps_result(near_lat, near_lng):
coords = await intel._geocode_location(
"Main Street, Ossining, New York",
node_lat=NODE_LAT, node_lon=NODE_LON,
trust_named_region=True,
)
assert coords == {"lat": near_lat, "lng": near_lng, "location_type": "ROOFTOP"}
@pytest.mark.asyncio
async def test_imprecise_result_still_rejected_regardless_of_trust():
"""trust_named_region relaxes the distance check only — the location_type
precision filter (server-26#37) still applies unconditionally."""
with _maps_result(FAR_LAT, FAR_LNG, location_type="APPROXIMATE"):
coords = await intel._geocode_location(
"1108 Jackson Avenue, New York City - NYPD Citywide 2 Patch",
node_lat=NODE_LAT, node_lon=NODE_LON,
trust_named_region=True,
)
assert coords is None
# ---------------------------------------------------------------------------
# _location_query_parts — pure query assembly, no HTTP involved
# ---------------------------------------------------------------------------
def test_operator_configured_area_wins_and_is_named_region():
parts, named = intel._location_query_parts(
"High Street", {"municipality": "Yorktown", "state": "New York"},
"Tac 1", node_state="New York", node_county="Westchester",
)
assert parts == ["High Street", "Yorktown", "New York"]
assert named is True
def test_local_talkgroup_name_gets_node_state_but_not_node_county():
"""
"Ossining PD" is genuinely local to the node, so appending the node's own
state is correct. Its COUNTY is dropped even here — server-26#159's fix
applies uniformly once a municipality is derived, since there is no way
to tell "local" and "distant-but-node-adjacent" apart from the string
alone, and the county was never necessary for a bare municipality name
that already disambiguates via the state.
"""
parts, named = intel._location_query_parts(
"High Street", {}, "Ossining PD",
node_state="New York", node_county="Westchester",
)
assert parts == ["High Street", "Ossining", "New York"]
assert named is True
def test_citywide_patched_feed_does_not_get_the_nodes_county_grafted_on():
"""
server-26#159's actual production case: the talkgroup names its own
(distant) region, so the node's county (Westchester, ~56km away) must not
be appended — it would make the query self-contradictory ("...New York
City..., Westchester, New York") and risks degrading the geocode result's
precision independently of the distance check this issue also fixes.
"""
parts, named = intel._location_query_parts(
"1108 Jackson Avenue", {}, "New York City - NYPD Citywide 2 Patch",
node_state="New York", node_county="Westchester",
)
assert "Westchester" not in parts
assert parts == ["1108 Jackson Avenue", "New York City - NYPD Citywide 2 Patch", "New York"]
assert named is True
def test_uninformative_talkgroup_name_falls_back_to_node_county_and_state():
"""A tactical channel or bare code gives _municipality_from_tg nothing —
the only remaining evidence really is where the node sits, so the
original node-county-and-state fallback is preserved for this case."""
parts, named = intel._location_query_parts(
"High Street", {}, "Tac 1",
node_state="New York", node_county="Westchester",
)
assert parts == ["High Street", "Westchester", "New York"]
assert named is False