meshai/work/tests/test_nws_refactor.py
Matt Johnson 6aec582fcb chore: remove central.budget re-export shim
The shim's implementation lived at notifications.formatters._budget from
the start; central.budget was only a 9-line re-export kept around for
import-path compatibility. Point every importer directly at the real
module and delete the shim:

- notifications/renderers/composer.py: lazy import inside a function
- central/wfigs_handler.py, central/satpass_handler.py: import line only
- tests/test_fire_refactor.py, test_nws_refactor.py, test_firms_refactor.py:
  import line only, no behavior change

test_budget_shim.py existed solely to assert identity-equality between
the shim and the real module; with the shim gone there is nothing left
for it to test, so it is deleted too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 21:03:23 +00:00

689 lines
29 KiB
Python

"""NWS refactor tests — formatter+gater architecture verification.
Originally four test groups; groups 1-3 below were golden-parity tests
against the now-deleted Central NATS-consumer bridge
(meshai.central.nws_handler / handle_nws / _render). That bridge is dead —
production runs the native formatter+gater path exclusively — so byte-parity
and old-vs-new comparisons against it no longer have anything to compare
against and were deleted (git history preserves the original handler and
the parity tests that proved the rewrite matched it). What remains exercises
the LIVE native path only:
1. Formatter golden: formatters.nws.format() renders the expected wire text
for real fixtures and pathological synthetic cases (SVR path-sampling,
dangling-separator regression, TOR/FFW branches). These goldens are
hardcoded literals, NOT computed by importing the deleted handler. They
were derived by temporarily restoring the pre-excision
meshai.central.nws_handler._render() from git history (ca751fb5^) in a
throwaway script, confirming it produced byte-identical output to the
current native format() for every case below, and pinning the resulting
string as the literal. That verification script/module was never
committed; only the confirmed-matching literals live here. See "golden
verified against pre-excision _render()" comments below.
2. Gate-sequence: replay a synthetic 4-step lifecycle (first→dup<3h→
dup>3h→Cancel) through gating.nws.decide(), and a reference-triggered
"Update" prefix case — both against native code only.
3. Schema-conformance: env/nws.py _fetch() emits all canonical schema keys;
description is not truncated; to_event() produces a canonical event.data.
4. Formatter/gater registration: formatters/__init__ and gating/__init__
register the NWS categories against the native format()/decide().
"""
from __future__ import annotations
import time
from datetime import datetime
import pytest
from meshai.notifications.formatters._budget import budget_for
from meshai.notifications.formatters.nws import format as nws_format
from meshai.notifications.gating.nws import decide as nws_decide
from meshai.persistence import close_thread_connection, init_db
from meshai.persistence import db as persistence_db
from tests.harness.goldens import assert_byte_identical, load_fixtures, pinned_tz
# ── DB fixture ────────────────────────────────────────────────────────────────
@pytest.fixture
def mem_db(monkeypatch, tmp_path):
db_path = str(tmp_path / "nws-refactor-test.sqlite")
monkeypatch.setenv("MESHAI_DB_PATH", db_path)
persistence_db._initialised.clear()
close_thread_connection()
conn = init_db()
yield conn
close_thread_connection()
persistence_db._initialised.discard(db_path)
class _FakeEvent:
"""Minimal fake Event for calling the formatter without the full pipeline."""
def __init__(self, data: dict):
self.data = data
def _canonical(event_type, *, same_code="", area_desc="Twin Falls County",
county="Twin Falls", state="ID", expires_epoch=1_751_400_000,
certainty="Observed", parameters=None, description="",
prefix="") -> dict:
"""Build a canonical event.data dict as the native adapter's to_event()
(or the decider's data_patch) would produce it, for feeding directly to
nws_format()."""
return {
"cap_id": "test",
"event": event_type,
"same_code": same_code,
"cap_severity": None,
"certainty": certainty,
"expires_at": expires_epoch,
"area_desc": area_desc,
"geocoder": {"city": None, "county": county, "state": state},
"description": description,
"parameters": parameters or {},
"msgType": "Alert",
"references": [],
"category": "",
"headline": None,
"_nws_prefix": prefix,
}
# =============================================================================
# 1. Formatter golden — native format() wire text
# =============================================================================
class TestFormatterGolden:
"""formatters.nws.format() renders the expected wire text.
Fixture-driven cases (golden verified against pre-excision _render(),
see module docstring) plus hand-built pathological cases that pin
known-tricky behavior: SVR path-sampling, the "no dangling separator"
regression, and the TOR/FFW hazard branches.
"""
def _canonical_from_fixture(self, fix: dict) -> dict:
"""Extract canonical event.data from a Central-style NWS fixture.
Standalone re-implementation of the field extraction that used to
live in the deleted meshai.central.nws_handler (event-type fallback
via category, ISO-to-epoch parsing) — kept here only as test
scaffolding to turn a raw fixture into a canonical dict.
"""
envelope = fix["envelope"]
inner = envelope.get("data") or {}
d = inner.get("data") or {}
ge = (d.get("_enriched") or {}).get("geocoder") or {}
category_raw = inner.get("category") or ""
event_type = d.get("event") or "Weather Alert"
area_desc = d.get("areaDesc")
county = d.get("areaDesc") or ge.get("county")
state = ge.get("state") or d.get("state")
expires_epoch = _parse_iso(d.get("expires"))
same_code = ((d.get("eventCode") or {}).get("SAME") or [""])[0]
return {
"cap_id": d.get("id") or inner.get("id"),
"event": event_type,
"same_code": same_code,
"cap_severity": d.get("severity"),
"certainty": d.get("certainty") or "",
"expires_at": expires_epoch,
"area_desc": area_desc,
"geocoder": {"city": ge.get("city"), "county": county, "state": state},
"description": d.get("description"),
"parameters": d.get("parameters") or {},
"msgType": d.get("msgType"),
"references": d.get("references") or [],
"category": category_raw,
"headline": d.get("headline"),
"_nws_prefix": "",
}
@pytest.mark.parametrize("n,expected", [
(0, "🌬️ Special Weather Statement\nUntil 5:45 PM MDT — Northern Elko County"
"\nLandspouts, 40mph gusts, and half inch hail · observed"
"\nMoving W 23 mph"),
(8, "⛈️ Severe Thunderstorm Warning\nUntil 4:30 PM MDT — Cassia, ID"
"\nup to 50mph winds, 1\" hail · radar"
"\nMoving SW 20 mph"),
(9, "🌩️ Severe Thunderstorm Warning\nUntil 4:30 PM MDT — Cassia, ID"
"\n1\" hail · observed"
"\nMoving SW 20 mph — Oakley Reservoir and Oakley"),
])
def test_fixture_golden(self, n, expected):
"""Real nws/ fixtures render to the pinned wire text.
golden verified against pre-excision _render() (see module docstring).
"""
fixes = load_fixtures("nws")
fix = fixes[n]
epoch = float(fix.get("captured_epoch", 1_783_206_513.0))
canonical = self._canonical_from_fixture(fix)
budget = budget_for("nws")
with pinned_tz("America/Boise"):
result = nws_format(_FakeEvent(canonical), now=epoch, budget=budget)
assert_byte_identical(result, expected)
def test_svr_long_locations_path_sampled(self):
"""SVR with a long town list renders a PATH SAMPLE (first → last),
never a tail-drop that silently loses the final town.
golden verified against pre-excision _render() (see module docstring).
"""
long_locations = (
"Buhl, Eden, Hazelton, Murtaugh, Richfield, Dietrich, "
"Gooding, Hagerman, Wendell, and Shoshone"
)
description = (
"HAZARD...Damaging winds to 60 mph and quarter-size hail.\n\n"
f"Locations impacted include...{long_locations}"
)
canonical = _canonical(
"Severe Thunderstorm Warning", same_code="SVR",
certainty="Observed", description=description,
parameters={
"maxWindGust": ["60 MPH"], "maxHailSize": ["1.00"],
"eventMotionDescription": ["2200000T254DEG...35KT 42.5,-114.5"],
},
)
expected = (
"⛈️ Severe Thunderstorm Warning\nUntil 2:00 PM MDT — Twin Falls County"
"\n60mph winds, 1\" hail · radar"
"\nMoving W 40 mph — Buhl → Shoshone"
)
with pinned_tz("America/Boise"):
result = nws_format(_FakeEvent(canonical), now=1_751_400_000,
budget=budget_for("nws"))
assert len(result) <= 140
assert "" in result, "long town list must be path-sampled"
assert "Buhl" in result and "Shoshone" in result
assert_byte_identical(result, expected)
def test_svr_short_locations_shown_in_full(self):
"""SVR with a short town list shows the FULL comma-joined list —
never the path-sample arrow.
golden verified against pre-excision _render() (see module docstring).
"""
description = (
"HAZARD...Damaging winds to 60 mph and quarter-size hail.\n\n"
"Locations impacted include...Buhl, Eden, and Hazelton"
)
canonical = _canonical(
"Severe Thunderstorm Warning", same_code="SVR",
certainty="Observed", description=description,
parameters={
"maxWindGust": ["60 MPH"], "maxHailSize": ["1.00"],
"eventMotionDescription": ["2200000T254DEG...35KT 42.5,-114.5"],
},
)
expected = (
"⛈️ Severe Thunderstorm Warning\nUntil 2:00 PM MDT — Twin Falls County"
"\n60mph winds, 1\" hail · radar"
"\nMoving W 40 mph — Buhl, Eden, Hazelton"
)
with pinned_tz("America/Boise"):
result = nws_format(_FakeEvent(canonical), now=1_751_400_000,
budget=budget_for("nws"))
assert "" not in result, "short list must not be path-sampled"
assert_byte_identical(result, expected)
def test_sps_no_dangling_separator(self):
"""Regression: an SPS with wind+motion+long town list must never
collapse to a trailing bare em-dash ('Moving SW 24 mph —…').
golden verified against pre-excision _render() (see module docstring).
"""
long_locations = (
"Twin Falls, Kimberly, Filer, Buhl, Hansen, Murtaugh, Hollister, "
"Eden, Hazelton, and Rogerson"
)
description = (
"HAZARD...Wind gusts in excess of 45 mph and pea size hail.\n\n"
"SOURCE...Radar indicated.\n\n"
f"Locations impacted include...{long_locations}"
)
canonical = _canonical(
"Special Weather Statement", same_code="SPS",
certainty="Observed", description=description,
parameters={"eventMotionDescription": ["2200000T225DEG...21KT 42.5,-114.5"]},
)
expected = (
"🌬️ Special Weather Statement\nUntil 2:00 PM MDT — Twin Falls County"
"\n45mph gusts and 0.25\" hail · observed"
"\nMoving SW 24 mph — Twin Falls"
)
with pinned_tz("America/Boise"):
result = nws_format(_FakeEvent(canonical), now=1_751_400_000,
budget=budget_for("nws"))
assert len(result) <= 140
for line in result.splitlines():
stripped = line.rstrip()
assert not stripped.endswith(""), f"bare trailing em-dash: {line!r}"
assert "—…" not in stripped and "— …" not in stripped
assert "45mph gusts" in result, "wind hazard not tightened"
assert '0.25" hail' in result, "hail not rendered numerically ('pea' -> 0.25\")"
assert "in excess of" not in result, "filler phrase survived tightening"
assert_byte_identical(result, expected)
def test_sps_pathological_towns_degrade_to_motion_only(self):
"""When not even one sampled town fits the remaining budget, line 4
degrades to motion-only — never a dangling separator.
golden verified against pre-excision _render() (see module docstring).
"""
long_town = "Averyverylongimpossibletownnamethatwillnotfitthebudgetatall" * 2
description = (
"HAZARD...Wind gusts in excess of 45 mph.\n\n"
f"Locations impacted include...{long_town}"
)
canonical = _canonical(
"Special Weather Statement", same_code="SPS",
certainty="Observed", description=description,
parameters={"eventMotionDescription": ["2200000T225DEG...21KT 42.5,-114.5"]},
)
expected = (
"🌬️ Special Weather Statement\nUntil 2:00 PM MDT — Twin Falls County"
"\n45mph gusts · observed"
"\nMoving SW 24 mph"
)
with pinned_tz("America/Boise"):
result = nws_format(_FakeEvent(canonical), now=1_751_400_000,
budget=budget_for("nws"))
assert len(result) <= 140
last = result.splitlines()[-1]
if last.startswith("Moving"):
assert "" not in last, f"expected motion-only, got: {last!r}"
assert_byte_identical(result, expected)
def test_tor_observed_on_ground_with_damage_threat(self):
"""TOR branch: OBSERVED detection -> 'on ground'; damage threat appended.
golden verified against pre-excision _render() (see module docstring).
"""
canonical = _canonical(
"Tornado Warning", same_code="TOR", certainty="Observed",
description="TORNADO...OBSERVED\n\nLocations impacted include...Twin Falls.",
parameters={"tornadoDetection": ["OBSERVED"],
"tornadoDamageThreat": ["Considerable"]},
)
expected = (
"🌪️ Tornado Warning\nUntil 2:00 PM MDT — Twin Falls County"
"\ntornado on ground · considerable damage"
"\nTwin Falls"
)
with pinned_tz("America/Boise"):
result = nws_format(_FakeEvent(canonical), now=1_751_400_000,
budget=budget_for("nws"))
assert_byte_identical(result, expected)
def test_tor_radar_indicated_no_threat(self):
"""TOR branch: non-OBSERVED detection -> 'radar'; no threat segment
when tornadoDamageThreat is empty.
golden verified against pre-excision _render() (see module docstring).
"""
canonical = _canonical(
"Tornado Warning", same_code="TOR", certainty="Possible",
description="TORNADO...RADAR INDICATED\n\nLocations impacted include...Buhl.",
parameters={"tornadoDetection": ["RADAR INDICATED"], "tornadoDamageThreat": []},
)
expected = (
"🌪️ Tornado Warning\nUntil 2:00 PM MDT — Twin Falls County"
"\ntornado radar"
"\nBuhl"
)
with pinned_tz("America/Boise"):
result = nws_format(_FakeEvent(canonical), now=1_751_400_000,
budget=budget_for("nws"))
assert_byte_identical(result, expected)
def test_ffw_thunderstorm_flood_cause(self):
"""FFW/FLW branch: flood-cause keyword ('thunderstorm') is appended
as a ' · thunderstorms' segment.
golden verified against pre-excision _render() (see module docstring).
"""
canonical = _canonical(
"Flash Flood Warning", same_code="FFW", certainty="Observed",
description=("HAZARD...Flash flooding caused by thunderstorms. Excessive "
"runoff will result in flooding of small creeks.\n\n"
"Locations impacted include...Twin Falls."),
parameters={},
)
expected = (
"🌊 Flash Flood Warning\nUntil 2:00 PM MDT — Twin Falls County"
"\nFlash flooding caused by thunderstorms · thunderstorms"
"\nTwin Falls"
)
with pinned_tz("America/Boise"):
result = nws_format(_FakeEvent(canonical), now=1_751_400_000,
budget=budget_for("nws"))
assert_byte_identical(result, expected)
# =============================================================================
# 1. Gate-sequence: native gating/nws.decide() only
# =============================================================================
def _parse_iso(s):
"""Parse a CAP ISO datetime string to an epoch int (or None).
Standalone equivalent of the now-deleted meshai.central.nws_handler
._parse_iso, kept here only as test scaffolding for building canonical
dicts to feed nws_decide().
"""
if not s:
return None
try:
return int(datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp())
except Exception:
return None
class TestGateSequence:
"""Replay a 4-step lifecycle through the native gating.nws.decide().
Steps:
1. First sighting → broadcast
2. Repeat within 3h window → suppress
3. Repeat outside 3h window → broadcast (Active prefix)
4. Cancel/Expire tombstone → suppress
"""
def _make_envelope(self, cap_id: str, event: str = "Severe Thunderstorm Warning",
msg_type: str = "Alert", references=None) -> dict:
return {
"envelope": {
"data": {
"adapter": "nws",
"category": "wx.alert.severe_thunderstorm_warning",
"severity": 3,
"geo": {"centroid": [-114.46, 42.5], "primary_region": "US-ID"},
"data": {
"id": cap_id,
"event": event,
"severity": "Severe",
"certainty": "Observed",
"areaDesc": "Twin Falls County",
"msgType": msg_type,
"headline": f"{event} for Twin Falls County",
"description": "HAZARD...60 MPH winds.",
"expires": "2026-07-04T03:00:00Z",
"references": references or [],
"parameters": {"maxWindGust": ["60 MPH"], "maxHailSize": ["0.00"]},
"eventCode": {"SAME": ["SVR"]},
},
}
},
"subject": "central.wx.alert.us.id.county.0370011",
"captured_epoch": 1_783_200_000,
}
def _make_canonical(self, fixture: dict) -> dict:
"""Build canonical dict from a fixture for nws_decide().
`_make_envelope` always sets an explicit "event" field, so the
deleted central _category_to_event_type() fallback is never
actually exercised here; "Weather Alert" documents that fallback
without depending on the deleted module.
"""
env = fixture["envelope"]
inner = env.get("data") or {}
d = inner.get("data") or {}
category_raw = inner.get("category") or ""
return {
"cap_id": d.get("id"),
"event": d.get("event") or "Weather Alert",
"same_code": ((d.get("eventCode") or {}).get("SAME") or [""])[0],
"cap_severity": d.get("severity"),
"certainty": d.get("certainty") or "",
"expires_at": _parse_iso(d.get("expires")),
"area_desc": d.get("areaDesc"),
"geocoder": {"city": None, "county": d.get("areaDesc"), "state": None},
"description": d.get("description"),
"parameters": d.get("parameters") or {},
"msgType": d.get("msgType"),
"references": d.get("references") or [],
"category": category_raw,
"headline": d.get("headline"),
}
def test_new_gate_sequence(self, mem_db):
"""4-step lifecycle through NEW nws_decide(): first→dup<3h→dup>3h→Cancel."""
cap_id = "urn:oid:new.gate.001"
t0 = 1_783_200_000.0
t1 = t0 + 1000 # <3h
t2 = t0 + 11000 # >3h
t3 = t0 + 12000
def go(msg_type="Alert", now=t0):
fix = self._make_envelope(cap_id, msg_type=msg_type)
canon = self._make_canonical(fix)
gate = nws_decide(canon, source="nws", now=now)
if gate.broadcast and gate.commit:
gate.commit(now)
return gate
gate1 = go(now=t0)
assert gate1.broadcast is True, f"step1: first sighting broadcast, got: {gate1.reason}"
assert gate1.data_patch.get("_nws_prefix") == "", "step1: first sighting has empty prefix"
gate2 = go(now=t1)
assert gate2.broadcast is False, f"step2: dup within 3h suppressed, got: {gate2.reason}"
gate3 = go(now=t2)
assert gate3.broadcast is True, f"step3: after 3h rebroadcast, got: {gate3.reason}"
assert gate3.data_patch.get("_nws_prefix") == "Active", (
f"step3: rebroadcast prefix 'Active', got: {gate3.data_patch.get('_nws_prefix')!r}"
)
gate4 = go("Cancel", now=t3)
assert gate4.broadcast is False, f"step4: Cancel tombstone suppressed, got: {gate4.reason}"
def test_update_prefix_on_reference(self, mem_db):
"""A new alert that references a previously-broadcast alert gets 'Update' prefix."""
parent_id = "urn:oid:parent.001"
child_id = "urn:oid:child.001"
t0 = 1_783_200_000.0
t1 = t0 + 500
# Broadcast parent first, through the same native nws_decide() path
# used everywhere else in this class (mirrors test_new_gate_sequence).
fix_parent = self._make_envelope(parent_id)
canon_parent = self._make_canonical(fix_parent)
gate_parent = nws_decide(canon_parent, source="nws", now=t0)
assert gate_parent.broadcast is True, "parent should broadcast"
if gate_parent.commit:
gate_parent.commit(t0)
# Child references parent
fix_child = self._make_envelope(
child_id,
references=[{"identifier": parent_id, "sent": "2026-07-04T00:00:00Z",
"effective": "2026-07-04T00:00:00Z"}],
)
canonical = self._make_canonical(fix_child)
gate_child = nws_decide(canonical, source="nws", now=t1)
assert gate_child.broadcast is True, f"child should broadcast: {gate_child.reason}"
assert gate_child.data_patch.get("_nws_prefix") == "Update", (
f"child referencing a broadcast parent should get 'Update' prefix, "
f"got {gate_child.data_patch.get('_nws_prefix')!r}"
)
# =============================================================================
# 2. Schema-conformance: native env/nws.py emits canonical schema
# =============================================================================
class TestSchemaConformance:
"""env/nws.py _fetch() and to_event() emit all canonical schema keys."""
_CANONICAL_KEYS = {
"cap_id", "event", "same_code", "cap_severity", "certainty",
"expires_at", "area_desc", "geocoder", "description", "parameters",
"msgType", "references", "category", "headline",
}
def _make_adapter(self):
from unittest.mock import MagicMock
from meshai.env.nws import NWSAlertsAdapter
cfg = MagicMock()
cfg.areas = ["ID"]
cfg.user_agent = "(test)"
cfg.severity_min = "moderate"
cfg.tick_seconds = 60
return NWSAlertsAdapter(cfg)
def _make_raw(self, description="HAZARD...60 MPH winds.") -> dict:
"""Simulate a _fetch() event dict with all canonical fields."""
expires = time.time() + 3600
return {
"source": "nws",
"event_id": "urn:oid:schema.test.001",
"event_type": "Severe Thunderstorm Warning",
"severity": "severe",
"headline": "SVR Warning",
"description": description,
"onset": time.time(),
"expires": expires,
"expires_at": expires,
"areas": ["IDZ016"],
"area_desc": "Twin Falls County",
"fetched_at": time.time(),
"cap_id": "urn:oid:schema.test.001",
"same_code": "SVR",
"cap_severity": "Severe",
"certainty": "Observed",
"parameters": {"maxWindGust": ["60 MPH"], "maxHailSize": ["1.00"]},
"msgType": "Alert",
"references": [],
}
def test_to_event_emits_all_canonical_keys(self):
"""to_event() produces event.data with all canonical schema keys."""
adapter = self._make_adapter()
raw = self._make_raw()
event = adapter.to_event(raw)
data = event.data
assert isinstance(data, dict), "event.data must be a dict"
missing = self._CANONICAL_KEYS - set(data.keys())
assert not missing, f"event.data missing canonical keys: {missing}"
def test_description_not_truncated(self):
"""to_event() carries FULL description (not truncated to 500 chars)."""
adapter = self._make_adapter()
long_desc = "X" * 1000
raw = self._make_raw(description=long_desc)
event = adapter.to_event(raw)
assert event.data["description"] == long_desc, (
f"description truncated: expected {len(long_desc)} chars, "
f"got {len(event.data['description'])}"
)
def test_geocoder_structure(self):
"""event.data['geocoder'] has city, county, state keys."""
adapter = self._make_adapter()
raw = self._make_raw()
event = adapter.to_event(raw)
geo = event.data.get("geocoder") or {}
assert "city" in geo, "geocoder missing 'city'"
assert "county" in geo, "geocoder missing 'county'"
assert "state" in geo, "geocoder missing 'state'"
def test_same_code_extracted(self):
"""event.data['same_code'] is extracted correctly from raw."""
adapter = self._make_adapter()
raw = self._make_raw()
raw["same_code"] = "SVR"
event = adapter.to_event(raw)
assert event.data["same_code"] == "SVR"
def test_cap_id_present(self):
"""event.data['cap_id'] is the alert identifier."""
adapter = self._make_adapter()
raw = self._make_raw()
event = adapter.to_event(raw)
assert event.data["cap_id"] == "urn:oid:schema.test.001"
def test_parameters_passed_through(self):
"""event.data['parameters'] is the full CAP parameters dict."""
adapter = self._make_adapter()
raw = self._make_raw()
event = adapter.to_event(raw)
params = event.data.get("parameters") or {}
assert "maxWindGust" in params, "parameters.maxWindGust missing"
# =============================================================================
# 3. Formatter registration: weather_warning + weather_statement registered
# =============================================================================
class TestFormatterRegistration:
"""formatters/__init__ and gating/__init__ register NWS categories."""
def test_weather_warning_formatter_registered(self):
from meshai.notifications.formatters import get_formatter
fn = get_formatter("weather_warning")
assert fn is not None, "weather_warning formatter not registered"
assert fn is nws_format, "weather_warning formatter should be nws.format"
def test_weather_statement_formatter_registered(self):
from meshai.notifications.formatters import get_formatter
fn = get_formatter("weather_statement")
assert fn is not None, "weather_statement formatter not registered"
assert fn is nws_format, "weather_statement formatter should be nws.format"
def test_weather_warning_gater_registered(self):
from meshai.notifications.gating import get_decider
fn = get_decider("weather_warning")
assert fn is not None, "weather_warning gater not registered"
assert fn is nws_decide, "weather_warning gater should be gating.nws.decide"
def test_weather_statement_gater_registered(self):
from meshai.notifications.gating import get_decider
fn = get_decider("weather_statement")
assert fn is not None, "weather_statement gater not registered"
assert fn is nws_decide, "weather_statement gater should be gating.nws.decide"
def test_pre_existing_formatters_still_registered(self):
"""Existing Phase-1 registrations must still be present (idempotent append)."""
from meshai.notifications.formatters import get_formatter
from meshai.notifications.formatters import quake as _q
from meshai.notifications.formatters import avalanche as _avy
from meshai.notifications.formatters import swpc as _swpc
assert get_formatter("earthquake_event") is _q.format
assert get_formatter("avalanche_warning") is _avy.format
assert get_formatter("geomagnetic_storm") is _swpc.format
def test_pre_existing_gaters_still_registered(self):
"""Existing Phase-1 gating registrations must still be present."""
from meshai.notifications.gating import get_decider
from meshai.notifications.gating import quake as _q
from meshai.notifications.gating import avalanche as _avy
from meshai.notifications.gating import swpc as _swpc
assert get_decider("earthquake_event") is _q.decide
assert get_decider("avalanche_warning") is _avy.decide
assert get_decider("geomagnetic_storm") is _swpc.decide