"""v0.5.7-weather: NWS HTML strip + ALERT_CATEGORIES weather audit. Covers three things shipped in v0.5.7-weather: 1. strip_html_tags() — NWS data.description / data.instruction arrive as raw HTML (per Central guide §Surprise 3). Verify tags are stripped, entities decoded, paragraph breaks become spaces, plain text is a no-op. 2. compose_mesh_message() integration — an Event whose title contains HTML produces a clean LoRa string (no literal
/
).
3. Weather category parity — ALERT_CATEGORIES{toggle=weather} is exactly the
set that nws.py._derive_category() can emit. Fail loudly if either side
drifts so the weather family stays "every event meshai sees is selectable".
"""
import inspect
import pytest
from meshai.notifications.categories import ALERT_CATEGORIES
from meshai.notifications.events import make_event
from meshai.notifications.renderers.composer import (
compose_mesh_message,
strip_html_tags,
)
# ---------- strip_html_tags() ----------------------------------------------
def test_strip_html_tags_removes_simple_tags():
assert strip_html_tags("
Severe
") == "Severe" def test_strip_html_tags_br_becomes_space(): #hello
world
") == "hello world" def test_strip_html_tags_decodes_entities(): assert strip_html_tags("Wind gusts 25 & 35 mph") == "Wind gusts 25 & 35 mph" # decodes to U+00A0 which the whitespace collapse normalizes to a # regular space — tight ASCII whitespace is what we want on LoRa. assert strip_html_tags("Twin Falls County") == "Twin Falls County" assert strip_html_tags("12 — 35 mph") == "12 — 35 mph" def test_strip_html_tags_nested_and_attrs(): raw = 'Tornado WARNING
line 1
\nline\t2
" assert strip_html_tags(raw) == "line 1 line 2" # ---------- compose_mesh_message integration ------------------------------- def test_compose_mesh_message_strips_html_in_title(): event = make_event( source="nws", category="weather_warning", severity="priority", title="Severe Thunderstorm Warning
", summary="", region="Twin Falls", ) line = compose_mesh_message(event) # No literal markup escapes onto the wire. assert "<" not in line assert "" not in line assert "Severe Thunderstorm Warning" in line def test_compose_mesh_message_strips_html_with_entities_and_br(): event = make_event( source="nws", category="weather_advisory", severity="routine", title="Wind Advisory —Special Weather Statement
", ) line = compose_mesh_message(event) assert "<" not in line assert "Special Weather Statement" in line # ---------- ALERT_CATEGORIES weather audit --------------------------------- def _nws_emitted_categories() -> set[str]: """Walk nws.py source for every literal returned by _derive_category(). Reflection-style audit: read the method body's source and collect the quoted return values. Keeps the test honest if someone adds a 5th branch without thinking about ALERT_CATEGORIES. """ from meshai.env.nws import NWSAlertsAdapter src = inspect.getsource(NWSAlertsAdapter._derive_category) import re return set(re.findall(r'return\s+"([a-z_]+)"', src)) def test_nws_emits_exactly_four_weather_categories(): emitted = _nws_emitted_categories() assert emitted == { "weather_warning", "weather_watch", "weather_advisory", "weather_statement", }, f"nws.py emission set drifted: {emitted}" def test_alert_categories_weather_complete(): """Every weather category nws.py can emit must exist in ALERT_CATEGORIES with toggle='weather'. Anything tagged toggle='weather' that nws.py cannot emit is an orphan (no UI selectable event would ever surface it). """ registry_weather = { cid for cid, info in ALERT_CATEGORIES.items() if info.get("toggle") == "weather" } emitted = _nws_emitted_categories() missing = emitted - registry_weather orphans = registry_weather - emitted assert not missing, f"nws.py emits categories missing from ALERT_CATEGORIES: {missing}" assert not orphans, f"ALERT_CATEGORIES has orphan weather entries: {orphans}" @pytest.mark.parametrize( "cat", ["weather_warning", "weather_watch", "weather_advisory", "weather_statement"], ) def test_weather_categories_have_required_fields(cat): info = ALERT_CATEGORIES[cat] assert info["toggle"] == "weather" assert info["name"] assert info["description"] assert info["default_severity"] in {"routine", "priority", "immediate"} assert info["example_message"]