"""Tests for meshai/coverage_area.py — Shapely geometry gate. Includes a reproduction of the LA/OR leak that originally triggered this work: an event whose geometry is firmly inside California must be DROPPED when only an Idaho coverage box is configured. Bbox/coordinate convention (GeoJSON): bbox = [west, south, east, north] = [min_lon, min_lat, max_lon, max_lat] Point = [lon, lat] Idaho reference area (magic-valley to eastern border, generous): west=-117, south=42, east=-111, north=44 """ from __future__ import annotations import json from unittest.mock import MagicMock, patch import pytest from meshai.coverage_area import ( MonitoringArea, areas_from_config, build_geom_json, classify_event_areas, classify_geom_areas, event_in_areas, ) from meshai.env.nws import NWSAlertsAdapter from meshai.notifications.events import make_event from meshai.notifications.pipeline.coverage_filter import ( CoverageFilter, FAIL_CLOSED_CATEGORIES, ) from meshai.config import Coverage # --------------------------------------------------------------------------- # Reference areas # --------------------------------------------------------------------------- IDAHO = MonitoringArea(north=44.0, south=42.0, east=-111.0, west=-117.0) # Second non-contiguous box covering Yellowstone / eastern corner of WY YELLOWSTONE = MonitoringArea(north=45.2, south=44.0, east=-109.5, west=-111.5) IDAHO_AREAS = [IDAHO] TWO_AREAS = [IDAHO, YELLOWSTONE] # A point firmly inside Idaho (Twin Falls area) TF_LON, TF_LAT = -114.47, 42.56 # A point at downtown Los Angeles — should be OUT of Idaho LA_LON, LA_LAT = -118.24, 34.05 # A CA polygon enclosing the LA area (4-corner box + close) CA_POLYGON_COORDS = [ [-119.0, 33.5], [-117.5, 33.5], [-117.5, 34.6], [-119.0, 34.6], [-119.0, 33.5], ] CA_POLYGON_GEOJSON = json.dumps({ "type": "Polygon", "coordinates": [CA_POLYGON_COORDS], }) # A polygon straddling the Idaho western border (partly in Oregon, partly Idaho) STRADDLER_COORDS = [ [-117.5, 42.5], # in Oregon [-116.0, 42.5], # in Idaho [-116.0, 43.5], # in Idaho [-117.5, 43.5], # in Oregon [-117.5, 42.5], ] STRADDLER_GEOJSON = json.dumps({ "type": "Polygon", "coordinates": [STRADDLER_COORDS], }) # =========================================================================== # MonitoringArea.as_box # =========================================================================== class TestMonitoringArea: def test_as_box_is_shapely_polygon(self): from shapely.geometry import box b = IDAHO.as_box() expected = box(IDAHO.west, IDAHO.south, IDAHO.east, IDAHO.north) assert b.equals(expected) def test_frozen_immutability(self): with pytest.raises(Exception): IDAHO.north = 99.0 # type: ignore[misc] # =========================================================================== # build_geom_json — geometry extraction priority # =========================================================================== class TestBuildGeomJson: def test_none_input_returns_none(self): assert build_geom_json(None) is None def test_empty_dict_returns_none(self): assert build_geom_json({}) is None def test_geometry_wins_over_bbox(self): geo = { "geometry": {"type": "Point", "coordinates": [TF_LON, TF_LAT]}, "bbox": [-115.0, 42.0, -113.0, 43.0], } result = build_geom_json(geo) assert json.loads(result) == {"type": "Point", "coordinates": [TF_LON, TF_LAT]} def test_bbox_renders_as_polygon(self): geo = {"bbox": [-115.0, 42.0, -113.0, 43.0]} result = build_geom_json(geo) parsed = json.loads(result) assert parsed["type"] == "Polygon" coords = parsed["coordinates"][0] # should be a closed ring of 5 points assert len(coords) == 5 assert coords[0] == coords[-1] def test_centroid_renders_as_point(self): geo = {"centroid": [TF_LON, TF_LAT]} result = build_geom_json(geo) parsed = json.loads(result) assert parsed == {"type": "Point", "coordinates": [TF_LON, TF_LAT]} def test_bbox_wins_over_centroid(self): geo = { "bbox": [-115.0, 42.0, -113.0, 43.0], "centroid": [LA_LON, LA_LAT], } result = build_geom_json(geo) assert "Polygon" in result def test_returns_none_if_no_usable_shape(self): geo = {"name": "irrelevant"} assert build_geom_json(geo) is None # =========================================================================== # classify_geom_areas # =========================================================================== class TestClassifyGeomAreas: def test_null_geom_returns_null_geom(self): assert classify_geom_areas(None, IDAHO_AREAS) == "null-geom" def test_no_area_returns_no_area(self): geom_json = json.dumps({"type": "Point", "coordinates": [TF_LON, TF_LAT]}) assert classify_geom_areas(geom_json, []) == "no-area" def test_idaho_point_in_bounds(self): geom_json = json.dumps({"type": "Point", "coordinates": [TF_LON, TF_LAT]}) assert classify_geom_areas(geom_json, IDAHO_AREAS) == "in-bounds" def test_la_point_out_of_bounds(self): """LA repro: a point at (34.05N, 118.24W) must be OUT of an Idaho box.""" geom_json = json.dumps({"type": "Point", "coordinates": [LA_LON, LA_LAT]}) assert classify_geom_areas(geom_json, IDAHO_AREAS) == "out-of-bounds" def test_ca_polygon_out_of_bounds(self): """CA polygon straddling 34N should be fully outside Idaho.""" assert classify_geom_areas(CA_POLYGON_GEOJSON, IDAHO_AREAS) == "out-of-bounds" def test_straddler_polygon_in_bounds(self): """Polygon crossing the ID/OR border still intersects Idaho → in-bounds.""" assert classify_geom_areas(STRADDLER_GEOJSON, IDAHO_AREAS) == "in-bounds" def test_invalid_geom_returns_invalid_geom(self): assert classify_geom_areas("this is not json", IDAHO_AREAS) == "invalid-geom" def test_empty_geom_string_returns_invalid_geom(self): assert classify_geom_areas("", IDAHO_AREAS) == "invalid-geom" def test_multi_area_union_point_in_second_box(self): """A point inside the Yellowstone box (not Idaho) → in-bounds via union.""" # Yellowstone area: north=45.2, south=44.0, east=-109.5, west=-111.5 ys_point = json.dumps({"type": "Point", "coordinates": [-110.5, 44.5]}) assert classify_geom_areas(ys_point, TWO_AREAS) == "in-bounds" def test_multi_area_union_point_in_neither_box(self): """A point in Wyoming (but not in either box) → out-of-bounds.""" # Far eastern Wyoming, outside both boxes wy_point = json.dumps({"type": "Point", "coordinates": [-104.0, 42.5]}) assert classify_geom_areas(wy_point, TWO_AREAS) == "out-of-bounds" def test_border_edge_point_is_in_bounds(self): """A point exactly on the Idaho box edge → in-bounds (intersects semantics).""" # Exactly on the western edge of Idaho box (lon=-117, lat=43) edge = json.dumps({"type": "Point", "coordinates": [-117.0, 43.0]}) assert classify_geom_areas(edge, IDAHO_AREAS) == "in-bounds" # =========================================================================== # areas_from_config # =========================================================================== class TestAreasFromConfig: def test_from_areas_list(self): cov = Coverage(areas=[ {"name": "Idaho", "west": -117.0, "south": 42.0, "east": -111.0, "north": 44.0} ]) areas = areas_from_config(cov) assert len(areas) == 1 a = areas[0] assert a.west == -117.0 and a.south == 42.0 assert a.east == -111.0 and a.north == 44.0 def test_falls_back_to_bbox(self): cov = Coverage(bbox=[-117.0, 42.0, -111.0, 44.0]) areas = areas_from_config(cov) assert len(areas) == 1 assert areas[0].west == -117.0 def test_areas_takes_precedence_over_bbox(self): cov = Coverage( bbox=[-110.0, 30.0, -100.0, 35.0], # TX — totally different areas=[{"name": "ID", "west": -117.0, "south": 42.0, "east": -111.0, "north": 44.0}], ) areas = areas_from_config(cov) assert len(areas) == 1 assert areas[0].west == -117.0 # Idaho, not TX def test_empty_config_returns_empty(self): cov = Coverage() assert areas_from_config(cov) == [] def test_multi_area_list(self): cov = Coverage(areas=[ {"name": "Idaho", "west": -117.0, "south": 42.0, "east": -111.0, "north": 44.0}, {"name": "Yellowstone", "west": -111.5, "south": 44.0, "east": -109.5, "north": 45.2}, ]) areas = areas_from_config(cov) assert len(areas) == 2 def test_malformed_area_dict_skipped(self): cov = Coverage(areas=[ {"name": "bad"}, # missing west/south/east/north {"name": "Idaho", "west": -117.0, "south": 42.0, "east": -111.0, "north": 44.0}, ]) areas = areas_from_config(cov) assert len(areas) == 1 # only the valid one # =========================================================================== # event_in_areas # =========================================================================== class TestEventInAreas: def _event_with_geom(self, geometry_dict, lat=None, lon=None): return make_event( source="nws", category="weather_warning", severity="priority", title="Test", lat=lat, lon=lon, data={"geometry": geometry_dict}, ) def _event_with_centroid(self, lat, lon): return make_event( source="nws", category="weather_warning", severity="priority", title="Test", lat=lat, lon=lon, ) def test_idaho_centroid_kept(self): event = self._event_with_centroid(TF_LAT, TF_LON) assert event_in_areas(event, IDAHO_AREAS) is True def test_la_centroid_dropped(self): """LA-repro: centroid at (34.05, -118.24) must be dropped by Idaho gate.""" event = self._event_with_centroid(LA_LAT, LA_LON) assert event_in_areas(event, IDAHO_AREAS) is False def test_ca_polygon_dropped(self): """LA-repro: event carrying a CA polygon must be dropped by Idaho gate.""" ca_geom = {"type": "Polygon", "coordinates": [CA_POLYGON_COORDS]} event = self._event_with_geom(ca_geom) assert event_in_areas(event, IDAHO_AREAS) is False def test_idaho_polygon_kept(self): idaho_geom = { "type": "Polygon", "coordinates": [[ [-115.0, 42.5], [-113.0, 42.5], [-113.0, 43.5], [-115.0, 43.5], [-115.0, 42.5], ]], } event = self._event_with_geom(idaho_geom) assert event_in_areas(event, IDAHO_AREAS) is True def test_no_geometry_kept_fail_open(self): """Event with no lat/lon and no data geometry → null-geom → kept.""" event = make_event( source="swpc", category="kp_index", severity="priority", title="No geometry event", ) assert event_in_areas(event, IDAHO_AREAS) is True def test_empty_areas_kept(self): event = self._event_with_centroid(LA_LAT, LA_LON) assert event_in_areas(event, []) is True def test_event_with_bbox_in_data(self): """Event carrying data[bbox] (not geometry) is classified via bbox polygon.""" event = make_event( source="firms", category="wildfire_hotspot", severity="priority", title="Fire", data={"bbox": [-114.5, 42.5, -113.5, 43.5]}, # Idaho bbox ) assert event_in_areas(event, IDAHO_AREAS) is True def test_event_with_out_of_area_bbox_in_data(self): event = make_event( source="firms", category="wildfire_hotspot", severity="priority", title="Fire", data={"bbox": [-119.0, 33.5, -117.5, 34.6]}, # CA bbox ) assert event_in_areas(event, IDAHO_AREAS) is False # =========================================================================== # CoverageFilter — unit tests # =========================================================================== class TestCoverageFilter: def _make_filter(self, areas=None, enabled=True, excluded=None, received=None): if received is None: received = [] flt = CoverageFilter( next_handler=received.append, areas=areas if areas is not None else IDAHO_AREAS, enabled=enabled, excluded_adapters=set(excluded or []), ) return flt, received # -- LA leak repro ------------------------------------------------------- def test_la_event_with_polygon_dropped(self): """LA-repro: event with CA polygon GeoJSON is dropped by Idaho gate.""" flt, received = self._make_filter() ca_geom = {"type": "Polygon", "coordinates": [CA_POLYGON_COORDS]} event = make_event( source="nws", category="weather_warning", severity="priority", title="Severe Thunderstorm Warning — Los Angeles County", lat=LA_LAT, lon=LA_LON, data={"geometry": ca_geom}, ) flt.handle(event) assert len(received) == 0, "LA event with CA polygon must be dropped" def test_la_event_with_centroid_dropped(self): """LA-repro: event with centroid at LA is dropped by Idaho gate.""" flt, received = self._make_filter() event = make_event( source="nws", category="weather_warning", severity="priority", title="LA weather alert", lat=LA_LAT, lon=LA_LON, ) flt.handle(event) assert len(received) == 0, "LA centroid event must be dropped" # -- Idaho events pass --------------------------------------------------- def test_idaho_event_kept(self): flt, received = self._make_filter() event = make_event( source="nws", category="weather_warning", severity="priority", title="Idaho alert", lat=TF_LAT, lon=TF_LON, ) flt.handle(event) assert len(received) == 1 # -- Disabled / no-areas pass-through ------------------------------------ def test_coverage_disabled_keeps_everything(self): flt, received = self._make_filter(enabled=False) event = make_event( source="nws", category="weather_warning", severity="priority", title="LA alert", lat=LA_LAT, lon=LA_LON, ) flt.handle(event) assert len(received) == 1 def test_empty_areas_keeps_everything(self): flt, received = self._make_filter(areas=[]) event = make_event( source="nws", category="weather_warning", severity="priority", title="LA alert", lat=LA_LAT, lon=LA_LON, ) flt.handle(event) assert len(received) == 1 # -- Excluded adapter bypass --------------------------------------------- def test_excluded_adapter_bypasses_gate(self): """Events from excluded adapters pass even if out of area.""" flt, received = self._make_filter(excluded=["swpc"]) event = make_event( source="swpc", category="kp_index", severity="priority", title="Solar event", lat=LA_LAT, lon=LA_LON, ) flt.handle(event) assert len(received) == 1 def test_non_excluded_adapter_is_gated(self): """Events from non-excluded adapters still go through the gate.""" flt, received = self._make_filter(excluded=["swpc"]) event = make_event( source="nws", category="weather_warning", severity="priority", title="LA alert", lat=LA_LAT, lon=LA_LON, ) flt.handle(event) assert len(received) == 0 # -- fail-open ----------------------------------------------------------- def test_no_geometry_event_kept_fail_open(self): """Events with no geometry (null-geom) are kept (fail-open).""" flt, received = self._make_filter() event = make_event( source="swpc", category="kp_index", severity="priority", title="Solar storm — no location", ) flt.handle(event) assert len(received) == 1 # -- multi-area union ---------------------------------------------------- def test_multi_area_point_in_second_box_kept(self): flt, received = self._make_filter(areas=TWO_AREAS) # A point in the Yellowstone box (not Idaho proper) event = make_event( source="nws", category="weather_warning", severity="priority", title="Yellowstone area alert", lat=44.5, lon=-110.5, ) flt.handle(event) assert len(received) == 1 def test_multi_area_point_in_neither_dropped(self): flt, received = self._make_filter(areas=TWO_AREAS) # Far eastern Wyoming — outside both boxes event = make_event( source="nws", category="weather_warning", severity="priority", title="Wyoming alert", lat=42.5, lon=-104.0, ) flt.handle(event) assert len(received) == 0 # =========================================================================== # Pipeline wiring — CoverageFilter is in the chain # =========================================================================== class TestCoverageFilterPipelineWiring: def test_coverage_filter_wired_in_pipeline(self): """build_pipeline stashes coverage_filter in _pipeline_components.""" from unittest.mock import MagicMock, AsyncMock from meshai.notifications.pipeline import build_pipeline config = Coverage.__new__(Coverage) # we need a full Config below from meshai.config import Config config = Config() config.coverage = Coverage( areas=[{"name": "Idaho", "west": -117.0, "south": 42.0, "east": -111.0, "north": 44.0}], enabled=True, ) mock_backend = MagicMock() mock_backend.generate = AsyncMock(return_value="stub") bus = build_pipeline(config, mock_backend) comps = bus._pipeline_components assert "coverage_filter" in comps cf = comps["coverage_filter"] assert hasattr(cf, "handle") assert isinstance(cf, CoverageFilter) def test_coverage_filter_gate_fires_in_pipeline(self): """Events emitted to the bus from out-of-area are dropped before dispatch.""" from unittest.mock import MagicMock, AsyncMock from meshai.notifications.pipeline import build_pipeline from meshai.config import Config, NotificationToggle config = Config() config.coverage = Coverage( areas=[{"name": "Idaho", "west": -117.0, "south": 42.0, "east": -111.0, "north": 44.0}], enabled=True, ) # Enable weather toggle so ToggleFilter passes the event through to # CoverageFilter (otherwise toggle filter swallows it first) config.notifications.toggles["weather"].enabled = True mock_backend = MagicMock() mock_backend.generate = AsyncMock(return_value="stub") received_by_tee: list = [] bus = build_pipeline(config, mock_backend) # Monkeypatch the coverage_filter's next_handler to observe what reaches _tee cf = bus._pipeline_components["coverage_filter"] cf._next = received_by_tee.append # type: ignore[assignment] # Emit an out-of-area LA event la_event = make_event( source="nws", category="weather_warning", severity="priority", title="LA Thunderstorm Warning", lat=LA_LAT, lon=LA_LON, ) bus.emit(la_event) assert len(received_by_tee) == 0, "LA event must not reach _tee" # Emit an in-area Idaho event id_event = make_event( source="nws", category="weather_warning", severity="priority", title="Idaho Thunderstorm Warning", lat=TF_LAT, lon=TF_LON, ) bus.emit(id_event) assert len(received_by_tee) == 1, "Idaho event must reach _tee" # =========================================================================== # Idaho + MultiPolygon geometry fixtures (for the fail-closed / geometry work) # =========================================================================== # A polygon firmly inside the Idaho box (centroid ~ 42.9N, -114.2W → Twin Falls) ID_POLYGON_COORDS = [ [-115.0, 42.5], [-113.0, 42.5], [-113.0, 43.5], [-115.0, 43.5], [-115.0, 42.5], ] ID_POLYGON_GEOM = {"type": "Polygon", "coordinates": [ID_POLYGON_COORDS]} # A MultiPolygon whose FIRST polygon's outer ring is inside Idaho; a second, # far-away polygon (in Kansas) exercises the "first polygon wins" centroid rule. KS_POLYGON_COORDS = [ [-98.0, 38.0], [-97.0, 38.0], [-97.0, 39.0], [-98.0, 39.0], [-98.0, 38.0], ] ID_MULTIPOLYGON_GEOM = { "type": "MultiPolygon", "coordinates": [[ID_POLYGON_COORDS], [KS_POLYGON_COORDS]], } # =========================================================================== # classify_event_areas — 3-state classification helper # =========================================================================== class TestClassifyEventAreas: def test_idaho_polygon_in_bounds(self): event = make_event( source="nws", category="weather_advisory", severity="routine", title="ID advisory", data={"geometry": ID_POLYGON_GEOM}, ) assert classify_event_areas(event, IDAHO_AREAS) == "in-bounds" def test_ca_polygon_out_of_bounds(self): ca_geom = {"type": "Polygon", "coordinates": [CA_POLYGON_COORDS]} event = make_event( source="nws", category="weather_advisory", severity="routine", title="CA advisory", data={"geometry": ca_geom}, ) assert classify_event_areas(event, IDAHO_AREAS) == "out-of-bounds" def test_no_geometry_no_centroid_is_null_geom(self): """The exact zone-only-alert shape: geometry=None, no lat/lon.""" event = make_event( source="nws", category="weather_advisory", severity="routine", title="Heat Advisory (zone-only)", data={"geometry": None}, ) assert classify_event_areas(event, IDAHO_AREAS) == "null-geom" def test_multipolygon_in_bounds(self): event = make_event( source="nws", category="weather_warning", severity="priority", title="ID multipolygon warning", data={"geometry": ID_MULTIPOLYGON_GEOM}, ) assert classify_event_areas(event, IDAHO_AREAS) == "in-bounds" # =========================================================================== # CoverageFilter — fail-CLOSED for weather, fail-OPEN for everything else # =========================================================================== class TestCoverageFilterFailClosed: def _make_filter(self, areas=None, excluded=None): received: list = [] flt = CoverageFilter( next_handler=received.append, areas=areas if areas is not None else IDAHO_AREAS, enabled=True, excluded_adapters=set(excluded or []), ) return flt, received def test_fail_closed_categories_are_the_nws_categories(self): """Guard: the fail-closed set is exactly env/nws._derive_category's outputs.""" assert FAIL_CLOSED_CATEGORIES == { "weather_warning", "weather_watch", "weather_advisory", "weather_statement", } # -- THE REAL LA LEAK: zone-only advisory, no polygon, no centroid -------- def test_zone_only_advisory_no_geometry_dropped_failclosed(self): """REAL repro of the leak: a zone-only NWS Heat Advisory with NO polygon and NO centroid, under an Idaho-only coverage area, is now DROPPED (fail-closed). Previously the fail-open gate KEPT it → wrong-region leak. """ flt, received = self._make_filter() event = make_event( source="nws", category="weather_advisory", severity="routine", title="Heat Advisory issued for Los Angeles County", # zone-only: geometry is explicitly None, and there is NO lat/lon data={"geometry": None}, ) assert event.lat is None and event.lon is None # truly unlocatable flt.handle(event) assert len(received) == 0, ( "zone-only weather advisory with no geometry must be DROPPED " "(fail-closed) — this is the exact LA leak" ) def test_zone_only_statement_no_geometry_dropped_failclosed(self): """A weather_statement (Special Weather Statement) with no geometry is also fail-closed — all four NWS categories are covered.""" flt, received = self._make_filter() event = make_event( source="nws", category="weather_statement", severity="routine", title="Special Weather Statement", data={"geometry": None}, ) flt.handle(event) assert len(received) == 0 # -- out-of-bounds real polygon (CA) ------------------------------------- def test_ca_polygon_advisory_dropped_out_of_bounds(self): flt, received = self._make_filter() ca_geom = {"type": "Polygon", "coordinates": [CA_POLYGON_COORDS]} event = make_event( source="nws", category="weather_advisory", severity="routine", title="CA advisory", data={"geometry": ca_geom}, ) flt.handle(event) assert len(received) == 0, "out-of-bounds CA polygon must be dropped" # -- in-bounds real polygon (ID) ----------------------------------------- def test_idaho_polygon_advisory_kept_in_bounds(self): flt, received = self._make_filter() event = make_event( source="nws", category="weather_advisory", severity="routine", title="ID advisory", data={"geometry": ID_POLYGON_GEOM}, ) flt.handle(event) assert len(received) == 1, "in-bounds Idaho polygon must be kept" def test_idaho_multipolygon_advisory_kept_in_bounds(self): flt, received = self._make_filter() event = make_event( source="nws", category="weather_advisory", severity="routine", title="ID multipolygon advisory", data={"geometry": ID_MULTIPOLYGON_GEOM}, ) flt.handle(event) assert len(received) == 1, "in-bounds Idaho MultiPolygon must be kept" # -- fail-OPEN preserved for non-weather --------------------------------- def test_non_weather_no_geometry_kept_fail_open(self): """A non-weather event (quake) with no geometry AND no centroid is still KEPT (fail-open unchanged) — proves we did not over-tighten other adapters. Only the FAIL_CLOSED_CATEGORIES are dropped when unlocatable. """ flt, received = self._make_filter() event = make_event( source="usgs", category="earthquake", severity="priority", title="Quake with no geometry", # no lat/lon, no data geometry ) assert event.lat is None and event.lon is None assert event.category not in FAIL_CLOSED_CATEGORIES flt.handle(event) assert len(received) == 1, "non-weather unlocatable event must be KEPT (fail-open)" def test_swpc_no_geometry_kept_fail_open(self): """SWPC (space weather, global) with no geometry stays fail-open.""" flt, received = self._make_filter() event = make_event( source="swpc", category="kp_index", severity="priority", title="Geomagnetic storm — no location", ) flt.handle(event) assert len(received) == 1 # =========================================================================== # NWS adapter — attaches raw alert geometry + MultiPolygon centroid, and the # geometry flows env-event → to_event → Event.data["geometry"] # =========================================================================== def _nws_adapter(): config = MagicMock() config.areas = ["ID"] config.user_agent = "(test, test@example.com)" config.severity_min = "moderate" config.tick_seconds = 60 return NWSAlertsAdapter(config) def _mock_urlopen_with_features(features): """Return a context-manager mock whose .read() yields a geo+json payload.""" payload = json.dumps({"features": features}).encode("utf-8") resp = MagicMock() resp.read.return_value = payload resp.__enter__.return_value = resp resp.__exit__.return_value = False return resp class TestNWSGeometryAttach: def _fetch_one(self, geometry): adapter = _nws_adapter() feature = { "properties": { "id": "urn:oid:nws-test-1", "event": "Heat Advisory", "severity": "Moderate", "headline": "Heat Advisory", "description": "Hot.", "areaDesc": "Test County", }, "geometry": geometry, } resp = _mock_urlopen_with_features([feature]) with patch("meshai.env.nws.urlopen", return_value=resp): adapter._fetch() events = adapter.get_events() assert len(events) == 1 return adapter, events[0] def test_polygon_geometry_attached_to_event(self): """The parsed env event carries event['geometry'] = the feature geometry.""" _, event = self._fetch_one(ID_POLYGON_GEOM) assert event["geometry"] == ID_POLYGON_GEOM def test_polygon_centroid_computed(self): _, event = self._fetch_one(ID_POLYGON_GEOM) # centroid of the ID polygon ring ~ (42.9N, -114.2W) — inside Idaho assert 42.0 <= event["lat"] <= 44.0 assert -117.0 <= event["lon"] <= -111.0 def test_multipolygon_geometry_attached_and_centroid_computed(self): """MultiPolygon: geometry attached verbatim + centroid from first polygon's outer ring (must land in Idaho, not the far-away 2nd polygon). """ _, event = self._fetch_one(ID_MULTIPOLYGON_GEOM) assert event["geometry"] == ID_MULTIPOLYGON_GEOM # first polygon is the Idaho one → centroid inside Idaho assert 42.0 <= event["lat"] <= 44.0 assert -117.0 <= event["lon"] <= -111.0 def test_zone_only_geometry_is_none(self): """A zone-only alert (no geometry) yields event['geometry'] is None and NO centroid — the exact unlocatable shape the gate fails closed on.""" _, event = self._fetch_one(None) assert event["geometry"] is None assert "lat" not in event and "lon" not in event def test_to_event_carries_geometry_into_data(self): """Trace: env event['geometry'] → to_event → Event.data['geometry'].""" adapter, event = self._fetch_one(ID_POLYGON_GEOM) pipeline_event = adapter.to_event(event) assert pipeline_event.data["geometry"] == ID_POLYGON_GEOM def test_to_event_carries_none_geometry_for_zone_only(self): adapter, event = self._fetch_one(None) pipeline_event = adapter.to_event(event) assert pipeline_event.data["geometry"] is None