mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
feat(region-routing): tag satpass events by observer coordinates so they region-route; add satpass to VALID_TOGGLES (#90)
Co-authored-by: Matt Johnson <mj@k7zvx.com>
This commit is contained in:
parent
3be54fa2df
commit
9b6053365a
3 changed files with 293 additions and 3 deletions
247
work/tests/test_satpass_region_tagging.py
Normal file
247
work/tests/test_satpass_region_tagging.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
"""Tests for satpass observer-based region tagging (coverage_area.py).
|
||||
|
||||
Covers:
|
||||
- observer_region_names: single observer in a region, two observers in different
|
||||
regions (union, config order, deduped), slug not found / outside all areas.
|
||||
- event_region_names: geometry path takes priority; falls back to observer path
|
||||
only when geometry yields nothing.
|
||||
- categories: satpass is now a first-class VALID_TOGGLE; categories_for_toggle
|
||||
returns ["sat_pass"].
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from meshai.coverage_area import (
|
||||
MonitoringArea,
|
||||
event_region_names,
|
||||
observer_region_names,
|
||||
)
|
||||
from meshai.notifications.categories import (
|
||||
VALID_TOGGLES,
|
||||
all_toggles,
|
||||
categories_for_toggle,
|
||||
)
|
||||
from meshai.notifications.events import make_event
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reference areas — named bboxes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SC_IDAHO = MonitoringArea(north=43.0, south=42.0, east=-113.5, west=-115.5, name="SC Idaho")
|
||||
MAGIC_VALLEY = MonitoringArea(north=43.2, south=42.3, east=-114.0, west=-115.0, name="Magic Valley")
|
||||
# Unnamed area — must never appear in results
|
||||
UNNAMED = MonitoringArea(north=44.0, south=43.0, east=-113.0, west=-116.0, name=None)
|
||||
|
||||
AREAS = [SC_IDAHO, MAGIC_VALLEY, UNNAMED]
|
||||
|
||||
# Coordinates inside SC_IDAHO (Twin Falls ~42.56N, -114.47W)
|
||||
TF_LAT, TF_LON = 42.56, -114.47
|
||||
|
||||
# Coordinates inside MAGIC_VALLEY but NOT SC_IDAHO
|
||||
MV_LAT, MV_LON = 42.5, -114.5 # falls in both SC_IDAHO and MAGIC_VALLEY per overlap
|
||||
# Use a point that is inside MAGIC_VALLEY bbox only, outside SC_IDAHO:
|
||||
# SC_IDAHO west=-115.5 east=-113.5 magic-valley west=-115.0 east=-114.0
|
||||
# A point at lon=-114.8 (between -115.0 and -114.0) and lat=42.7 (inside both bboxes)
|
||||
# Let's use distinct non-overlapping coords:
|
||||
# SC_IDAHO 42.0–43.0 N, -115.5–-113.5 W
|
||||
# MAGIC_VALLEY 42.3–43.2 N, -115.0–-114.0 W
|
||||
# These boxes overlap. Use a point in SC_IDAHO only (lat=42.1, lon=-115.2):
|
||||
SC_ONLY_LAT, SC_ONLY_LON = 42.1, -115.2 # south of MAGIC_VALLEY (42.3 min)
|
||||
# And a point in MAGIC_VALLEY only (lat=43.1, lon=-114.3 — north of SC_IDAHO 43.0 max):
|
||||
MV_ONLY_LAT, MV_ONLY_LON = 43.1, -114.3
|
||||
|
||||
# Outside everything
|
||||
OUT_LAT, OUT_LON = 34.0, -118.0 # Los Angeles
|
||||
|
||||
|
||||
def _fake_observers(entries):
|
||||
"""Return a patcher for get_observers that yields `entries`."""
|
||||
return patch(
|
||||
"meshai.coverage_area.observer_region_names.__code__", # won't work — use module path
|
||||
)
|
||||
|
||||
|
||||
def _patch_observers(entries):
|
||||
return patch(
|
||||
"meshai.persistence.observer_locations.get_observers",
|
||||
return_value=entries,
|
||||
)
|
||||
|
||||
|
||||
def _make_satpass_event(observer_list_str, **kwargs):
|
||||
"""Build a minimal satpass Event-like object with no geometry."""
|
||||
return make_event(
|
||||
source="satpass",
|
||||
category="sat_pass",
|
||||
severity="routine",
|
||||
title="ISS Pass",
|
||||
data={"observer_list": observer_list_str},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# observer_region_names — core unit tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestObserverRegionNames:
|
||||
def test_single_observer_in_one_region(self):
|
||||
event = _make_satpass_event("twin-falls")
|
||||
with _patch_observers([{"slug": "twin-falls", "lat": SC_ONLY_LAT, "lon": SC_ONLY_LON}]):
|
||||
result = observer_region_names(event, AREAS)
|
||||
assert result == ["SC Idaho"]
|
||||
|
||||
def test_two_observers_in_different_regions_union_config_order(self):
|
||||
"""Two observers in SC Idaho and Magic Valley (non-overlapping points)
|
||||
→ both region names returned in config order (SC Idaho first, then MV)."""
|
||||
event = _make_satpass_event("obs-a,obs-b")
|
||||
fake_obs = [
|
||||
{"slug": "obs-a", "lat": SC_ONLY_LAT, "lon": SC_ONLY_LON}, # SC Idaho only
|
||||
{"slug": "obs-b", "lat": MV_ONLY_LAT, "lon": MV_ONLY_LON}, # Magic Valley only
|
||||
]
|
||||
with _patch_observers(fake_obs):
|
||||
result = observer_region_names(event, AREAS)
|
||||
assert result == ["SC Idaho", "Magic Valley"]
|
||||
|
||||
def test_deduplication_same_region_multiple_observers(self):
|
||||
"""Two observers both inside SC Idaho → region appears only once."""
|
||||
event = _make_satpass_event("obs-1,obs-2")
|
||||
fake_obs = [
|
||||
{"slug": "obs-1", "lat": SC_ONLY_LAT, "lon": SC_ONLY_LON},
|
||||
{"slug": "obs-2", "lat": 42.2, "lon": -115.0}, # also inside SC Idaho
|
||||
]
|
||||
with _patch_observers(fake_obs):
|
||||
result = observer_region_names(event, AREAS)
|
||||
assert result.count("SC Idaho") == 1
|
||||
|
||||
def test_unknown_slug_returns_empty(self):
|
||||
"""Observer slug not in get_observers → []."""
|
||||
event = _make_satpass_event("nonexistent-slug")
|
||||
with _patch_observers([{"slug": "other", "lat": TF_LAT, "lon": TF_LON}]):
|
||||
result = observer_region_names(event, AREAS)
|
||||
assert result == []
|
||||
|
||||
def test_observer_outside_all_areas_returns_empty(self):
|
||||
"""Observer coords outside all configured areas → []."""
|
||||
event = _make_satpass_event("la-observer")
|
||||
with _patch_observers([{"slug": "la-observer", "lat": OUT_LAT, "lon": OUT_LON}]):
|
||||
result = observer_region_names(event, AREAS)
|
||||
assert result == []
|
||||
|
||||
def test_unnamed_area_never_in_result(self):
|
||||
"""Unnamed MonitoringArea (name=None) never contributes to output."""
|
||||
event = _make_satpass_event("obs")
|
||||
# Point inside UNNAMED bbox (lat=43.5, lon=-114.5) — outside SC Idaho / MV:
|
||||
fake_obs = [{"slug": "obs", "lat": 43.5, "lon": -114.5}]
|
||||
with _patch_observers(fake_obs):
|
||||
result = observer_region_names(event, AREAS)
|
||||
assert result == []
|
||||
|
||||
def test_no_observer_list_returns_empty(self):
|
||||
"""Event without observer_list key → []."""
|
||||
event = make_event(
|
||||
source="satpass", category="sat_pass", severity="routine",
|
||||
title="Pass", data={},
|
||||
)
|
||||
with _patch_observers([]):
|
||||
result = observer_region_names(event, AREAS)
|
||||
assert result == []
|
||||
|
||||
def test_empty_areas_returns_empty(self):
|
||||
"""No configured areas → []."""
|
||||
event = _make_satpass_event("obs")
|
||||
with _patch_observers([{"slug": "obs", "lat": TF_LAT, "lon": TF_LON}]):
|
||||
result = observer_region_names(event, [])
|
||||
assert result == []
|
||||
|
||||
def test_get_observers_exception_returns_empty_failopen(self):
|
||||
"""If get_observers raises, observer_region_names returns [] (fail-open)."""
|
||||
event = _make_satpass_event("obs")
|
||||
with patch(
|
||||
"meshai.persistence.observer_locations.get_observers",
|
||||
side_effect=RuntimeError("db gone"),
|
||||
):
|
||||
result = observer_region_names(event, AREAS)
|
||||
assert result == []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# event_region_names — geometry takes priority; observer fallback for satpass
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestEventRegionNames:
|
||||
def test_event_with_geometry_uses_geometry_not_observers(self):
|
||||
"""An event with a polygon in SC Idaho must tag via geometry; the
|
||||
observer fallback must NOT be consulted (and if it were, it would
|
||||
return a different/no region because the observer slug is absent)."""
|
||||
geom = {
|
||||
"type": "Point",
|
||||
"coordinates": [SC_ONLY_LON, SC_ONLY_LAT], # [lon, lat]
|
||||
}
|
||||
event = make_event(
|
||||
source="satpass", category="sat_pass", severity="routine",
|
||||
title="Pass",
|
||||
lat=SC_ONLY_LAT, lon=SC_ONLY_LON,
|
||||
data={"observer_list": "no-such-slug"},
|
||||
)
|
||||
# With a lat/lon centroid, _event_geom_json will produce a Point.
|
||||
# Even though the observer slug won't resolve, the geometry path fires first.
|
||||
# SC_ONLY_LAT=42.1, SC_ONLY_LON=-115.2 is inside SC_IDAHO bbox.
|
||||
result = event_region_names(event, AREAS)
|
||||
assert "SC Idaho" in result
|
||||
|
||||
def test_satpass_event_no_geometry_falls_back_to_observers(self):
|
||||
"""Satpass event with NO lat/lon and NO geometry data → geometry path
|
||||
yields [], observer fallback kicks in and returns correct region."""
|
||||
event = make_event(
|
||||
source="satpass", category="sat_pass", severity="routine",
|
||||
title="ISS Pass",
|
||||
data={"observer_list": "boise-obs"},
|
||||
# deliberately no lat/lon
|
||||
)
|
||||
assert event.lat is None
|
||||
assert event.lon is None
|
||||
fake_obs = [{"slug": "boise-obs", "lat": SC_ONLY_LAT, "lon": SC_ONLY_LON}]
|
||||
with _patch_observers(fake_obs):
|
||||
result = event_region_names(event, AREAS)
|
||||
assert result == ["SC Idaho"]
|
||||
|
||||
def test_event_with_geometry_in_region_uses_geometry_observers_not_consulted(self):
|
||||
"""When geometry tags a region, the observer path must not run — even if
|
||||
observers would tag a different region."""
|
||||
# Event has a geometry that puts it in SC Idaho
|
||||
event = make_event(
|
||||
source="satpass", category="sat_pass", severity="routine",
|
||||
title="Pass",
|
||||
lat=SC_ONLY_LAT, lon=SC_ONLY_LON,
|
||||
data={"observer_list": "mv-obs"},
|
||||
)
|
||||
# Observer is in Magic Valley (MV_ONLY) — if observer path ran it would
|
||||
# return Magic Valley. But geometry path should win: SC Idaho.
|
||||
fake_obs = [{"slug": "mv-obs", "lat": MV_ONLY_LAT, "lon": MV_ONLY_LON}]
|
||||
with _patch_observers(fake_obs):
|
||||
result = event_region_names(event, AREAS)
|
||||
# Geometry (lat/lon centroid at SC_ONLY) fires first → SC Idaho
|
||||
assert "SC Idaho" in result
|
||||
# Magic Valley should NOT be in result (observer path not reached)
|
||||
assert "Magic Valley" not in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# categories.py — satpass is a first-class VALID_TOGGLE
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestSatpassToggle:
|
||||
def test_satpass_in_valid_toggles(self):
|
||||
assert "satpass" in VALID_TOGGLES
|
||||
|
||||
def test_satpass_in_all_toggles(self):
|
||||
assert "satpass" in all_toggles()
|
||||
|
||||
def test_categories_for_toggle_satpass_returns_sat_pass(self):
|
||||
result = categories_for_toggle("satpass")
|
||||
assert result == ["sat_pass"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue