""" Unit tests for Maps-based place verification (server-26#37). The property that matters most is the one that looks like a no-op: WITHOUT AN ANCHOR, NOTHING HAPPENS. A system whose area is too wide to discriminate stores no anchor, and verification must then skip entirely rather than accept whatever geocodes. A check that passes everything is worse than no check, because it reads as verification in the logs and in the data. After that: a candidate may only rewrite a transcript if it actually sounds like what was heard. Places Text Search will return the nearest plausible business for any garbage string, so the API answering at all is not evidence. """ import pytest from unittest.mock import AsyncMock, patch from app.internal import place_verifier as pv ANCHOR_AREA = { "municipality": "Ossining", "county": "Westchester", "state": "New York", "local_knowledge": [{"term": "Snowden Avenue", "meaning": "residential street"}], "center": {"lat": 41.16, "lng": -73.86}, "radius_km": 6.0, "resolved_from": "ossining|westchester|new york", } SEGS = [{"start": 0.0, "end": 1.0, "text": "Shout out to Optum."}, {"start": 1.0, "end": 2.0, "text": "Copy that."}] @pytest.fixture(autouse=True) def _enabled(): with patch.object(pv.settings, "place_verification_enabled", True), \ patch.object(pv.settings, "google_maps_api_key", "test-key"): yield def _geocode(result): return patch.object(pv, "_geocode_in_anchor", AsyncMock(return_value=result)) def _places(result): return patch.object(pv, "_places_soundalike", AsyncMock(return_value=result)) # -- Phonetics ----------------------------------------------------------------- @pytest.mark.parametrize("heard, real", [ ("Snowden Avenue", "Snowdon Ave"), ("5 acre", "5-baker"), ("why vac", "YVAC"), ("Croton Ave", "Croton Avenue"), ]) def test_real_mishearings_score_above_the_threshold(heard, real): assert pv.sounds_like(heard, real) >= pv.settings.place_soundalike_min_ratio @pytest.mark.parametrize("heard, unrelated", [ ("Optum", "Ossining"), ("Cool Parts", "Croton Point"), ]) def test_unrelated_names_score_below_it(heard, unrelated): assert pv.sounds_like(heard, unrelated) < pv.settings.place_soundalike_min_ratio # -- The skip path ------------------------------------------------------------- @pytest.mark.asyncio async def test_no_anchor_means_skip_not_accept(): """A statewide system stores no anchor. Nothing may be checked or rewritten.""" with patch.object(pv, "_geocode_in_anchor") as geo: out = await pv.verify("c1", "text here", SEGS, ["Optum"], {"state": "Colorado"}, {}) assert out == (None, None) geo.assert_not_called() @pytest.mark.asyncio async def test_no_locations_means_no_requests(): with patch.object(pv, "_geocode_in_anchor") as geo: assert await pv.verify("c1", "t", None, [], ANCHOR_AREA, {}) == (None, None) geo.assert_not_called() @pytest.mark.asyncio async def test_disabled_by_setting(): with patch.object(pv.settings, "place_verification_enabled", False), \ patch.object(pv, "_geocode_in_anchor") as geo: assert await pv.verify("c1", "t", None, ["Optum"], ANCHOR_AREA, {}) == (None, None) geo.assert_not_called() # -- The accept path ----------------------------------------------------------- @pytest.mark.asyncio async def test_a_place_that_resolves_inside_the_anchor_is_left_alone(): with _geocode({"lat": 41.16, "lng": -73.86}), _places(None) as places: out = await pv.verify("c1", "Units to Snowden Avenue.", None, ["Snowden Avenue"], ANCHOR_AREA, {}) assert out == (None, None) places.assert_not_called() # a hit must not cost a second request @pytest.mark.asyncio async def test_the_query_carries_the_full_place(): seen = {} async def capture(query, anchor): seen["query"] = query return {"lat": 41.16, "lng": -73.86} with patch.object(pv, "_geocode_in_anchor", capture): await pv.verify("c1", "t", None, ["High Street"], ANCHOR_AREA, {}) assert seen["query"] == "High Street, Ossining, Westchester, New York" # -- The correction path ------------------------------------------------------- @pytest.mark.asyncio async def test_known_term_is_preferred_and_costs_nothing(): """ A sound-alike the operator already entered is both free and more trustworthy than anything Maps guesses, so it must be tried before any request goes out. """ with _geocode(None), _places(None) as places, \ patch.object(pv.area_context, "add_pending", AsyncMock()) as add: text, segs = await pv.verify( "c1", "Units to Snowdon Ave.", None, ["Snowdon Ave"], ANCHOR_AREA, {} ) assert text == "Units to Snowden Avenue." places.assert_not_called() add.assert_not_called() # already known — nothing to propose @pytest.mark.asyncio async def test_a_maps_soundalike_is_applied_and_proposed_to_the_talkgroup(): candidate = {"term": "Croton Point", "meaning": "Croton Point Ave, Croton NY", "score": 0.8} with _geocode(None), _places(candidate), \ patch.object(pv.area_context, "add_pending", AsyncMock(return_value=1)) as add: text, segs = await pv.verify( "c1", "Respond to Cool Parts.", None, ["Cool Parts"], ANCHOR_AREA, {}, system_id="sys-1", talkgroup_id=9048, ) assert text == "Respond to Croton Point." args = add.await_args.args assert args[0] == "sys-1" and args[1] == 9048 assert args[2][0]["term"] == "Croton Point" assert args[2][0]["source_call_ids"] == ["c1"] @pytest.mark.asyncio async def test_nothing_plausible_leaves_the_transcript_untouched(): """ An invented name with no real counterpart nearby stays as it is. Guessing would put a fabricated location into the incident record, which is the outcome this whole pass exists to avoid. """ with _geocode(None), _places(None): assert await pv.verify("c1", "Shout out to Optum.", SEGS, ["Optum"], ANCHOR_AREA, {}) == (None, None) @pytest.mark.asyncio async def test_segments_are_corrected_alongside_the_joined_text(): """Extraction reads numbered segments, so a joined-only fix reaches nothing.""" with _geocode(None), _places(None), \ patch.object(pv.area_context, "add_pending", AsyncMock()): text, segs = await pv.verify( "c1", "Shout out to Snowdon Ave. Copy that.", [{"start": 0.0, "end": 1.0, "text": "Shout out to Snowdon Ave."}, {"start": 1.0, "end": 2.0, "text": "Copy that."}], ["Snowdon Ave"], ANCHOR_AREA, {}, ) assert segs is not None assert segs[0]["text"] == "Shout out to Snowden Avenue." assert segs[0]["start"] == 0.0, "timing survives untouched" assert segs[1]["text"] == "Copy that." @pytest.mark.asyncio async def test_a_geocoder_failure_never_breaks_the_transcript(): with patch.object(pv, "_geocode_in_anchor", AsyncMock(side_effect=RuntimeError("boom"))): assert await pv.verify("c1", "t here", None, ["Optum"], ANCHOR_AREA, {}) == (None, None) @pytest.mark.asyncio async def test_only_a_bounded_number_of_nouns_is_checked(): with patch.object(pv.settings, "place_verify_max_per_call", 2), \ patch.object(pv, "_geocode_in_anchor", AsyncMock(return_value={"lat": 41.16, "lng": -73.86})) as geo: await pv.verify("c1", "t", None, ["a", "b", "c", "d"], ANCHOR_AREA, {}) assert geo.await_count == 2