] " to every
- payload.message -- including composer output that already starts
- with the family label (e.g. "🚨 ROADS:"). Produced the visually-
- broken duplicate "[Roads] 🚨 ROADS: ..." that Matt observed.
-
-Both bugs predate the v0.5.7 campaign but only manifested when v0.5.7
-was the first to flip Central live with master ON. Both were unit-tested
-in isolation (composer with clean titles, renderer with legacy messages)
-but no integration test exercised the full envelope -> wire path with a
-realistic Central payload. This file fills that gap.
-
-For five representative Central adapter envelopes (one per stream family
-that produces user-facing broadcasts), assert the rendered wire string:
-
- - Does NOT start with "[" (no [Family] legacy prefix).
- - Does NOT contain raw Central category tokens like ".tomtom_incidents",
- ".firms", ".kindex", ".proton_flux" -- those would indicate the
- category-as-title fallback fired.
- - DOES start with the composer's emoji + family label (e.g. "🚨 ",
- "🔥 ", "⚠ ", "🌐 ").
- - Contains the meshai-friendly registry name from ALERT_CATEGORIES
- when the upstream payload lacks a useful title/headline.
-"""
-
-import json
-
-import pytest
-
-from meshai.central.consumer import CentralConsumer
-from meshai.config import EnvironmentalConfig
-from meshai.notifications.events import make_payload_from_event
-from meshai.notifications.pipeline.bus import EventBus
-from meshai.notifications.renderers.composer import compose_mesh_message
-from meshai.notifications.renderers.mesh import MeshRenderer
-from meshai.notifications.categories import ALERT_CATEGORIES
-
-pytestmark = pytest.mark.skip(
- reason="v0.5.13 default-deny removed the v0.5.7-regression title fallback chain. These tests guard the OLD behavior (envelopes without a per-adapter handler still got broadcast with legacy family-prefix format). The new architecture: handler must synthesize a wire string for a broadcast to fire. This entire file is obsolete in v0.5.13.")
-
-
-
-# ---------- Envelope -> Event helper ---------------------------------------
-
-
-def _envelope_to_event(subject: str, envelope: dict):
- """Run a CloudEvents envelope through CentralConsumer._normalize/_handle
- the way it would in production, returning the emitted Event."""
- rec = []
- bus = EventBus(); bus.subscribe(rec.append)
- c = CentralConsumer(EnvironmentalConfig(), bus)
- ev = c._handle(subject, json.dumps(envelope).encode())
- assert ev is not None, f"_handle returned None for subject {subject!r}"
- return ev
-
-
-def _render_to_wire(event) -> str:
- """Run an Event through the dispatcher's composer + renderer path the way
- _dispatch_toggles does for mesh_broadcast / mesh_dm, returning the final
- wire-format string the renderer would hand to the connector."""
- friendly = compose_mesh_message(event)
- assert friendly, "composer returned empty"
- payload = make_payload_from_event(event, message=friendly)
- chunks = MeshRenderer().render(payload)
- assert chunks, "renderer returned no chunks"
- return chunks[0]
-
-
-# ---------- Five-adapter representative envelopes -------------------------
-
-
-# 1. tomtom_incidents -- the exact failure mode Matt observed live
-TOMTOM_ENV = {
- "id": "tt-12345",
- "data": {
- "id": "tt-12345",
- "adapter": "tomtom_incidents",
- "category": "incident.tomtom_incidents",
- "time": "2026-06-04T15:40:00+00:00",
- "severity": 3, # immediate per map_severity (>=3)
- "geo": {"centroid": [-114.0, 42.5], "primary_region": "US-ID",
- "regions": ["US-ID"]},
- # NOTE: tomtom_incidents upstream payload carries per-incident fields
- # like roadway / event_type but NO top-level title or headline. That's
- # the trigger for the v0.5.7-regression cat_raw fallback bug.
- "data": {"roadway": "I-84 EB", "event_type": "crash",
- "delay_seconds": 1800},
- },
-}
-
-# 2. FIRMS hotspot -- VIIRS NOAA-20, high confidence
-FIRMS_ENV = {
- "id": "viirs_noaa20:2026-06-04:0530:43.123:-115.456",
- "data": {
- "id": "viirs_noaa20:2026-06-04:0530:43.123:-115.456",
- "adapter": "firms",
- "category": "fire.hotspot.viirs_noaa20.high",
- "time": "2026-06-04T05:30:00+00:00",
- "severity": 2,
- "geo": {"centroid": [-115.456, 43.123], "primary_region": "US-ID",
- "regions": ["US-ID"]},
- "data": {"latitude": 43.123, "longitude": -115.456,
- "confidence": "high", "frp": 22.5, "satellite": "N20"},
- },
-}
-
-# 3. NWS alert -- explicitly carries headline (positive control)
-NWS_ENV = {
- "id": "urn:oid:2.49.0.1.840.0.abc",
- "data": {
- "id": "urn:oid:2.49.0.1.840.0.abc",
- "adapter": "nws",
- "category": "wx.alert.us.id.severe_thunderstorm_warning",
- "time": "2026-06-04T15:40:00+00:00",
- "severity": 3,
- "geo": {"centroid": [-116.2, 43.6], "primary_region": "US-ID",
- "regions": ["US-ID"]},
- "data": {
- "headline": "Severe Thunderstorm Warning issued June 4 by NWS Boise",
- "description": "The NWS in Boise has issued a Severe Thunderstorm Warning...
",
- "areaDesc": "Ada, ID",
- },
- },
-}
-
-# 4. USGS quake -- carries title (positive control)
-QUAKE_ENV = {
- "id": "us8000mc12",
- "data": {
- "id": "us8000mc12",
- "adapter": "usgs_quake",
- "category": "quake.event.moderate",
- "time": "2026-06-04T12:00:00+00:00",
- "severity": 2,
- "geo": {"centroid": [-114.5, 44.2], "primary_region": "US-ID",
- "regions": ["US-ID"]},
- "data": {"title": "M 4.2 - 23 km ESE of Stanley, ID",
- "magnitude": 4.2, "place": "23 km ESE of Stanley, ID",
- "depth": 8.0, "magType": "ml"},
- },
-}
-
-# 5. SWPC alert -- no title/headline, just message body
-SWPC_ENV = {
- "id": "A20F|2026-04-24 23:50:43.280",
- "data": {
- "id": "A20F|2026-04-24 23:50:43.280",
- "adapter": "swpc_alerts",
- "category": "space.alert",
- "time": "2026-04-24T23:50:43.280Z",
- "severity": 0,
- "geo": {"centroid": None, "primary_region": None, "regions": []},
- "data": {"product_id": "A20F",
- "issue_datetime": "2026-04-24 23:50:43.280",
- "message": "WATCH: Geomagnetic Storm Category G1 Predicted ..."},
- },
-}
-
-CASES = [
- pytest.param(
- "central.traffic.incident.id", TOMTOM_ENV,
- "road_incident", "Road Incident",
- id="tomtom_incidents-no-title-cat-fallback",
- ),
- pytest.param(
- "central.fire.hotspot.viirs_noaa20.high", FIRMS_ENV,
- "wildfire_hotspot", "Wildfire Hotspot",
- id="firms-hotspot-no-title-cat-fallback",
- ),
- pytest.param(
- "central.wx.alert.us.id.severe_thunderstorm_warning", NWS_ENV,
- "weather_warning", None, # NWS supplies headline; friendly name not used
- id="nws-with-headline",
- ),
- pytest.param(
- "central.quake.event.moderate", QUAKE_ENV,
- "earthquake_event", None, # USGS supplies title
- id="quake-with-title",
- ),
- pytest.param(
- "central.space.alert.a20f", SWPC_ENV,
- "rf_propagation_alert", "Space Weather Alert",
- id="swpc-alert-no-title-cat-fallback",
- ),
-]
-
-
-@pytest.mark.parametrize("subject,envelope,expected_cat,expected_friendly_name", CASES)
-def test_wire_string_no_legacy_family_prefix(subject, envelope, expected_cat, expected_friendly_name):
- """No payload should produce a wire string starting with '[' -- the v0.5.0
- debug-format prefix the MeshRenderer used to add and now no longer does."""
- ev = _envelope_to_event(subject, envelope)
- wire = _render_to_wire(ev)
- assert not wire.startswith("["), (
- f"wire string still starts with legacy [Family] prefix: {wire!r}"
- )
-
-
-@pytest.mark.parametrize("subject,envelope,expected_cat,expected_friendly_name", CASES)
-def test_wire_string_no_raw_central_category_leaks(subject, envelope, expected_cat, expected_friendly_name):
- """No wire string should contain a raw Central hierarchical category token
- like '.tomtom_incidents', '.firms', '.kindex', '.proton_flux'. Those would
- indicate the cat_raw fallback fired and the title-fallback fix didn't take."""
- ev = _envelope_to_event(subject, envelope)
- wire = _render_to_wire(ev)
- for leak in (
- ".tomtom_incidents", ".firms",
- ".kindex", ".proton_flux",
- "fire.hotspot.viirs", "incident.tomtom",
- ):
- assert leak not in wire, (
- f"raw Central category token {leak!r} leaked to wire: {wire!r}"
- )
-
-
-@pytest.mark.parametrize("subject,envelope,expected_cat,expected_friendly_name", CASES)
-def test_event_category_is_meshai_flat(subject, envelope, expected_cat, expected_friendly_name):
- """The consumer must produce a meshai-flat category (not the raw Central
- hierarchical string) so downstream filtering + UI selectability work."""
- ev = _envelope_to_event(subject, envelope)
- assert ev.category == expected_cat, (
- f"expected event.category={expected_cat!r} got {ev.category!r}"
- )
- assert ev.category in ALERT_CATEGORIES, (
- f"event.category {ev.category!r} not in ALERT_CATEGORIES -- audit gap"
- )
-
-
-@pytest.mark.parametrize("subject,envelope,expected_cat,expected_friendly_name", CASES)
-def test_friendly_name_used_when_upstream_has_no_title(subject, envelope, expected_cat, expected_friendly_name):
- """For Central adapters whose upstream payload lacks `title`/`headline`,
- the consumer's title fallback must use the meshai-friendly registry name
- (`ALERT_CATEGORIES[category]['name']`) instead of `cat_raw`. NWS / USGS
- quake carry their own title; this assertion skips those (expected_friendly_name=None)."""
- if expected_friendly_name is None:
- pytest.skip("adapter supplies its own title -- registry fallback not exercised")
- ev = _envelope_to_event(subject, envelope)
- assert ev.title == expected_friendly_name, (
- f"expected title={expected_friendly_name!r} got {ev.title!r}"
- )
-
-
-@pytest.mark.parametrize("subject,envelope,expected_cat,expected_friendly_name", CASES)
-def test_wire_string_starts_with_composer_label(subject, envelope, expected_cat, expected_friendly_name):
- """The wire string should start with an emoji + family label like
- '🚨 ROADS:', '🔥 FIRE:', '⚠ WX:', '🌐 RF:', '⛷ AVY:'. Confirms the
- composer is what produces the formatting (not the renderer)."""
- ev = _envelope_to_event(subject, envelope)
- wire = _render_to_wire(ev)
- # Find ":" within the first ~20 chars: that's the label terminator.
- head = wire[:30]
- assert ":" in head, (
- f"wire string head {head!r} has no composer label terminator ':'"
- )
-
-
-# ---------- Specific Matt-saw regression ----------------------------------
-
-
-def test_matt_smoking_gun_no_longer_reproduces():
- """The exact regression Matt saw at 15:40:30 on 2026-06-04:
- [Roads] 🚨 ROADS: incident.tomtom_incidents, US-ID. immediate
- must NEVER reproduce. Strong-form assertion combining all three failure
- modes: no '[Roads]' prefix, no raw category leak, no missing friendly name."""
- ev = _envelope_to_event("central.traffic.incident.id", TOMTOM_ENV)
- wire = _render_to_wire(ev)
-
- assert not wire.startswith("[Roads]"), (
- f"the exact regression reproduced: {wire!r}"
- )
- assert "incident.tomtom_incidents" not in wire, (
- f"raw central category still leaks to wire: {wire!r}"
- )
- # Friendly name in primary slot
- assert "Road Incident" in wire, (
- f"friendly registry name not in wire: {wire!r}"
- )
- # Severity tail present
- assert "immediate" in wire, (
- f"severity tail missing: {wire!r}"
- )
diff --git a/work/tests/test_central_region_routing.py b/work/tests/test_central_region_routing.py
deleted file mode 100644
index 8fa6d98..0000000
--- a/work/tests/test_central_region_routing.py
+++ /dev/null
@@ -1,126 +0,0 @@
-"""v0.5.4: Central v0.9.20 region-aware subject building.
-
-Exercises `_subjects_for(adapter, region)` and the wiring through
-`CentralConsumer._subject_owned()`. The spec is hard-coded in the test
-strings on purpose so a future drift in the v0.9.20 subject scheme
-fails noisily here instead of silently shipping wrong filters.
-"""
-
-from meshai.central.consumer import (
- CentralConsumer,
- _subjects_for,
- _SUBJECTS_BARE,
-)
-from meshai.config import EnvironmentalConfig
-
-
-# --------------------------------------------------------------------- per-adapter
-
-def test_subjects_for_nws_us_id():
- """NWS: region BEFORE wildcard (matches alert..<...>)."""
- assert _subjects_for("nws", "us.id") == ["central.wx.alert.us.id.>"]
-
-
-def test_subjects_for_usgs_quake_us_id_uses_tail_only_wildcard():
- """v0.5.7-seismic: USGS quake publishes `central.quake.event.` with
- NO region in the subject (per Central v0.10.0 guide §usgs_quake; same
- situation as FIRMS). The pre-v0.5.7-seismic `central.quake.event.>.us.id`
- was syntactically invalid (`>` mid-subject) AND wouldn't have matched
- anything Central publishes (only 4 tokens, no us.). Region
- filtering for quakes now happens client-side via data.latitude/longitude.
- Subscription uses tail-only `>` (NATS-legal)."""
- assert _subjects_for("usgs_quake", "us.id") == ["central.quake.event.>"]
-
-
-def test_subjects_for_firms_us_id_uses_tail_only_wildcard():
- """v0.5.7-fire: FIRMS publishes `central.fire.hotspot..`
- with NO region in the subject (per Central v0.10.0 guide §firms). The
- pre-v0.5.7-fire `central.fire.hotspot.>.us.id` was syntactically invalid
- (`>` mid-subject) AND wouldn't have matched anything Central actually
- publishes. Region filtering for FIRMS now happens client-side via
- data.latitude/longitude. Subscription uses tail-only `>` (NATS-legal)."""
- assert _subjects_for("firms", "us.id") == ["central.fire.hotspot.>"]
-
-
-def test_subjects_for_fires_us_id_includes_tombstones():
- """v0.5.7-fire: WFIGS subjects -- active state-token at depth-3 + the
- removal-tombstone subjects (`central.fire.{incident,perimeter}.removed.`)
- per Central v0.10.0 guide §wfigs_incidents §wfigs_perimeters. Pre-v0.5.7-fire
- we only subscribed to active subjects, silently dropping fall-off signals."""
- assert _subjects_for("fires", "us.id") == [
- "central.fire.incident.id.>",
- "central.fire.perimeter.id.>",
- "central.fire.incident.removed.id",
- "central.fire.perimeter.removed.id",
- ]
-
-
-def test_subjects_for_traffic_uses_convention_b():
- """v0.5.7-traffic: traffic adapter -> bare-state Convention B with `*`
- in the event_type slot. Pre-v0.5.7-traffic this was `>.{state}` which
- is invalid NATS (`>` must be at the tail). The bare-state subject is
- shared with roads511 (sub-adapter routing picks the right meshai source)."""
- assert _subjects_for("traffic", "us.id") == ["central.traffic.*.id"]
-
-
-def test_subjects_for_roads511_dual_subscribes_convention_a_and_b():
- """v0.5.7-traffic: roads511 owns BOTH the shared bare-state subject
- (Convention B, shared with traffic) AND the us. subject
- (Convention A) where the new Idaho-only itd_511 adapter publishes."""
- assert _subjects_for("roads511", "us.id") == [
- "central.traffic.*.id",
- "central.traffic.*.us.id",
- ]
-
-
-def test_subjects_for_usgs_includes_unknown_workaround():
- """v0.5.7-water: USGS NWIS hydro subscribes to BOTH the region-tagged
- filter and the .unknown filter. Per the v0.10.0-itd-511 nwis.py
- producer, the actual published subject is
- `central.hydro....` where is
- either `us.` (7 tokens) or `unknown` (6 tokens). The
- pre-v0.5.7-water shape `central.hydro.>.` was invalid NATS
- (`>` mid-subject). Fixed by using three single-token `*` wildcards
- in the parameter/agency/site slots."""
- assert _subjects_for("usgs", "us.id") == [
- "central.hydro.*.*.*.us.id",
- "central.hydro.*.*.*.unknown",
- ]
-
-
-def test_subjects_for_swpc_stays_global():
- """SWPC: space weather is planetary; region argument is ignored."""
- assert _subjects_for("swpc", "us.id") == ["central.space.>"]
- assert _subjects_for("swpc", "us.mt") == ["central.space.>"] # same regardless
- assert _subjects_for("swpc", "") == ["central.space.>"]
-
-
-# --------------------------------------------------------------------- backward compat
-
-def test_subjects_for_empty_region_falls_back_to_bare_wildcards():
- """Empty/None region = pre-v0.9.20 behaviour for every adapter, byte-identical
- to the legacy _SUBJECTS_BARE map. Adapters absent from the map return []."""
- for adapter, expected in _SUBJECTS_BARE.items():
- assert _subjects_for(adapter, "") == expected, f"empty region mismatch for {adapter}"
- assert _subjects_for(adapter, None) == expected, f"None region mismatch for {adapter}"
- # Unknown adapters return empty regardless of region.
- assert _subjects_for("ducting", "us.id") == []
- assert _subjects_for("avalanche", "") != [] # avalanche now in central pipeline
-
-
-# --------------------------------------------------------------------- integration
-
-def test_central_region_default_propagates_to_consumer_subjects():
- """Default region = 'us.id': flipping nws to central → consumer subscribes
- to the region-aware subject, not the bare wildcard."""
- env = EnvironmentalConfig()
- assert env.central.region == "us.id" # spec default
- env.nws.feed_source = "central"
- so = CentralConsumer(env, None)._subject_owned()
- # satpass also defaults to feed_source='central', so it appears too
- assert "central.wx.alert.us.id.>" in so
- assert so["central.wx.alert.us.id.>"] == {"nws"}
- assert "central.sat.pass.us.id.>" in so
- assert so["central.sat.pass.us.id.>"] == {"satpass"}
- assert "central.sat.tle.>" in so
- assert so["central.sat.tle.>"] == {"satpass"}
diff --git a/work/tests/test_central_sub_adapter_routing.py b/work/tests/test_central_sub_adapter_routing.py
deleted file mode 100644
index c905945..0000000
--- a/work/tests/test_central_sub_adapter_routing.py
+++ /dev/null
@@ -1,94 +0,0 @@
-"""v0.5.1: sub-adapter (owned-sources) routing for shared Central subjects."""
-
-import json
-
-from meshai.config import EnvironmentalConfig
-from meshai.central.consumer import CentralConsumer
-from meshai.notifications.pipeline.bus import EventBus
-import pytest
-
-pytestmark = pytest.mark.skip(
- reason="v0.5.13 default-deny: sub-adapter routing tests asserted that envelopes without a wire-string-returning handler still emit an Event. New architecture: no handler-wire = no Event. v0.6 will rebuild these tests around the new default-deny model.")
-
-
-
-def _envelope(adapter, category="x.y", eid="e1"):
- return {"id": eid, "data": {
- "id": eid, "adapter": adapter, "category": category,
- "time": "2026-05-28T00:00:00Z", "severity": 1,
- "geo": {"centroid": [-114.0, 42.0], "primary_region": "US-ID", "regions": ["US-ID"]},
- "data": {}}}
-
-
-def _route(central, adapter, subject, category="x.y"):
- """Simulate a message arriving on the subscription that matches `subject`,
- with that subscription's owned-sources, and return the emitted Event (or None).
-
- v0.5.4: this helper deliberately clears central.region so sub-adapter
- routing is exercised against bare wildcards (its concern is the
- owned-sources filter, not the region-aware subject shape — those are
- tested in test_central_region_routing.py).
- """
- env = EnvironmentalConfig()
- env.central.region = ""
- for a in central:
- getattr(env, a).feed_source = "central"
- rec = []
- bus = EventBus(); bus.subscribe(rec.append)
- c = CentralConsumer(env, bus)
- so = c._subject_owned()
- owned = None
- for filt, o in so.items():
- prefix = filt[:-1] if filt.endswith(">") else filt
- if subject == filt or subject.startswith(prefix):
- owned = o
- break
- ev = c._handle(subject, json.dumps(_envelope(adapter, category)).encode(), owned)
- return ev, rec
-
-
-def test_roads511_only_drops_wzdx():
- ev, rec = _route(["roads511"], "wzdx", "central.traffic.work_zone.ok")
- assert ev is None and rec == []
-
-
-def test_roads511_only_emits_state_511_atis():
- ev, rec = _route(["roads511"], "state_511_atis", "central.traffic.event.id.1")
- assert ev is not None and ev.source == "roads511" and len(rec) == 1
-
-
-def test_both_central_wzdx_routes_to_traffic():
- ev, rec = _route(["traffic", "roads511"], "wzdx", "central.traffic.work_zone.ok")
- assert ev is not None and ev.source == "traffic"
-
-
-def test_both_central_state511_routes_to_roads511():
- ev, rec = _route(["traffic", "roads511"], "state_511_atis", "central.traffic.event.id.1")
- assert ev is not None and ev.source == "roads511"
-
-
-def test_firms_only_drops_wfigs():
- ev, rec = _route(["firms"], "wfigs_incidents", "central.fire.incident.mt.x")
- assert ev is None and rec == []
-
-
-def test_firms_only_emits_firms():
- ev, rec = _route(["firms"], "firms", "central.fire.hotspot.viirs_noaa20.high")
- assert ev is not None and ev.source == "firms" and len(rec) == 1
-
-
-def test_tomtom_incidents_remaps_to_traffic():
- ev, rec = _route(["traffic"], "tomtom_incidents", "central.traffic.incident.x")
- assert ev is not None and ev.source == "traffic"
-
-
-def test_subject_owned_shares_traffic_subject():
- # v0.5.4: assert the legacy bare-wildcard shape by clearing region.
- # Region-aware shared-subject behaviour ('central.traffic.>.id' for both
- # traffic and roads511) is covered in test_central_region_routing.py.
- env = EnvironmentalConfig()
- env.central.region = ""
- env.traffic.feed_source = "central"
- env.roads511.feed_source = "central"
- so = CentralConsumer(env, None)._subject_owned()
- assert so.get("central.traffic.>") == {"traffic", "roads511"}
diff --git a/work/tests/test_clock_seam.py b/work/tests/test_clock_seam.py
index 4922ef2..688c82a 100644
--- a/work/tests/test_clock_seam.py
+++ b/work/tests/test_clock_seam.py
@@ -2,8 +2,7 @@
Tests:
1. clock.now() returns a float close to time.time() at runtime.
- 2. Monkeypatching clock.now propagates into the three refactored handlers
- (quake_handler._now, nws_handler._now, wfigs_handler._now).
+ 2. Monkeypatching clock.now propagates into wfigs_handler._now.
"""
import time
@@ -11,8 +10,6 @@ import time
import pytest
import meshai.notifications.clock as clock_mod
-import meshai.central.quake_handler as quake_handler
-import meshai.central.nws_handler as nws_handler
import meshai.central.wfigs_handler as wfigs_handler
@@ -32,24 +29,6 @@ def test_clock_now_is_monkeypatchable(monkeypatch):
assert clock_mod.now() == _FROZEN_TS
-def test_quake_handler_now_uses_clock_seam(monkeypatch):
- """quake_handler._now() must reflect a monkeypatched clock.now."""
- monkeypatch.setattr(clock_mod, "now", lambda: _FROZEN_TS)
- result = quake_handler._now()
- assert result == int(_FROZEN_TS), (
- f"quake_handler._now() returned {result!r}, expected {int(_FROZEN_TS)}"
- )
-
-
-def test_nws_handler_now_uses_clock_seam(monkeypatch):
- """nws_handler._now() must reflect a monkeypatched clock.now."""
- monkeypatch.setattr(clock_mod, "now", lambda: _FROZEN_TS)
- result = nws_handler._now()
- assert result == int(_FROZEN_TS), (
- f"nws_handler._now() returned {result!r}, expected {int(_FROZEN_TS)}"
- )
-
-
def test_wfigs_handler_now_uses_clock_seam(monkeypatch):
"""wfigs_handler._now() must reflect a monkeypatched clock.now."""
monkeypatch.setattr(clock_mod, "now", lambda: _FROZEN_TS)
diff --git a/work/tests/test_config_source_field.py b/work/tests/test_config_source_field.py
index 028701c..4ac39be 100644
--- a/work/tests/test_config_source_field.py
+++ b/work/tests/test_config_source_field.py
@@ -1,10 +1,10 @@
-"""v0.4 C.1: per-adapter `source` field + CentralConsumerConfig."""
+"""v0.4 C.1: per-adapter `source` field."""
import pytest
from meshai.config import (
NWSConfig, FIRMSConfig, USGSQuakeConfig,
- EnvironmentalConfig, CentralConsumerConfig,
+ EnvironmentalConfig,
)
_ADAPTERS = ("nws", "swpc", "ducting", "fires", "avalanche",
@@ -35,13 +35,6 @@ def test_source_garbage_rejects():
FIRMSConfig(feed_source="")
-def test_environmental_has_central_default():
- env = EnvironmentalConfig()
- assert isinstance(env.central, CentralConsumerConfig)
- assert env.central.enabled is False
- assert env.central.url.startswith("nats://")
-
-
def test_source_field_survives_dict_coercion():
"""A `source` in yaml/dict is coerced onto the adapter config."""
from meshai.config import Config, _dict_to_dataclass
diff --git a/work/tests/test_consumer_default_deny.py b/work/tests/test_consumer_default_deny.py
deleted file mode 100644
index 9dce44d..0000000
--- a/work/tests/test_consumer_default_deny.py
+++ /dev/null
@@ -1,231 +0,0 @@
-"""v0.5.13 tests for consumer._normalize() default-deny gate.
-
-The consumer must return None when the per-adapter handler dispatch
-returns synthesized=None -- regardless of what data.title / data.headline
-say. Conversely, when a handler returns a wire string, _normalize must
-return an Event with that exact title + _meshai_precomposed=True so the
-composer bypass kicks in.
-
-Covers four cases:
- (a) envelope with NO matching handler (adapter='avalanche' has no
- Central adapter wired) -> _normalize returns None
- (b) envelope hits handler, handler returns None (e.g. sub-G3 swpc,
- stale tomtom) -> _normalize returns None
- (c) envelope hits handler, handler returns wire string
- -> _normalize returns Event with title=wire and
- data['_meshai_precomposed'] = True
- (d) envelope with data.title and data.headline set, but no handler match
- -> _normalize STILL returns None (no title fallback)
-"""
-import pytest
-from unittest.mock import patch, MagicMock
-
-from meshai.config import Config
-from meshai.central.consumer import CentralConsumer
-from meshai.persistence import close_thread_connection, init_db
-from meshai.persistence import db as persistence_db
-
-
-@pytest.fixture
-def mem_db(monkeypatch, tmp_path):
- db_path = str(tmp_path / "v0513-test.sqlite")
- monkeypatch.setenv("MESHAI_DB_PATH", db_path)
- persistence_db._initialised.clear()
- close_thread_connection()
- yield init_db()
- close_thread_connection()
- persistence_db._initialised.discard(db_path)
-
-
-@pytest.fixture
-def consumer():
- """CentralConsumer with mocked bus (we test _normalize only).
- CentralConsumer.__init__(env_config, event_bus) where env_config
- is the EnvironmentalConfig (provides .central + per-adapter source).
- """
- cfg = Config()
- cfg.notifications.cold_start_grace_seconds = 0
- bus = MagicMock()
- c = CentralConsumer(cfg.environmental, bus)
- return c
-
-
-# ---------- envelope builders ----------------------------------------------
-
-
-def _make_envelope(adapter, category, *, inner_id="test_001",
- title=None, headline=None, severity="routine",
- extra_data=None):
- inner_data = dict(extra_data or {})
- if title is not None:
- inner_data["title"] = title
- if headline is not None:
- inner_data["headline"] = headline
- return {
- "subject": f"central.{adapter}.test",
- "id": f"env_{inner_id}",
- "data": {
- "id": inner_id,
- "adapter": adapter,
- "category": category,
- "severity": severity,
- "time": "2026-06-05T15:00:00Z",
- "geo": {"primary_region": "US-ID"},
- "data": inner_data,
- },
- }
-
-
-# ============================================================================
-# (a) envelope with NO matching handler -> default-deny
-# ============================================================================
-
-
-def test_no_handler_match_returns_none(consumer, mem_db):
- """Avalanche has no handler (no Central adapter). envelope must
- drop at consumer._normalize as the default-deny baseline."""
- env = _make_envelope("avalanche", "avalanche.forecast",
- inner_id="aval_001")
- out = consumer._normalize(env["subject"], env)
- assert out is None
-
-
-def test_unknown_adapter_returns_none(consumer, mem_db):
- """Any future adapter that meshai doesn't know about must default-deny."""
- env = _make_envelope("future_adapter", "some.category.v1",
- inner_id="future_001",
- title="Some Title", headline="Some Headline")
- out = consumer._normalize(env["subject"], env)
- assert out is None
-
-
-# ============================================================================
-# (b) handler returns None -> default-deny (regardless of data.title)
-# ============================================================================
-
-
-def test_handler_returns_none_drops_event(consumer, mem_db, monkeypatch):
- """Stale tomtom incident -> incident_handler returns None -> drop."""
- env = _make_envelope("tomtom_incidents", "incident.tomtom_incidents",
- inner_id="ID:tomtom:TTI-stale",
- title="Old Jam", headline="Headline Jam",
- extra_data={
- "id": "ID:tomtom:TTI-stale",
- "magnitude_of_delay": 4,
- "icon_category": 6,
- "time_validity": "past", # filtered
- "start_time": "2024-01-01T00:00:00Z",
- "latitude": 43.5, "longitude": -116.0,
- })
- out = consumer._normalize(env["subject"], env)
- assert out is None, "default-deny: handler None -> no Event"
-
-
-def test_data_title_does_not_rescue_handler_none(consumer, mem_db):
- """v0.5.13: even when envelope has data.title set, if no handler\
- synthesized, the broadcast is denied."""
- env = _make_envelope("swpc_kindex", "space.kindex",
- inner_id="kp_sub_threshold",
- title="Kp Update",
- extra_data={
- "id": "kp_sub_threshold",
- "kp_index": 2.0, # well below G3 (Kp>=7)
- "time": "2026-06-05T15:00:00Z",
- })
- out = consumer._normalize(env["subject"], env)
- assert out is None
- assert out is None # double-check
-
-
-# ============================================================================
-# (c) handler returns wire string -> Event emitted with precomposed marker
-# ============================================================================
-
-
-def test_handler_returns_wire_event_emitted(consumer, mem_db, monkeypatch):
- """Fresh tomtom envelope passes the handler gate -> Event created."""
- # Disable Photon to avoid network calls in test.
- import meshai.central_normalizer as cn
- monkeypatch.setattr(cn, "_photon_reverse_places", lambda lat, lon: [])
- if hasattr(cn, "_H3_NEAREST_CACHE"):
- cn._H3_NEAREST_CACHE.clear()
-
- import time
- now_iso = "2026-06-05T15:00:00Z"
- # Build a fresh envelope (start_time = now-300s would require dynamic
- # clock control; we instead set the freshness window via the envelope
- # built relative to a fixed time and mock the handler to bypass freshness).
- env = _make_envelope(
- "tomtom_incidents", "incident.tomtom_incidents",
- inner_id="ID:tomtom:TTI-aaaa1111-2222-3333-4444-555555555555-TTR1",
- extra_data={
- "id": "ID:tomtom:TTI-aaaa1111-2222-3333-4444-555555555555-TTR1",
- "magnitude_of_delay": 4,
- "icon_category": 6,
- "time_validity": "present",
- "start_time": now_iso,
- "latitude": 43.6, "longitude": -116.2,
- "delay": 180, "from": "A St", "to": "B St",
- "road_numbers": ["I-84"],
- "state_code": "ID",
- "_enriched": {"geocoder": {"city": "Boise", "county": "Ada",
- "state": "ID"}},
- },
- )
-
- # Use a "now" that aligns with start_time so freshness gate passes.
- import datetime as _dt
- now_epoch = int(_dt.datetime.fromisoformat(
- now_iso.replace("Z","+00:00")).timestamp()) + 60 # 1 min after start
-
- with patch("time.time", return_value=now_epoch):
- out = consumer._normalize(env["subject"], env)
-
- assert out is not None, "fresh tomtom should produce an Event"
- assert out.data.get("_meshai_precomposed") is True
- assert out.title.startswith("🚗") # jam emoji
- assert "Boise" in out.title
-
-
-# ============================================================================
-# (d) envelope with title but no handler still drops (no title fallback)
-# ============================================================================
-
-
-def test_envelope_with_title_still_drops_without_handler(consumer, mem_db):
- """Regression guard: the v0.5.7-fallback path (data.title -> headline ->\
- friendly_name -> cat_raw) is GONE in v0.5.13. Uses an unhandled adapter
- (avalanche) since v0.6-1 added the FIRMS handler."""
- env = _make_envelope("avalanche", "avalanche.forecast",
- inner_id="aval_with_title",
- title="Avalanche Warning",
- headline="Backcountry advisory")
- out = consumer._normalize(env["subject"], env)
- assert out is None, (
- "v0.5.13 default-deny: data.title and data.headline must NOT rescue\n"
- "an envelope that no handler synthesized for."
- )
-
-
-# ============================================================================
-# (e) memory rule 19 -- confirms _normalize ENTRY logging behavior
-# ============================================================================
-
-
-def test_default_deny_path_is_silent_at_INFO(consumer, mem_db, caplog):
- """Default-deny paths log at DEBUG, not INFO/WARNING. We don't want
- millions of DEBUG-noise to feel like errors at default log levels."""
- import logging
- caplog.set_level(logging.INFO, logger="meshai.central.consumer")
- env = _make_envelope("firms", "fire.hotspot.viirs",
- inner_id="silent_check")
- out = consumer._normalize(env["subject"], env)
- assert out is None
- # No INFO/WARNING/ERROR for normal default-deny.
- info_or_higher = [r for r in caplog.records
- if r.levelno >= logging.INFO
- and r.name == "meshai.central.consumer"]
- assert len(info_or_higher) == 0, (
- f"default-deny should be silent at INFO+; got: "
- f"{[(r.levelname, r.message) for r in info_or_higher]}"
- )
diff --git a/work/tests/test_fire_v057.py b/work/tests/test_fire_v057.py
deleted file mode 100644
index 8692bdb..0000000
--- a/work/tests/test_fire_v057.py
+++ /dev/null
@@ -1,262 +0,0 @@
-"""v0.5.7-fire: FIRMS NATS pattern + WFIGS tombstone dedup + categories audit.
-
-Covers four things shipped in v0.5.7-fire:
-
-1. FIRMS subject pattern -- per Central v0.10.0 guide, FIRMS publishes
- `central.fire.hotspot..` with NO region in the
- subject. The pre-v0.5.7-fire `central.fire.hotspot.>.us.id` was
- syntactically invalid (`>` mid-subject) AND wouldn't have matched
- anything. NOTE on user-prompt discrepancy: the v0.5.7-fire prompt
- specified `central.fire.hotspot.*.*.us.id` (7 tokens with us.
- tail) but the actual Central v0.10.0 guide shows exactly 5 tokens with
- no region. We follow the guide -- following the prompt verbatim would
- produce a subscription that matches zero messages in production.
-2. WFIGS subjects -- active state-token subjects + the four removal
- tombstone subjects per guide §wfigs_incidents §wfigs_perimeters.
-3. WFIGS tombstone dedup -- env_id form `:removed:` must
- strip to the bare IrwinID for group_key so all tombstones for the same
- incident share the group_key (per guide §wfigs_incidents removal
- semantics: "the same incident can have one or more removal tombstones
- over its lifecycle"). Two tombstones with the same IrwinID but different
- :removed: tails: both must propagate through _handle as distinct
- Events; both must share group_key == IrwinID.
-4. ALERT_CATEGORIES fire-family audit -- fire_proximity and
- wildfire_proximity removed (Matt: parametric, can't set "near"
- threshold in UI); new_ignition, wildfire_hotspot, wildfire_incident kept
- / added.
-"""
-
-import inspect
-import json
-import re
-
-import pytest
-
-from meshai.central.consumer import (
- CentralConsumer,
- _SUBJECTS_BARE,
- _subjects_for,
- map_category,
-)
-from meshai.config import EnvironmentalConfig
-from meshai.notifications.categories import ALERT_CATEGORIES
-from meshai.notifications.pipeline.bus import EventBus
-
-pytestmark = pytest.mark.skip(
- reason="v0.5.13 default-deny: WFIGS tombstones now correctly return None from wfigs_handler (logged to event_log handled=0, no Event). These tests asserted the legacy clear-event-emission. New behavior is covered by tests/test_wfigs_handler.py.")
-
-
-
-def _assert_legal_nats(subject: str) -> None:
- """Assert NATS multi-level wildcard `>` only appears at the tail token."""
- tokens = subject.split(".")
- if ">" in tokens:
- assert tokens[-1] == ">", f"`>` not at tail in {subject!r}"
- assert tokens.count(">") == 1, f"multiple `>` in {subject!r}"
- for tok in tokens:
- assert tok, f"empty token in {subject!r}"
- if tok not in {"*", ">"}:
- assert "*" not in tok and ">" not in tok, f"mixed wildcard in token {tok!r}"
-
-
-# ---------- FIRMS subject pattern -----------------------------------------
-
-
-def test_firms_subject_uses_tail_only_wildcard():
- """FIRMS publishes . only -- no us.."""
- subs = _subjects_for("firms", "us.id")
- assert subs == ["central.fire.hotspot.>"]
- for s in subs:
- _assert_legal_nats(s)
-
-
-def test_firms_subject_has_no_mid_string_wildcard():
- """Belt-and-braces: `>` only at tail, no mid-subject placement."""
- for s in _subjects_for("firms", "us.id"):
- tokens = s.split(".")
- for tok in tokens[:-1]:
- assert tok != ">", f"`>` mid-subject in {s!r}"
-
-
-# ---------- WFIGS subjects (fires) ----------------------------------------
-
-
-def test_fires_subjects_cover_active_and_tombstones():
- """v0.5.7-fire: tombstone subjects are now subscribed alongside active."""
- subs = _subjects_for("fires", "us.id")
- assert subs == [
- "central.fire.incident.id.>",
- "central.fire.perimeter.id.>",
- "central.fire.incident.removed.id",
- "central.fire.perimeter.removed.id",
- ]
- for s in subs:
- _assert_legal_nats(s)
-
-
-def test_fires_subjects_no_mid_subject_wildcard():
- for s in _subjects_for("fires", "us.id"):
- tokens = s.split(".")
- for tok in tokens[:-1]:
- assert tok != ">", f"`>` mid-subject in {s!r}"
-
-
-# ---------- WFIGS tombstone dedup -----------------------------------------
-
-
-def _envelope(adapter, eid, category="fire.incident.removed"):
- """Build a CloudEvents-shaped envelope for a single WFIGS tombstone."""
- return {"id": eid, "data": {
- "id": eid, "adapter": adapter, "category": category,
- "time": "2026-05-19T02:50:39+00:00", "severity": 0,
- "geo": {"centroid": None, "primary_region": None, "regions": []},
- "data": {"irwin_id": "{01AAC875-E26E-49E4-9DB0-80B5965A7B9F}",
- "state": "US-ID", "county": "Custer",
- "reason": "fallen_off_current_service",
- "last_observed_at": "2026-05-19T02:50:00+00:00"}}}
-
-
-def test_wfigs_tombstone_strips_removed_iso_suffix():
- """Single WFIGS tombstone -- group_key recovers the bare IrwinID."""
- rec = []
- bus = EventBus(); bus.subscribe(rec.append)
- c = CentralConsumer(EnvironmentalConfig(), bus)
- irwin = "{01AAC875-E26E-49E4-9DB0-80B5965A7B9F}"
- eid = f"{irwin}:removed:2026-05-19T02:50:39.843049+00:00"
- env = _envelope("wfigs_incidents", eid)
- ev = c._handle("central.fire.incident.removed.id", json.dumps(env).encode())
- assert ev is not None
- assert ev.data.get("_central_tombstone") is True
- assert ev.group_key == irwin, f"group_key did not strip :removed: tail: {ev.group_key!r}"
-
-
-def test_wfigs_two_tombstones_same_irwin_both_propagate():
- """Per guide §wfigs_incidents: the same incident can have multiple
- removal tombstones over its lifecycle. Both tombstones with the same
- IrwinID but different :removed: tails must:
- - both be emitted by _handle (not collapsed at consumer layer)
- - share the same group_key (== IrwinID) so they signal lapse
- against the same original event
- """
- rec = []
- bus = EventBus(); bus.subscribe(rec.append)
- c = CentralConsumer(EnvironmentalConfig(), bus)
- irwin = "{01AAC875-E26E-49E4-9DB0-80B5965A7B9F}"
- eid1 = f"{irwin}:removed:2026-05-19T02:50:39.843049+00:00"
- eid2 = f"{irwin}:removed:2026-05-20T14:22:17.111222+00:00"
- env1 = _envelope("wfigs_incidents", eid1)
- env2 = _envelope("wfigs_incidents", eid2)
- ev1 = c._handle("central.fire.incident.removed.id", json.dumps(env1).encode())
- ev2 = c._handle("central.fire.incident.removed.id", json.dumps(env2).encode())
- # Both emitted -- no consumer-layer dedup collapsing.
- assert ev1 is not None and ev2 is not None
- assert len(rec) == 2, f"expected 2 events on bus, got {len(rec)}"
- # Both share the bare IrwinID as group_key (so they lapse the original
- # incident's accumulator entry by the same key).
- assert ev1.group_key == irwin
- assert ev2.group_key == irwin
- # Event.id is intentionally deterministic from (source, category,
- # group_key, lat, lon) — two tombstones for the same incident produce
- # the same Event.id by design. Distinctness is preserved on
- # data['_central_tombstone_id'] which carries the full :removed:
- # tail so downstream consumers can tell the two fall-off events apart
- # if they want to.
- assert ev1.data.get("_central_tombstone_id") == eid1
- assert ev2.data.get("_central_tombstone_id") == eid2
- assert ev1.data["_central_tombstone_id"] != ev2.data["_central_tombstone_id"]
-
-
-def test_legacy_gdacs_tombstone_still_strips_plain_suffix():
- """Regression guard: the legacy GDACS `:removed` shape (no :
- tail) must still strip cleanly. The v0.5.7-fire regex is a superset
- of the pre-v0.5.7-fire regex, not a replacement."""
- rec = []
- bus = EventBus(); bus.subscribe(rec.append)
- c = CentralConsumer(EnvironmentalConfig(), bus)
- env = {"id": "FL1103885:removed", "data": {
- "id": "FL1103885:removed", "adapter": "gdacs", "category": "disaster.fl.removed",
- "time": "2026-05-28T00:00:00Z", "severity": 0,
- "geo": {"centroid": None, "primary_region": None, "regions": []},
- "data": {}}}
- ev = c._handle("central.disaster.fl.removed.austria", json.dumps(env).encode())
- assert ev is not None
- assert ev.data.get("_central_tombstone") is True
- assert ev.group_key == "FL1103885"
-
-
-# ---------- ALERT_CATEGORIES fire-family audit ----------------------------
-
-
-def test_fire_proximity_removed_from_registry():
- """Matt: 'fire near mesh has its own set of parameters that I don't even
- know what they could be. like how far is near mesh? I don't know I
- can't set that.' -- removed in v0.5.7-fire; parametric distance is
- queued for v0.5.8."""
- assert "fire_proximity" not in ALERT_CATEGORIES
-
-
-def test_wildfire_proximity_removed_from_registry():
- """Duplicate 'Fire Near Mesh' name w/ fire_proximity; same parametric
- issue; removed in v0.5.7-fire."""
- assert "wildfire_proximity" not in ALERT_CATEGORIES
-
-
-def test_no_duplicate_fire_near_mesh_names():
- """No two fire-family registry entries share the 'Fire Near Mesh' name."""
- names = [info["name"] for cid, info in ALERT_CATEGORIES.items()
- if info.get("toggle") == "fire"]
- assert names.count("Fire Near Mesh") == 0
- assert len(set(names)) == len(names), f"duplicate fire-family names: {names}"
-
-
-def _native_emitted_fire_categories() -> set[str]:
- """Walk firms.py and fires.py for category= literals."""
- from meshai.env import firms as firms_mod, fires as fires_mod
- emitted: set[str] = set()
- for mod in (firms_mod, fires_mod):
- src = inspect.getsource(mod)
- emitted |= set(re.findall(r'category="([a-z_]+)"', src))
- # Also pick up `category = "..."` ternary forms.
- emitted |= set(re.findall(r'category\s*=\s*"([a-z_]+)"\s+if', src))
- emitted |= set(re.findall(r'else\s+"([a-z_]+)"', src))
- # Filter to known fire-family ids (other ternary branches may surface
- # non-fire strings; we only care about ones routed through toggle=fire).
- return {c for c in emitted if c in ALERT_CATEGORIES
- and ALERT_CATEGORIES[c].get("toggle") == "fire"}
-
-
-def _central_path_fire_categories() -> set[str]:
- central_inputs = [
- "fire.hotspot.viirs_noaa20.high",
- "fire.incident.id.ada",
- "fire.incident.removed",
- "fire.perimeter.id.ada",
- "fire.perimeter.removed",
- "fire.unknown_subtype",
- ]
- return {map_category(c) for c in central_inputs}
-
-
-def test_alert_categories_fire_complete():
- """Native + central-path emit must equal registry's fire-family set."""
- registry_fire = {
- cid for cid, info in ALERT_CATEGORIES.items()
- if info.get("toggle") == "fire"
- }
- emitted = _native_emitted_fire_categories() | _central_path_fire_categories()
- missing = emitted - registry_fire
- orphans = registry_fire - emitted
- assert not missing, f"fire emit set missing from ALERT_CATEGORIES: {missing}"
- assert not orphans, f"ALERT_CATEGORIES has orphan fire entries: {orphans}"
-
-
-@pytest.mark.parametrize(
- "cat", ["new_ignition", "wildfire_hotspot", "wildfire_incident"],
-)
-def test_fire_categories_have_required_fields(cat):
- info = ALERT_CATEGORIES[cat]
- assert info["toggle"] == "fire"
- assert info["name"]
- assert info["description"]
- assert info["default_severity"] in {"routine", "priority", "immediate"}
- assert info["example_message"]
diff --git a/work/tests/test_firms_fusion_event_contract.py b/work/tests/test_firms_fusion_event_contract.py
index 09531a3..58acd80 100644
--- a/work/tests/test_firms_fusion_event_contract.py
+++ b/work/tests/test_firms_fusion_event_contract.py
@@ -1,319 +1,49 @@
-"""Regression tests for the FIRMS fire-fusion Event contract (issues #117-#119).
+"""Regression tests for the FIRMS FirePacer contract (issue #119).
-Three independent bugs in the path from firms_handler's growth / spotting /
-halt / cluster fusion decisions to the actual meshai Event that reaches the
-dispatcher + pacer:
+Originally three sections (A/B/C) guarding issues #117-#119 in the path
+from firms_handler's growth/spotting/halt/cluster fusion decisions to the
+actual meshai Event that reaches the dispatcher + pacer. Sections A and B,
+plus one test in section C, drove that path exclusively through
+`meshai.central.consumer.CentralConsumer._normalize()`/`._handle()` -- the
+Central NATS-consumer bridge, which has been deleted (production runs the
+native env/firms.py -> firms_handler.ingest_hotspot_pixel fusion path
+exclusively; see meshai.env.firms.FirmsAdapter._make_fusion_event, which
+independently applies the same `_severity_override`-over-`severity`
+resolution). Deleting CentralConsumer makes those tests uncollectable, and
+since the mechanism they guarded (issues #117/#118) lived entirely inside
+the now-dead consumer, they can no longer exist as tests of live behavior
+-- git history preserves them.
- #117 category overrides dead: consumer._normalize() computed `category`
- from the raw Central category BEFORE the per-adapter handler ran, and
- the final make_event() call never re-read data["category"], so every
- firms_handler category stamp (wildfire_growth / wildfire_halted /
- wildfire_spotting / unattributed_hotspot_cluster) was a silent no-op.
+The #117/#118 category+severity contract they exercised at the data_patch
+level is independently covered against the LIVE native gating path in
+tests/test_firms_refactor.py (asserts `data_patch["category"]` /
+`data_patch["_severity_override"]` for growth/spotting/halt/cluster) and at
+the Event/category level in tests/test_firms_native_fusion.py (drives the
+real adapter tick() -> to_event() chain and asserts `ev.category`).
- #118 severity overrides dead for 3 of 4 fusion kinds: consumer.py only ever
- honored data["_severity_override"], but firms_handler's halt / spotting
- / cluster sites stamped the plain data["severity"] key instead (only
- growth used the correct key), so those events fell back to whatever
- map_severity(inner.get("severity")) produced from the raw envelope.
-
- #119 FirePacer didn't cover FIRMS: the pacer gate only matched
- source in ("fires", "wfigs") and severity == "priority", but FIRMS
- fusion broadcasts carry source="firms" and growth/spotting are
- severity="immediate" -- so none of them were ever paced. The fix
- broadens the gate to source="firms" + {"priority","immediate"}, and
- adds head-of-line insertion so an "immediate" event is never stuck
- behind already-queued "priority" events.
-
-Cluster detection itself is a deliberate, always-on feature of main (PR #73:
-curated new-fire cluster broadcasts, cold-start silent-seeded). Nothing here
-enables or disables it -- the cluster case below only asserts that its
-severity override reaches the Event (the #118 fix).
-
-Sections:
- A. Category overrides survive to the emitted Event (growth/halt/spotting).
- B. Severity overrides survive to the emitted Event, using the shared
- `_severity_override` contract (spotting=immediate, halt=routine,
- cluster=priority).
- C. FirePacer routes FIRMS broadcasts; immediate jumps the queue; nothing
- is ever dropped.
+What remains here (issue #119, section C) is two tests that exercise the
+FirePacer class directly with no dependency on CentralConsumer -- these are
+native, standalone FirePacer unit tests (head-of-line ordering, no-drop
+guarantee) and survive unchanged. The third section-C test (routing a real
+FIRMS growth broadcast into a mocked pacer via CentralConsumer._handle) is
+deleted along with A/B for the same reason; the equivalent native-path
+routing guarantee (store._emit_event() -> FirePacer, for source="firms")
+is already covered end-to-end in tests/test_native_fire_pacer.py.
"""
from __future__ import annotations
import asyncio
-import math
-import time
-import uuid
-import pytest
-
-from meshai.config import Config
-from meshai.central.consumer import CentralConsumer
from meshai.notifications.events import make_event
from meshai.notifications.pipeline.pacer import FirePacer
-from meshai.persistence import close_thread_connection, init_db
-from meshai.persistence import db as persistence_db
-
-_MI_PER_DEG_LAT = 69.0
-_SUBJECT = "central.fire.hotspot.N20.high.us.id"
-
-
-# ── isolation ────────────────────────────────────────────────────────────────
-
-@pytest.fixture(autouse=True)
-def _isolate_db(tmp_path, monkeypatch):
- db_path = str(tmp_path / f"meshai-{uuid.uuid4().hex}.sqlite")
- monkeypatch.setenv("MESHAI_DB_PATH", db_path)
- persistence_db._initialised.clear()
- close_thread_connection()
- init_db()
- try:
- from meshai.adapter_config import adapter_config as _ac
- _ac.invalidate()
- except Exception:
- pass
- yield db_path
- close_thread_connection()
- persistence_db._initialised.discard(db_path)
-
-
-@pytest.fixture(autouse=True)
-def _no_cutover(monkeypatch):
- """Default deploy state: nothing cut over -> firms_handler's legacy
- stamps are what actually reach `data`."""
- monkeypatch.delenv("MESHAI_CUTOVER_CATEGORIES", raising=False)
- from meshai.notifications.cutover import _clear_cache
- _clear_cache()
- yield
- _clear_cache()
-
-
-# firms_handler.handle_firms defaults `now` to real wall-clock time
-# (int(time.time())) whenever it is called without an explicit `now`
-# kwarg -- which is exactly how consumer._normalize()/_handle() call it (no
-# `now` is threaded through from the envelope). The growth/spotting/halt
-# fixtures below use fixed 2026-06-06 acq_date/acq_time values (matching the
-# rest of the FIRMS test suite), so pin the wall clock to a fixed reference
-# in the same window; otherwise the halt detector opportunistically fires on
-# every pixel once the real host clock is weeks past the canned acq_times.
-_FIXED_NOW = 1780768800.0 # 2026-06-06 18:00 UTC
-
-
-@pytest.fixture(autouse=True)
-def _fixed_clock(monkeypatch):
- monkeypatch.setattr("time.time", lambda: _FIXED_NOW)
- yield
-
-
-@pytest.fixture
-def consumer():
- """CentralConsumer with a mocked bus, mirroring
- test_consumer_default_deny.py's `consumer` fixture."""
- from unittest.mock import MagicMock
- cfg = Config()
- cfg.notifications.cold_start_grace_seconds = 0
- bus = MagicMock()
- c = CentralConsumer(cfg.environmental, bus)
- return c, bus
-
-
-# ── envelope + fire-seeding helpers (mirror test_firms_refactor.py) ─────────
-
-def _seed_fire(*, irwin_id, lat, lon, name="Stub Fire", **cols):
- from meshai.persistence import get_db
- conn = get_db()
- base = {"irwin_id": irwin_id, "incident_name": name, "lat": lat, "lon": lon,
- "last_event_at": int(time.time())}
- base.update(cols)
- keys = ",".join(base)
- ph = ",".join("?" * len(base))
- conn.execute(f"INSERT INTO fires({keys}) VALUES ({ph})", tuple(base.values()))
-
-
-def _envelope(*, lat, lon, acq_date="2026-06-06", acq_time="1200",
- frp=20.0, satellite="N20", eid=None):
- eid = eid or f"firms-{lat}-{lon}-{acq_time}"
- return {
- "id": f"env_{eid}",
- "data": {
- "id": eid,
- "adapter": "firms",
- "category": "wildfire_hotspot",
- "severity": "routine",
- "geo": {"primary_region": "US-ID"},
- "data": {
- "latitude": lat, "longitude": lon, "frp": frp,
- "bright_ti4": 320.0, "satellite": satellite,
- "instrument": "VIIRS", "confidence": "high",
- "acq_date": acq_date, "acq_time": acq_time,
- "daynight": "D", "version": "2.0NRT",
- },
- },
- }
-
-
-def _offset_mi(lat, lon, north_mi, east_mi):
- dlat = north_mi / _MI_PER_DEG_LAT
- dlon = east_mi / (_MI_PER_DEG_LAT * math.cos(math.radians(lat)))
- return lat + dlat, lon + dlon
# ═════════════════════════════════════════════════════════════════════════════
-# A. Category overrides reach the emitted Event (issue #117)
-# ═════════════════════════════════════════════════════════════════════════════
-
-class TestCategoryOverrideReachesEvent:
- def test_growth_event_category_is_wildfire_growth(self, consumer):
- c, _bus = consumer
- center_lat, center_lon = 42.0, -114.0
- _seed_fire(irwin_id="ID-CAT-G", lat=center_lat, lon=center_lon,
- name="Pine Gulch")
- # Pass A: 5 pixels build the baseline (no broadcast yet).
- for i in range(5):
- evt = c._normalize(_SUBJECT, _envelope(
- lat=center_lat + 0.0001 * i, lon=center_lon + 0.0001 * (i - 2),
- acq_time=f"12{i:02d}", frp=20.0 + i, eid=f"ga{i}"))
- assert evt is None, "pass-A pixels must not broadcast"
- # Pass B: 1 mi N, later pass bucket -> growth boundary.
- pass_b_lat = center_lat + (1.0 / _MI_PER_DEG_LAT)
- evt = c._normalize(_SUBJECT, _envelope(
- lat=pass_b_lat, lon=center_lon, acq_time="1800", frp=22.0, eid="gb"))
- assert evt is not None
- assert evt.category == "wildfire_growth", (
- "category override must survive to the Event, not the generic "
- "wildfire_hotspot/wildfire_incident fallback")
- assert evt.source == "firms"
-
- def test_halt_event_category_is_wildfire_halted(self, consumer):
- c, _bus = consumer
- now = 1780768800
- idle_at = now - 14 * 3600
- from meshai.persistence import get_db
- get_db().execute(
- "INSERT INTO fires(irwin_id, incident_name, lat, lon, "
- "last_event_at, last_pass_id, last_pass_at) VALUES (?,?,?,?,?,?,?)",
- ("ID-CAT-H", "Cold Fire", 42.5, -114.5, int(idle_at),
- "N20-329627", float(idle_at)))
- # A fresh unattributed pixel far away triggers the opportunistic
- # halt detector for the idle fire.
- evt = c._normalize(_SUBJECT, _envelope(
- lat=45.0, lon=-118.0, acq_time="1800", eid="halt1"))
- assert evt is not None
- assert evt.category == "wildfire_halted"
-
- def test_spotting_event_category_is_wildfire_spotting(self, consumer):
- c, _bus = consumer
- center_lat, center_lon = 43.0, -115.0
- _seed_fire(irwin_id="ID-CAT-S", lat=center_lat, lon=center_lon,
- name="Spot Fire")
- for i in range(6):
- angle = i * math.pi / 3
- la = center_lat + (0.5 / _MI_PER_DEG_LAT) * math.sin(angle)
- cos_lat = math.cos(math.radians(center_lat))
- lo = center_lon + (0.5 / (_MI_PER_DEG_LAT * cos_lat)) * math.cos(angle)
- evt = c._normalize(_SUBJECT, _envelope(
- lat=la, lon=lo, acq_time=f"12{i * 2:02d}", eid=f"sa{i}"))
- assert evt is None
- sp_lat, sp_lon = _offset_mi(center_lat, center_lon,
- north_mi=2.0 / math.sqrt(2),
- east_mi=2.0 / math.sqrt(2))
- evt = c._normalize(_SUBJECT, _envelope(
- lat=sp_lat, lon=sp_lon, acq_time="1800", eid="sb"))
- assert evt is not None
- assert evt.category == "wildfire_spotting"
-
-
-# ═════════════════════════════════════════════════════════════════════════════
-# B. Severity overrides reach the emitted Event via `_severity_override`
-# (issue #118)
-# ═════════════════════════════════════════════════════════════════════════════
-
-class TestSeverityOverrideReachesEvent:
- def test_spotting_event_severity_is_immediate(self, consumer):
- c, _bus = consumer
- center_lat, center_lon = 44.0, -116.0
- _seed_fire(irwin_id="ID-SEV-S", lat=center_lat, lon=center_lon)
- for i in range(6):
- angle = i * math.pi / 3
- la = center_lat + (0.5 / _MI_PER_DEG_LAT) * math.sin(angle)
- cos_lat = math.cos(math.radians(center_lat))
- lo = center_lon + (0.5 / (_MI_PER_DEG_LAT * cos_lat)) * math.cos(angle)
- c._normalize(_SUBJECT, _envelope(
- lat=la, lon=lo, acq_time=f"12{i * 2:02d}", eid=f"ssa{i}"))
- sp_lat, sp_lon = _offset_mi(center_lat, center_lon,
- north_mi=2.0 / math.sqrt(2),
- east_mi=2.0 / math.sqrt(2))
- evt = c._normalize(_SUBJECT, _envelope(
- lat=sp_lat, lon=sp_lon, acq_time="1800", eid="ssb"))
- assert evt is not None
- assert evt.severity == "immediate", (
- "spotting must reach the pacer/dispatcher as immediate severity, "
- "not fall back to map_severity() of the raw envelope")
-
- def test_halt_event_severity_is_routine(self, consumer):
- c, _bus = consumer
- now = 1780768800
- idle_at = now - 14 * 3600
- from meshai.persistence import get_db
- get_db().execute(
- "INSERT INTO fires(irwin_id, incident_name, lat, lon, "
- "last_event_at, last_pass_id, last_pass_at) VALUES (?,?,?,?,?,?,?)",
- ("ID-SEV-H", "Cold Fire", 42.5, -114.5, int(idle_at),
- "N20-329627", float(idle_at)))
- evt = c._normalize(_SUBJECT, _envelope(
- lat=45.0, lon=-118.0, acq_time="1800", eid="sevhalt"))
- assert evt is not None
- assert evt.severity == "routine"
-
- def test_cluster_event_severity_is_priority(self, consumer):
- c, _bus = consumer
- base_lat, base_lon = 43.500, -114.500
- pixels = [
- (base_lat, base_lon, "1200"),
- (base_lat + 0.001, base_lon + 0.001, "1210"),
- (base_lat - 0.001, base_lon - 0.002, "1220"),
- ]
- events = []
- for i, (la, lo, t) in enumerate(pixels):
- evt = c._normalize("central.fire.hotspot.N20.high.unknown", _envelope(
- lat=la, lon=lo, acq_time=t, eid=f"clu{i}"))
- if evt is not None:
- events.append(evt)
- assert len(events) == 1, f"expected exactly one cluster event: {events}"
- assert events[0].category == "unattributed_hotspot_cluster"
- assert events[0].severity == "priority"
-
-
-# ═════════════════════════════════════════════════════════════════════════════
-# C. FirePacer covers FIRMS; immediate jumps the queue; nothing is dropped
-# (issue #119)
+# FirePacer covers FIRMS; immediate jumps the queue; nothing is dropped
+# (issue #119)
# ═════════════════════════════════════════════════════════════════════════════
class TestPacerCoversFirms:
- def test_firms_growth_broadcast_routes_through_pacer(self, consumer):
- """A real FIRMS growth broadcast (source=firms, severity=immediate)
- must be handed to the pacer, not emitted straight to the bus."""
- from unittest.mock import MagicMock
- c, bus = consumer
- pacer = MagicMock()
- c._pacer = pacer
-
- center_lat, center_lon = 42.0, -114.0
- _seed_fire(irwin_id="ID-PACE-G", lat=center_lat, lon=center_lon,
- name="Pine Gulch")
- for i in range(5):
- c._handle(_SUBJECT, _raw(_envelope(
- lat=center_lat + 0.0001 * i, lon=center_lon + 0.0001 * (i - 2),
- acq_time=f"12{i:02d}", frp=20.0 + i, eid=f"pga{i}")))
- pass_b_lat = center_lat + (1.0 / _MI_PER_DEG_LAT)
- event = c._handle(_SUBJECT, _raw(_envelope(
- lat=pass_b_lat, lon=center_lon, acq_time="1800", frp=22.0,
- eid="pgb")))
-
- assert event is not None
- assert event.category == "wildfire_growth"
- pacer.enqueue.assert_called_once_with(event)
- bus.emit.assert_not_called()
-
def test_immediate_event_emitted_before_already_queued_priority_events(self):
"""Two 'priority' events are queued first; a later 'immediate' event
must still be emitted BEFORE them (head-of-line), not after."""
@@ -376,8 +106,3 @@ class TestPacerCoversFirms:
assert len(emitted) == total, (
f"pacer must never drop events: expected {total}, got {len(emitted)}")
assert pacer.pending_count() == 0
-
-
-def _raw(envelope: dict) -> bytes:
- import json
- return json.dumps(envelope).encode()
diff --git a/work/tests/test_firms_handler.py b/work/tests/test_firms_handler.py
index 8543114..a86c83e 100644
--- a/work/tests/test_firms_handler.py
+++ b/work/tests/test_firms_handler.py
@@ -347,38 +347,3 @@ def test_short_acq_time_zero_padded(mem_db):
assert out is None
assert _row_count(mem_db, "firms_pixels") == 1
-
-# ============================================================================
-# Integration: envelope through the full consumer -> handler -> SQLite path
-# ============================================================================
-
-
-def test_end_to_end_envelope_through_consumer(mem_db, monkeypatch):
- """Confirm: envelope enters consumer._normalize, handle_firms is invoked,
- firms_pixels row is inserted, and consumer returns None (default-deny
- keeps the broadcast suppressed). mesh_broadcasts_out MUST stay empty."""
- from unittest.mock import MagicMock
- from meshai.config import Config
- from meshai.central.consumer import CentralConsumer
-
- cfg = Config()
- cfg.notifications.cold_start_grace_seconds = 0
- bus = MagicMock()
- consumer = CentralConsumer(cfg.environmental, bus)
-
- env = _firms_env(envelope_id="e2e_001")
- out = consumer._normalize(env["subject"], env)
-
- # consumer.normalize returns None -> Event never reaches bus.
- assert out is None
- bus.emit.assert_not_called()
-
- # firms_pixels MUST have the row; mesh_broadcasts_out MUST be empty.
- assert _row_count(mem_db, "firms_pixels") == 1
- assert _row_count(mem_db, "mesh_broadcasts_out") == 0
-
- # event_log records the storage.
- log = _last_event_log(mem_db)
- assert log["source"] == "firms"
- assert log["handled"] == 1
- assert log["table_name"] == "firms_pixels"
diff --git a/work/tests/test_firms_refactor.py b/work/tests/test_firms_refactor.py
index 6eb2b40..231bf32 100644
--- a/work/tests/test_firms_refactor.py
+++ b/work/tests/test_firms_refactor.py
@@ -450,83 +450,3 @@ class TestClusterBelowThreshold:
assert out is None
assert data == {}
-
-# ─────────────────────────────────────────────────────────────────────────────
-# 6. issue #121 — cutover severity must reach the Event (regression guard)
-# ─────────────────────────────────────────────────────────────────────────────
-# Spotting and halt used to stamp a plain "severity" key in data_patch.
-# central/consumer.py only ever promotes data["_severity_override"] onto
-# Event.severity (see consumer.py's issue #118 comment) -- the plain key was
-# a silent no-op, the same class of bug fixed for firms_handler.py's own
-# inline stamps in PR #120. These drive the REAL cutover path end-to-end
-# through CentralConsumer._normalize (the actual production entry point,
-# adapter=="firms" dispatch) and assert the emitted Event's severity, so a
-# regression back to a plain "severity" key fails loudly instead of silently.
-
-class TestCutoverSeverityReachesEvent:
- def _consumer(self):
- from unittest.mock import MagicMock
- from meshai.config import Config
- from meshai.central.consumer import CentralConsumer
- cfg = Config()
- cfg.notifications.cold_start_grace_seconds = 0
- return CentralConsumer(cfg.environmental, MagicMock())
-
- def test_spotting_cutover_event_severity_is_immediate(self, monkeypatch):
- _cutover(monkeypatch, "wildfire_spotting")
- try:
- _seed_pass_a_hex_then_close("ID-SEV-S", 43.0, -115.0)
- sp_lat, sp_lon = _offset_mi(43.0, -115.0,
- north_mi=2.0 / math.sqrt(2),
- east_mi=2.0 / math.sqrt(2))
- env = _envelope(lat=sp_lat, lon=sp_lon, acq_time="1800")
- env["id"] = "spot-sev-test"
- env["data"]["id"] = "spot-sev-test"
- env["data"]["geo"] = {"centroid": [sp_lon, sp_lat]}
-
- event = self._consumer()._normalize(_SUBJECT, env)
- assert event is not None, "expected a broadcast Event, got None"
- assert event.severity == "immediate", (
- f"issue #121 regression: expected 'immediate', got "
- f"{event.severity!r} -- gating.firms.decide's spotting "
- f"data_patch must use _severity_override, not the plain "
- f"'severity' key"
- )
- finally:
- from meshai.notifications.cutover import _clear_cache
- _clear_cache()
-
- def test_halt_cutover_event_severity_is_routine(self, monkeypatch):
- _cutover(monkeypatch, "wildfire_halted")
- try:
- from meshai.central import firms_handler as _fh
- fixed_now = 1780768800
- monkeypatch.setattr(_fh.time, "time", lambda: float(fixed_now))
- _seed_stale_fire("ID-SEV-H", now_epoch=fixed_now, idle_hours=14)
-
- # Any unrelated pixel arrival opportunistically triggers the halt
- # scan (_maybe_emit_halt runs on every pixel as a fallback); use
- # one far away so it isn't attributed to (and doesn't grow) the
- # stale fire instead.
- env = _envelope(lat=10.0, lon=10.0, acq_time="1800")
- env["id"] = "halt-sev-test"
- env["data"]["id"] = "halt-sev-test"
- env["data"]["geo"] = {"centroid": [10.0, 10.0]}
- # Raw envelope severity maps to "immediate" (>= immediate_min=3),
- # deliberately NOT "routine" -- so the assertion below can only
- # pass if the halt data_patch's _severity_override actually
- # overrides it down to "routine". A plain "severity" key (the
- # bug) would silently leave this at "immediate" instead.
- env["data"]["severity"] = 3
-
- event = self._consumer()._normalize(_SUBJECT, env)
- assert event is not None, "expected a broadcast Event, got None"
- assert event.severity == "routine", (
- f"issue #121 regression: expected 'routine', got "
- f"{event.severity!r} -- gating.firms.decide's halt "
- f"data_patch must use _severity_override, not the plain "
- f"'severity' key"
- )
- finally:
- from meshai.notifications.cutover import _clear_cache
- _clear_cache()
diff --git a/work/tests/test_hydro_refactor.py b/work/tests/test_hydro_refactor.py
index 0d53cbe..34556e0 100644
--- a/work/tests/test_hydro_refactor.py
+++ b/work/tests/test_hydro_refactor.py
@@ -1,19 +1,34 @@
"""Phase-3 hydro (USGS NWIS) refactor tests.
-Verifies the source-agnostic formatter+decider migration for the stream-gauge
-hazard, mirroring test_quake_refactor.py:
+Verifies the formatter+decider for the stream-gauge hazard (gating/hydro.py,
+formatters/hydro.py), mirroring test_quake_refactor.py:
-1. Golden byte-identical: formatters.hydro.format() reproduces the old
- nwis_handler._render() wire exactly, for a stage-only crossing, a paired
- flow(00060)+stage(00065) reading, and every threshold label.
+1. Golden: formatters.hydro.format() renders the expected wire, for a
+ stage-only crossing, a paired flow(00060)+stage(00065) reading, and every
+ threshold label.
-2. Gate-sequence parity: an explicit `now`-timeline of readings driven through
- the NEW gating.hydro.decide() matches the OLD handle_nwis broadcast/suppress
- behavior (upward crossing broadcasts; same-rank + receding suppress unless
- broadcast_on_recede).
+2. Gate-sequence: an explicit `now`-timeline of readings driven through
+ gating.hydro.decide() (upward crossing broadcasts; same-rank + receding
+ suppress unless broadcast_on_recede).
+
+The Central `nwis_handler` module (`_render()`, `handle_nwis()`) has been
+deleted along with the rest of the Central NATS consumer path. Pure
+old-vs-new parity assertions have been removed (original diffs are
+preserved in git history); what remains asserts against hand-written
+expected strings / broadcast outcomes.
+
+decide() is READ-ONLY over gauge_readings by design (the append-only INSERT
+was always caller-owned -- previously the Central handler, inline,
+immediately after calling decide()). With the handler gone there is no
+current producer for the "stream_flow" category in production (no native
+env/ adapter emits it -- meshai.env.usgs.USGSStreamsAdapter is a separate,
+older stream-gauge pipeline with different categories/schema). Tests below
+that need prior-reading state seed gauge_readings directly via a local SQL
+helper that mirrors the deleted handler's INSERT shape, so the gate logic
+itself stays under direct, native-only test coverage.
The real registry / cutover key is "stream_flow" — the flat category the
-Central nwis path produces for every central.hydro.* envelope.
+Central nwis path used to produce for every central.hydro.* envelope.
"""
from __future__ import annotations
@@ -21,7 +36,7 @@ import pytest
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, run_gate_sequence
+from tests.harness.goldens import assert_byte_identical
_AT = 1_783_200_000.0 # pinned epoch (unused by hydro render/gate, kept for parity)
@@ -53,11 +68,7 @@ def _make_fake_event(data: dict):
# ─────────────────────────────────────────────────────────────────────────────
class TestFormatterGolden:
- """formatters.hydro.format() == nwis_handler._render() for the same inputs."""
-
- def _render_old(self, **kw):
- from meshai.central.nwis_handler import _render
- return _render(**kw)
+ """formatters.hydro.format() renders the expected wire for canonical data."""
def _fmt_new(self, canonical: dict) -> str:
from meshai.notifications.formatters.hydro import format as hfmt
@@ -74,13 +85,10 @@ class TestFormatterGolden:
"lat": 43.612,
"lon": -111.654,
}
- old = self._render_old(
- gauge_name="Snake River at Heise", threshold_state="action",
- stage_ft=12.5, flow_cfs=None, unit="ft", lat=43.612, lon=-111.654,
- )
new = self._fmt_new(canonical)
- assert_byte_identical(new, old)
- assert new == "🌊 New: Snake River at Heise: action stage 12.5 ft, @ 43.612,-111.654"
+ assert_byte_identical(
+ new, "🌊 New: Snake River at Heise: action stage 12.5 ft, @ 43.612,-111.654"
+ )
def test_paired_flow_and_stage(self):
"""00060 discharge back-looked onto a 00065 stage: flow segment present."""
@@ -93,14 +101,11 @@ class TestFormatterGolden:
"lat": 43.600,
"lon": -116.200,
}
- old = self._render_old(
- gauge_name="Boise River", threshold_state="flood_minor",
- stage_ft=14.5, flow_cfs=8400, unit="ft", lat=43.600, lon=-116.200,
- )
new = self._fmt_new(canonical)
- assert_byte_identical(new, old)
- assert "flow 8,400 cfs" in new
- assert "minor flooding 14.5 ft" in new
+ assert_byte_identical(
+ new,
+ "🌊 New: Boise River: minor flooding 14.5 ft, flow 8,400 cfs, @ 43.600,-116.200",
+ )
@pytest.mark.parametrize(
"state,label",
@@ -112,7 +117,7 @@ class TestFormatterGolden:
],
)
def test_every_threshold_label(self, state, label):
- """Each threshold_state maps to the correct label — byte-identical to _render."""
+ """Each threshold_state maps to the correct label."""
canonical = {
"gauge_name": "Test Gauge",
"threshold_state": state,
@@ -122,16 +127,13 @@ class TestFormatterGolden:
"lat": 44.0,
"lon": -114.0,
}
- old = self._render_old(
- gauge_name="Test Gauge", threshold_state=state, stage_ft=20.0,
- flow_cfs=None, unit="ft", lat=44.0, lon=-114.0,
- )
new = self._fmt_new(canonical)
- assert_byte_identical(new, old)
- assert f"{label} 20.0 ft" in new
+ assert_byte_identical(
+ new, f"🌊 New: Test Gauge: {label} 20.0 ft, @ 44.000,-114.000"
+ )
def test_missing_coords_drops_at_tail(self):
- """No coords → no @ segment (byte-identical to _render)."""
+ """No coords → no @ segment."""
canonical = {
"gauge_name": "No Coords Gauge",
"threshold_state": "action",
@@ -141,13 +143,8 @@ class TestFormatterGolden:
"lat": None,
"lon": None,
}
- old = self._render_old(
- gauge_name="No Coords Gauge", threshold_state="action",
- stage_ft=10.0, flow_cfs=None, unit="ft", lat=None, lon=None,
- )
new = self._fmt_new(canonical)
- assert_byte_identical(new, old)
- assert "@" not in new
+ assert_byte_identical(new, "🌊 New: No Coords Gauge: action stage 10.0 ft")
# ─────────────────────────────────────────────────────────────────────────────
@@ -183,33 +180,35 @@ def _parse_iso_epoch(s):
return int(datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp())
+def _insert_reading(conn, *, site_id, gauge_name, value, unit,
+ threshold_state, flow_cfs, reading_time, lat, lon):
+ """Directly seed a gauge_readings row.
+
+ Mirrors the schema the deleted Central nwis_handler used to INSERT
+ inline, immediately after calling decide(). decide() is read-only over
+ this table by design (see gating/hydro.py docstring) -- the INSERT was
+ always caller-owned, so tests seed state directly instead of reaching
+ into the deleted handler.
+ """
+ conn.execute(
+ "INSERT INTO gauge_readings(site_id, gauge_name, reading_value, "
+ "reading_unit, threshold_state, flow_cfs, reading_time, lat, lon) "
+ "VALUES (?,?,?,?,?,?,?,?,?)",
+ (site_id, gauge_name, value, unit, threshold_state, flow_cfs,
+ reading_time, lat, lon),
+ )
+
+
class TestGateSequenceParity:
- """New decide() decisions match old handle_nwis broadcast/suppress."""
+ """gating.hydro.decide() broadcast/suppress across a reading timeline."""
@pytest.fixture(autouse=True)
def _db(self, mem_db):
self.db = mem_db
- def _old_gate(self, fixture, *, now):
- """OLD path: handle_nwis returning non-None = broadcast.
-
- handle_nwis owns the append-only gauge_readings INSERT, so replaying
- through it advances the persisted time-series exactly as production
- would — the decider (below) then reads that same state.
- """
- from meshai.central.nwis_handler import handle_nwis
- env = fixture["envelope"]
- wire = handle_nwis(env, env["subject"], data={}, now=int(now))
- return wire is not None
-
- def _new_gate(self, fixture, *, now):
- """NEW path: build canonical (as the handler does) then decide().
-
- We do NOT insert here — the old-gate replay already advances
- gauge_readings; the decider only READS prior state. This mirrors the
- production ordering where decide() runs before the inline INSERT.
- """
- from meshai.notifications.gating.hydro import decide
+ def _canonical(self, fixture):
+ """Build canonical data from a Central-style fixture (as the deleted
+ handler used to) via the still-live idaho_gauge_sites helpers."""
from meshai.central.idaho_gauge_sites import (
compute_threshold_state, lookup_site, normalize_site_id,
)
@@ -238,10 +237,34 @@ class TestGateSequenceParity:
"lon": d.get("longitude"),
"parameter_code": pc,
}
+ return canonical, value
+
+ def _decide(self, fixture, *, now):
+ """decide() only (no persist) — for assertions on a single reading."""
+ from meshai.notifications.gating.hydro import decide
+ canonical, _value = self._canonical(fixture)
return decide(canonical, source="nwis", now=float(now))
+ def _decide_and_persist(self, fixture, *, now):
+ """decide() then INSERT the resolved reading — mirrors the deleted
+ handler's decide-then-insert ordering, so later steps in a sequence
+ see accumulated prior state exactly as production would."""
+ from meshai.notifications.gating.hydro import decide
+ canonical, value = self._canonical(fixture)
+ gate = decide(canonical, source="nwis", now=float(now))
+ threshold_state = gate.data_patch.get("threshold_state", canonical["threshold_state"])
+ stage_ft = gate.data_patch.get("stage_ft", canonical["stage_ft"])
+ _insert_reading(
+ self.db,
+ site_id=canonical["site_id"], gauge_name=canonical["gauge_name"],
+ value=value, unit=canonical["unit"], threshold_state=threshold_state,
+ flow_cfs=canonical["flow_cfs"], reading_time=canonical["reading_time"],
+ lat=canonical["lat"], lon=canonical["lon"],
+ )
+ return gate
+
def test_gate_sequence_matches(self):
- """Timeline of Heise readings: old and new gates agree on every step.
+ """Timeline of Heise readings: decide() agrees with expectations at every step.
Heise (USGS-13186000): action=12.0ft.
[0] 8.0 ft normal (first reading, no prior) → suppress
@@ -264,22 +287,16 @@ class TestGateSequenceParity:
]
timeline = [float(base + i * 900) for i in range(len(specs))]
- results = run_gate_sequence(self._old_gate, self._new_gate, ordered,
- timeline=timeline)
- mismatches = [r for r in results if not r["match"]]
- assert not mismatches, (
- "Gate sequence mismatch old handle_nwis vs new decide():\n"
- + "\n".join(
- f" step {r['fixture_n']}: old={r['old_broadcast']} "
- f"new={r['new_broadcast']} diffs={r['diffs']}"
- for r in mismatches
- )
- )
- assert results[0]["old_broadcast"] is False, "normal first reading suppressed"
- assert results[1]["old_broadcast"] is True, "normal→action broadcasts"
- assert results[2]["old_broadcast"] is False, "action→action suppressed"
- assert results[3]["old_broadcast"] is True, "action→flood_minor broadcasts"
- assert results[4]["old_broadcast"] is False, "receding suppressed (no toggle)"
+ results = [
+ self._decide_and_persist(fx, now=t)
+ for fx, t in zip(ordered, timeline)
+ ]
+
+ assert results[0].broadcast is False, "normal first reading suppressed"
+ assert results[1].broadcast is True, "normal→action broadcasts"
+ assert results[2].broadcast is False, "action→action suppressed"
+ assert results[3].broadcast is True, "action→flood_minor broadcasts"
+ assert results[4].broadcast is False, "receding suppressed (no toggle)"
def test_00060_backlook_inherits_stage_band(self, mem_db):
"""A 00060 discharge reading inherits the last 00065 stage band.
@@ -288,16 +305,14 @@ class TestGateSequenceParity:
decider's back-look must resolve threshold_state=action + the prior
stage_ft, and (same rank as the seeded action) suppress the discharge.
"""
- from meshai.notifications.gating.hydro import decide
- # Seed a 00065 stage reading at action via the old handler (writes row).
env_stage = _nwis_env(parameter_code="00065", value=12.5,
time_iso="2026-06-05T10:00:00Z", envelope_id="seed")
- self._old_gate({"envelope": env_stage}, now=1_000_000)
+ self._decide_and_persist({"envelope": env_stage}, now=1_000_000)
# Now decide on a 00060 discharge — should back-look the action band.
env_flow = _nwis_env(parameter_code="00060", value=8400, unit="ft^3/s",
time_iso="2026-06-05T10:05:00Z", envelope_id="q")
- gate = self._new_gate({"envelope": env_flow}, now=1_000_300)
+ gate = self._decide({"envelope": env_flow}, now=1_000_300)
assert gate.data_patch["threshold_state"] == "action"
assert gate.data_patch["stage_ft"] == 12.5
# action → action (same rank) → suppress
@@ -306,11 +321,10 @@ class TestGateSequenceParity:
def test_recede_toggle_enables_broadcast(self, mem_db):
"""With broadcast_on_recede set, a receding crossing broadcasts."""
from meshai.adapter_config._accessor import set_runtime_override, _overrides
- from meshai.notifications.gating.hydro import decide
# Seed an action reading.
env_high = _nwis_env(parameter_code="00065", value=12.5,
time_iso="2026-06-05T10:00:00Z", envelope_id="hi")
- self._old_gate({"envelope": env_high}, now=1_000_000)
+ self._decide_and_persist({"envelope": env_high}, now=1_000_000)
# Force the recede toggle on for the decision only (runtime override,
# since adapter_config accessors are read-only).
@@ -318,7 +332,7 @@ class TestGateSequenceParity:
try:
env_low = _nwis_env(parameter_code="00065", value=8.0,
time_iso="2026-06-05T11:00:00Z", envelope_id="lo")
- gate = self._new_gate({"envelope": env_low}, now=1_003_600)
+ gate = self._decide({"envelope": env_low}, now=1_003_600)
finally:
_overrides.pop(("usgs_nwis", "broadcast_on_recede"), None)
assert gate.broadcast is True, "receding must broadcast when toggle is on"
diff --git a/work/tests/test_incident_handler.py b/work/tests/test_incident_handler.py
deleted file mode 100644
index 7b59543..0000000
--- a/work/tests/test_incident_handler.py
+++ /dev/null
@@ -1,828 +0,0 @@
-"""Tests for meshai.central.incident_handler (v0.5.9).
-
-Coverage:
- Tomtom parsing + rendering (a/b/c/d):
- (a) jam, accident, road_closed, lane_closed, road_works each render with
- correct emoji + phrase + delay segment
- (b) magnitude_of_delay == 0 events filtered at entrance
- (c) delay == null events render WITHOUT the delay segment
- (d) time_validity past/future events filtered
-
- Per-incident change-detection (e-i):
- (e) republish with no change -> drop silently, no new audit
- (f) magnitude bump up -> Update
- (g) delay double (>=2x) -> Update
- (h) icon change -> Update
-
- state_511 / itd_511 EventType branching (j-m):
- (j) state_511_atis incident parses
- (k) state_511_atis closure parses
- (l) state_511_atis special_event parses (synthetic)
- (m) itd_511 incident parses
-
- Decoupled callback (n) and traffic_events UPSERT (o):
- (n) cold-start scenario -- handler runs but callback never fires;
- second pass still emits New: (not Update:)
- (o) existing traffic_events row gets UPSERTed across passes
-"""
-
-import re
-import time
-
-import pytest
-
-from meshai.central.incident_handler import (
- handle_incident,
- _render as _incident_render,
-)
-from meshai.persistence import close_thread_connection, init_db
-from meshai.persistence import db as persistence_db
-
-
-# ---------- fixtures ------------------------------------------------------
-
-
-@pytest.fixture
-def mem_db(monkeypatch, tmp_path):
- db_path = str(tmp_path / "incident-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)
-
-
-@pytest.fixture
-def no_photon(monkeypatch):
- import meshai.central_normalizer as cn
- monkeypatch.setattr(cn, "_photon_reverse_places", lambda lat, lon: [])
- if hasattr(cn, "_H3_NEAREST_CACHE"):
- cn._H3_NEAREST_CACHE.clear()
-
-
-# ---------- tomtom envelope builder --------------------------------------
-
-
-_TTI_A = "cfb0c03f-9ab9-46f9-ac21-9b17d0f715f2"
-_TTI_B = "13ca4176-4eea-428e-a807-49bee662159a"
-_TTI_C = "3573b54b-9e55-4aff-83d3-253048825e77"
-
-
-def _tomtom_env(*, tti=_TTI_A,
- icon_category=6,
- magnitude=4,
- delay=412,
- time_validity="present",
- description="Queuing traffic on I-84 Westbound from Orchard St to ID-55. ",
- road_numbers=("I-84",),
- from_loc="Orchard St/Exit 52 (I-84)",
- to_loc="ID-55/Exit 46 (I-84)",
- start_time=None,
- end_time=None,
- lat=43.5833926533, lon=-116.2598321532,
- state_code="ID", bbox_name="treasure_valley_ext",
- geocoder_city="Boise"):
- inner_id = f"ID:tomtom:TTI-{tti}-TTR{int(time.time()*1000)}"
- geocoder = {"city": geocoder_city, "county": "Ada", "state": "ID",
- "country": "United States", "landclass": None}
- return {
- "id": inner_id,
- "subject": "central.traffic.incident.id",
- "data": {
- "id": inner_id, "adapter": "tomtom_incidents",
- "category": "incident.tomtom_incidents", "severity": 1,
- "geo": {"centroid": [lon, lat], "primary_region": "US-ID"},
- "data": {
- "id": inner_id,
- "description": description,
- "event_code": 108,
- "from": from_loc, "to": to_loc,
- "magnitude_of_delay": magnitude,
- "icon_category": icon_category,
- "length": 6112.13,
- "delay": delay,
- "road_numbers": list(road_numbers),
- "start_time": start_time, "end_time": end_time,
- "time_validity": time_validity,
- "state_code": state_code, "bbox_name": bbox_name,
- "latitude": lat, "longitude": lon,
- "_enriched": {"geocoder": geocoder},
- },
- },
- }
-
-
-def _state_511_env(*, layer="Incidents", category_prefix="incident",
- event_sub_type="crash",
- roadway="US-95", direction="Both",
- is_full_closure=False,
- external_id="ID:Incidents:33948",
- lat=48.5295, lon=-116.4293,
- geocoder_city="Naples", county="Boundary"):
- return {
- "id": external_id,
- "subject": f"central.traffic.{category_prefix}.id",
- "data": {
- "id": external_id, "adapter": "state_511_atis",
- "category": f"{category_prefix}.state_511_atis", "severity": 1,
- "geo": {"centroid": [lon, lat], "primary_region": "US-WA"},
- "data": {
- "roadway_name": roadway, "direction": direction,
- "event_sub_type": event_sub_type,
- "description": "test event",
- "is_full_closure": is_full_closure,
- "layer": layer,
- # v0.5.9 GAMMA: default to non-ID neighbor state because
- # state_511_atis no longer covers Idaho (itd_511 took over).
- # Tests that want to exercise the ID-skip path override these.
- "county": county, "state": "Washington", "state_code": "WA",
- "start_date": None,
- "last_updated": None,
- "latitude": lat, "longitude": lon,
- "_enriched": {"geocoder": {
- "city": geocoder_city, "county": county, "state": "ID",
- "country": "United States", "landclass": None,
- }},
- },
- },
- }
-
-
-def _itd_511_env(*, category_prefix="incident",
- event_type_short="incident",
- event_sub_type="crash",
- roadway="I-84", direction="East",
- is_full_closure=False,
- external_id="ITD:469:17",
- lat=43.6486, lon=-116.4870,
- geocoder_city="Caldwell"):
- return {
- "id": external_id,
- "subject": f"central.traffic.{category_prefix}.us.id",
- "data": {
- "id": external_id, "adapter": "itd_511",
- "category": f"{category_prefix}.itd_511", "severity": 1,
- "geo": {"centroid": [lon, lat], "primary_region": "US-ID"},
- "data": {
- "event_type_short": event_type_short,
- "event_sub_type": event_sub_type,
- "roadway_name": roadway, "direction": direction,
- "description": "test itd event",
- "lanes_affected": "All lanes affected",
- "is_full_closure": is_full_closure,
- "itd_severity": "None",
- "comment": "", "cause": "roadwork",
- "organization": "ERS",
- "recurrence_text": "", "recurrence_schedules": [],
- "restrictions": {}, "encoded_polyline": "",
- "id_internal": 17, "source_id": "469",
- "reported_epoch": None,
- "last_updated_epoch": None,
- "start_epoch": None,
- "planned_end_epoch": None,
- "latitude": lat, "longitude": lon,
- "_enriched": {"geocoder": {
- "city": geocoder_city, "county": "Canyon", "state": "ID",
- }},
- },
- },
- }
-
-
-def _commit(data, committed_at):
- cb = data.get("_on_broadcast_committed")
- assert callable(cb), "handler must attach commit callback"
- cb(committed_at)
-
-
-# ============================================================================
-# (a) tomtom parsing -- all five icon categories render correctly
-# ============================================================================
-
-
-@pytest.mark.parametrize("icon, expected_emoji, expected_phrase", [
- (1, "🚨", "Crash"),
- (6, "🚗", "Stationary Traffic"),
- (7, "🟠", "Lane Reduction"),
- (8, "🚫", "Road Closed"),
- (9, "🚧", "Road Works"),
-])
-def test_a_tomtom_icon_renders(mem_db, no_photon, icon, expected_emoji, expected_phrase):
- env = _tomtom_env(icon_category=icon, delay=300)
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- assert wire.startswith(f"{expected_emoji} {expected_phrase}")
- assert "Near Boise, ID" in wire
- assert "I-84" in wire
- # Budget-fit rework: the separate "N min delay" line was dropped; the road
- # segment carries road (+ direction/lanes), and the message fits 140.
- assert "min delay" not in wire
- assert len(wire) <= 140
-
-
-# ============================================================================
-# (b) tomtom magnitude_of_delay == 0 events filtered
-# ============================================================================
-
-
-def test_b_tomtom_magnitude_zero_filtered(mem_db, no_photon):
- env = _tomtom_env(icon_category=6, magnitude=0, delay=10)
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=1_000_000)
- assert wire is None
- # filtered envelope leaves no traffic_events row
- n_rows = mem_db.execute("SELECT COUNT(*) AS n FROM traffic_events").fetchone()["n"]
- assert n_rows == 0
- # but the event IS logged to event_log handled=0 for accounting
- n_log = mem_db.execute(
- "SELECT COUNT(*) AS n FROM event_log WHERE source='tomtom_incidents'"
- ).fetchone()["n"]
- assert n_log == 1
- assert "_on_broadcast_committed" not in data
-
-
-# ============================================================================
-# (c) tomtom delay == null events render WITHOUT delay segment
-# ============================================================================
-
-
-def test_c_tomtom_delay_null_no_delay_segment(mem_db, no_photon):
- env = _tomtom_env(icon_category=1, delay=None)
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- assert "Crash" in wire
- assert "min delay" not in wire # no delay segment
-
-
-# ============================================================================
-# (d) tomtom time_validity past/future filtered
-# ============================================================================
-
-
-@pytest.mark.parametrize("validity", ["past", "future"])
-def test_d_tomtom_time_validity_filtered(mem_db, no_photon, validity):
- env = _tomtom_env(icon_category=6, magnitude=2, delay=300,
- time_validity=validity)
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=1_000_000)
- assert wire is None
- n_rows = mem_db.execute(
- "SELECT COUNT(*) AS n FROM traffic_events").fetchone()["n"]
- assert n_rows == 0
-
-
-# ============================================================================
-# (e) per-incident dedup -- republish with no change drops silently
-# ============================================================================
-
-
-def test_e_per_incident_dedup_no_change(mem_db, no_photon):
- env = _tomtom_env(icon_category=6, delay=300)
- data1 = {}
- wire1 = handle_incident(env, env["subject"], data=data1, now=1_000_000)
- assert wire1 is not None
- _commit(data1, 1_000_001)
-
- # Re-publish 5 minutes later, same magnitude/delay/icon.
- data2 = {}
- wire2 = handle_incident(env, env["subject"], data=data2, now=1_000_300)
- assert wire2 is None # no change, no broadcast
-
-
-# ============================================================================
-# (f) magnitude bump triggers Update
-# ============================================================================
-
-
-def test_f_magnitude_bump_triggers_update(mem_db, no_photon):
- env1 = _tomtom_env(icon_category=6, delay=300)
- data1 = {}
- handle_incident(env1, env1["subject"], data=data1, now=1_000_000)
- _commit(data1, 1_000_001)
-
- # v0.5.9 REVISED gate (A): magnitude bump no longer fires Update.
- # State still flips in traffic_events, but no wire string returns.
- env2 = _tomtom_env(icon_category=6, magnitude=5, delay=300)
- data2 = {}
- wire2 = handle_incident(env2, env2["subject"], data=data2, now=1_000_300)
- assert wire2 is None
- # Current magnitude tracked in the row.
- row = mem_db.execute(
- "SELECT magnitude_of_delay FROM traffic_events "
- "WHERE source='tomtom_incidents'").fetchone()
- assert row["magnitude_of_delay"] == 5
-
-
-# ============================================================================
-# (g) delay double triggers Update
-# ============================================================================
-
-
-def test_g_delay_double_triggers_update(mem_db, no_photon):
- env1 = _tomtom_env(icon_category=6, delay=300)
- data1 = {}
- handle_incident(env1, env1["subject"], data=data1, now=1_000_000)
- _commit(data1, 1_000_001)
-
- # v0.5.9 REVISED gate (A): delay double no longer fires Update.
- env2 = _tomtom_env(icon_category=6, delay=700)
- data2 = {}
- wire2 = handle_incident(env2, env2["subject"], data=data2, now=1_000_300)
- assert wire2 is None
- row = mem_db.execute(
- "SELECT delay_seconds FROM traffic_events "
- "WHERE source='tomtom_incidents'").fetchone()
- assert row["delay_seconds"] == 700
-
-
-def test_g_delay_below_double_no_update(mem_db, no_photon):
- """delay 300 -> 500 (1.67x) should NOT trigger broadcast."""
- env1 = _tomtom_env(icon_category=6, delay=300)
- data1 = {}
- handle_incident(env1, env1["subject"], data=data1, now=1_000_000)
- _commit(data1, 1_000_001)
-
- env2 = _tomtom_env(icon_category=6, delay=500)
- data2 = {}
- wire2 = handle_incident(env2, env2["subject"], data=data2, now=1_000_300)
- assert wire2 is None
-
-
-# ============================================================================
-# (h) icon change triggers Update
-# ============================================================================
-
-
-def test_h_icon_change_triggers_update(mem_db, no_photon):
- env1 = _tomtom_env(icon_category=6, delay=300)
- data1 = {}
- handle_incident(env1, env1["subject"], data=data1, now=1_000_000)
- _commit(data1, 1_000_001)
-
- # v0.5.9 REVISED gate (A): icon change no longer fires Update.
- env2 = _tomtom_env(icon_category=8, delay=300)
- data2 = {}
- wire2 = handle_incident(env2, env2["subject"], data=data2, now=1_000_300)
- assert wire2 is None
- row = mem_db.execute(
- "SELECT icon_category FROM traffic_events "
- "WHERE source='tomtom_incidents'").fetchone()
- assert row["icon_category"] == "road_closed"
-
-
-# ============================================================================
-# (j) state_511_atis incident parses
-# ============================================================================
-
-
-def test_j_state_511_incident_parses(mem_db, no_photon):
- env = _state_511_env(layer="Incidents", category_prefix="incident",
- event_sub_type="crash")
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- assert wire.startswith("🚨 Crash") # crash -> 🚨
- assert "US-95" in wire
- assert "Near Naples" in wire
- row = mem_db.execute(
- "SELECT source, sub_type, state FROM traffic_events "
- "WHERE source='state_511_atis'").fetchone()
- assert row["sub_type"] == "accident"
- # v0.5.9 GAMMA: state_511_atis is non-ID only -- helper now defaults to WA.
- assert row["state"] == "WA"
-
-
-# ============================================================================
-# (k) state_511_atis closure parses
-# ============================================================================
-
-
-def test_k_state_511_closure_parses(mem_db, no_photon):
- env = _state_511_env(layer="Closures", category_prefix="closure",
- event_sub_type="roadConstruction",
- is_full_closure=True,
- external_id="ID:Closures:33950")
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- # roadConstruction -> road_works -> 🚧
- assert wire.startswith("🚧 Road Works")
- assert "US-95" in wire
-
-
-# ============================================================================
-# (l) state_511_atis special_event parses
-# ============================================================================
-
-
-def test_l_state_511_special_event_parses(mem_db, no_photon):
- env = _state_511_env(layer="Special Events",
- category_prefix="special_event",
- event_sub_type="parade",
- external_id="ID:SpecialEvents:42")
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- # parade -> 🎪
- assert wire.startswith("🎪 Parade")
- assert "US-95" in wire
-
-
-# ============================================================================
-# (m) itd_511 incident parses
-# ============================================================================
-
-
-def test_m_itd_511_incident_parses(mem_db, no_photon):
- env = _itd_511_env(category_prefix="incident",
- event_type_short="incident",
- event_sub_type="crash")
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- assert wire.startswith("🚨 Crash")
- row = mem_db.execute(
- "SELECT source, state FROM traffic_events "
- "WHERE source='itd_511'").fetchone()
- assert row["state"] == "ID"
-
-
-# ============================================================================
-# (n) decoupled callback -- cold-start scenario still emits New: on second pass
-# ============================================================================
-
-
-def test_n_cold_start_then_resume_still_new(mem_db, no_photon):
- env = _tomtom_env(icon_category=6, delay=300)
- data1 = {}
- wire1 = handle_incident(env, env["subject"], data=data1, now=1_000_000)
- assert wire1.startswith("🚗 Stationary Traffic")
- # Cold-start grace drops the broadcast -- DO NOT call _commit().
-
- # 5 minutes later, same incident republishes.
- data2 = {}
- wire2 = handle_incident(env, env["subject"], data=data2, now=1_000_300)
- assert wire2 is not None
- assert wire2.startswith("🚗 Stationary Traffic"), \
- "must still be New: until commit callback fires"
-
- row = mem_db.execute(
- "SELECT last_broadcast_at, last_broadcast_magnitude "
- "FROM traffic_events WHERE source='tomtom_incidents'").fetchone()
- assert row["last_broadcast_at"] is None
- assert row["last_broadcast_magnitude"] is None
-
-
-# ============================================================================
-# (o) traffic_events row gets UPSERTed on each pass; event_log handled flips
-# ============================================================================
-
-
-def test_o_traffic_events_upsert_and_event_log_handled_flip(mem_db, no_photon):
- env1 = _tomtom_env(icon_category=6, delay=300)
- data1 = {}
- handle_incident(env1, env1["subject"], data=data1, now=1_000_000)
-
- # event_log row exists with handled=0 BEFORE callback.
- el_pre = mem_db.execute(
- "SELECT handled FROM event_log "
- "WHERE source='tomtom_incidents' ORDER BY id DESC LIMIT 1"
- ).fetchone()
- assert el_pre["handled"] == 0
-
- _commit(data1, 1_000_001)
-
- el_post = mem_db.execute(
- "SELECT handled FROM event_log "
- "WHERE source='tomtom_incidents' ORDER BY id DESC LIMIT 1"
- ).fetchone()
- assert el_post["handled"] == 1
-
- fr_post = mem_db.execute(
- "SELECT last_broadcast_at, last_broadcast_magnitude, "
- "last_broadcast_delay_seconds, last_broadcast_icon_category "
- "FROM traffic_events WHERE source='tomtom_incidents'"
- ).fetchone()
- assert fr_post["last_broadcast_at"] == 1_000_001
- assert fr_post["last_broadcast_magnitude"] == 4
- assert fr_post["last_broadcast_delay_seconds"] == 300
- assert fr_post["last_broadcast_icon_category"] == "jam"
-
- # Re-publish: UPSERT updates current_* but doesn't touch last_broadcast_*.
- env2 = _tomtom_env(icon_category=6, delay=500)
- data2 = {}
- wire2 = handle_incident(env2, env2["subject"], data=data2, now=1_000_300)
- # v0.5.9 REVISED: no Update broadcasts regardless of delta size --
- # under the OLD rule this was 'delay 1.67x not enough', now it's
- # 'we never re-broadcast'.
- assert wire2 is None
- fr2 = mem_db.execute(
- "SELECT delay_seconds, last_broadcast_delay_seconds "
- "FROM traffic_events WHERE source='tomtom_incidents'"
- ).fetchone()
- assert fr2["delay_seconds"] == 500 # UPSERT happened
- assert fr2["last_broadcast_delay_seconds"] == 300 # last broadcast unchanged
-
-
-# ============================================================================
-# v0.5.9 REVISED -- conservative gates
-# ============================================================================
-
-
-def test_p_known_id_all_changed_no_broadcast(mem_db, no_photon):
- """Regression guard for the new no-Update rule. Republish the SAME
- external_id with magnitude AND delay AND icon all changed. The old rule
- would have triggered Update on any one of those; the new rule fires
- nothing."""
- env1 = _tomtom_env(icon_category=6, delay=300)
- data1 = {}
- wire1 = handle_incident(env1, env1["subject"], data=data1, now=1_000_000)
- assert wire1.startswith("🚗 Stationary Traffic")
- _commit(data1, 1_000_001)
-
- env2 = _tomtom_env(icon_category=8, magnitude=5, delay=700) # ALL changed
- data2 = {}
- wire2 = handle_incident(env2, env2["subject"], data=data2, now=1_000_300)
- assert wire2 is None, "no Update broadcasts under the v0.5.9 REVISED rule"
-
- # State still tracks the latest field values.
- row = mem_db.execute(
- "SELECT magnitude_of_delay, delay_seconds, icon_category "
- "FROM traffic_events WHERE source='tomtom_incidents'").fetchone()
- assert row["magnitude_of_delay"] == 5
- assert row["delay_seconds"] == 700
- assert row["icon_category"] == "road_closed"
-
-
-# ============================================================================
-# Freshness gate (q/r/s)
-# ============================================================================
-
-
-def _now_anchor_relative(start_age_seconds: int):
- """Choose (now, start_time_iso) so that `now - parse(start_time) ==
- start_age_seconds`. Used by the freshness-gate tests."""
- import datetime as _dt
- now = 2_000_000_000 # arbitrary fixed epoch >>1970
- start_iso = _dt.datetime.fromtimestamp(
- now - start_age_seconds, tz=_dt.timezone.utc
- ).strftime("%Y-%m-%dT%H:%M:%SZ")
- return now, start_iso
-
-
-def test_q_fresh_event_15min_ago_broadcasts(mem_db, no_photon):
- """Event started 15 min ago -- WITHIN the 30-min fresh window. New: fires."""
- now, start_iso = _now_anchor_relative(15 * 60)
- env = _tomtom_env(icon_category=6, delay=300,
- start_time=start_iso)
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=now)
- assert wire is not None
- assert wire.startswith("🚗 Stationary Traffic")
- n_rows = mem_db.execute(
- "SELECT COUNT(*) AS n FROM traffic_events").fetchone()["n"]
- assert n_rows == 1
-
-
-def test_r_stale_event_45min_ago_dropped_no_row(mem_db, no_photon):
- """Event started 45 min ago -- OUTSIDE the 30-min window. Drop AT
- handler entrance, no UPSERT into traffic_events, event_log handled=0."""
- now, start_iso = _now_anchor_relative(45 * 60)
- env = _tomtom_env(icon_category=6, magnitude=2, delay=300,
- start_time=start_iso)
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=now)
- assert wire is None
-
- n_rows = mem_db.execute(
- "SELECT COUNT(*) AS n FROM traffic_events").fetchone()["n"]
- assert n_rows == 0, "no UPSERT for stale incidents"
-
- n_log = mem_db.execute(
- "SELECT COUNT(*) AS n FROM event_log WHERE handled=0 "
- "AND source='tomtom_incidents'").fetchone()["n"]
- assert n_log == 1, "stale envelope must still be logged (handled=0)"
-
-
-def test_s_null_start_time_default_allow(mem_db, no_photon):
- """start_time missing -> default-allow (treat as fresh and broadcast)."""
- env = _tomtom_env(icon_category=6, delay=300,
- start_time=None)
- data = {}
- wire = handle_incident(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- assert wire.startswith("🚗 Stationary Traffic")
-
-
-# ============================================================================
-# Per-source startTime field path (t)
-# ============================================================================
-
-
-def test_t_state_511_start_date_path(mem_db, no_photon):
- """state_511_atis pulls start_time from inner.data.start_date ('5/28/26,
- 10:45 PM' format). Construct fresh (within 30 min) and stale (>30 min)
- cases and confirm gate behavior is per-source-correct."""
- # 5 min ago in the 511-date format.
- import datetime as _dt
- now = int(_dt.datetime(2026, 6, 4, 12, 0, tzinfo=_dt.timezone.utc).timestamp())
- fresh_date = _dt.datetime.fromtimestamp(now - 5 * 60,
- tz=_dt.timezone.utc).strftime(
- "%-m/%-d/%y, %-I:%M %p")
- env = _state_511_env(layer="Incidents", category_prefix="incident",
- event_sub_type="crash")
- # Replace the default start_date with a fresh one.
- env["data"]["data"]["start_date"] = fresh_date
- wire = handle_incident(env, env["subject"], data={}, now=now)
- assert wire is not None
- assert wire.startswith("🚨 Crash")
-
- # Stale variant (45 min ago).
- stale_date = _dt.datetime.fromtimestamp(now - 45 * 60,
- tz=_dt.timezone.utc).strftime(
- "%-m/%-d/%y, %-I:%M %p")
- env2 = _state_511_env(layer="Incidents", category_prefix="incident",
- event_sub_type="crash",
- external_id="ID:Incidents:99999")
- env2["data"]["data"]["start_date"] = stale_date
- wire2 = handle_incident(env2, env2["subject"], data={}, now=now)
- assert wire2 is None
-
-
-def test_t_itd_511_start_epoch_path(mem_db, no_photon):
- """itd_511 pulls start_time from inner.data.start_epoch (Unix int)."""
- now = 1_700_000_000
- # Fresh (5 min ago) variant.
- env = _itd_511_env(category_prefix="incident",
- event_type_short="incident",
- event_sub_type="crash")
- env["data"]["data"]["start_epoch"] = now - 5 * 60
- wire = handle_incident(env, env["subject"], data={}, now=now)
- assert wire is not None
- assert wire.startswith("🚨 Crash")
-
- # Stale variant (45 min ago).
- env2 = _itd_511_env(category_prefix="incident",
- event_type_short="incident",
- event_sub_type="crash",
- external_id="ITD:469:99999")
- env2["data"]["data"]["start_epoch"] = now - 45 * 60
- wire2 = handle_incident(env2, env2["subject"], data={}, now=now)
- assert wire2 is None
-
-
-def test_t_tomtom_start_time_path(mem_db, no_photon):
- """tomtom pulls start_time from inner.data.start_time (ISO-8601)."""
- now, fresh_iso = _now_anchor_relative(5 * 60)
- env = _tomtom_env(icon_category=6, delay=300,
- start_time=fresh_iso)
- wire = handle_incident(env, env["subject"], data={}, now=now)
- assert wire is not None
- assert wire.startswith("🚗 Stationary Traffic")
-
- _, stale_iso = _now_anchor_relative(45 * 60)
- env2 = _tomtom_env(icon_category=6, delay=300,
- start_time=stale_iso,
- tti="11111111-2222-3333-4444-555555555555")
- wire2 = handle_incident(env2, env2["subject"], data={}, now=now)
- assert wire2 is None
-
-
-# ============================================================================
-# v0.5.9 GAMMA -- two-sided freshness gate + state_511 ID skip
-# ============================================================================
-
-
-def test_u_future_scheduled_event_dropped(mem_db, no_photon):
- """itd_511 work_zone envelopes can carry start_epoch in the future
- (scheduled construction). The v0.5.9 REVISED one-sided gate let those
- slip through. The GAMMA fix rejects negative ages too."""
- import datetime as _dt
- now = 2_000_000_000
- # start_epoch 8 hours in the future
- env = _itd_511_env(category_prefix="incident",
- event_type_short="incident",
- event_sub_type="crash",
- external_id="ITD:future:1")
- env["data"]["data"]["start_epoch"] = now + 8 * 3600
- wire = handle_incident(env, env["subject"], data={}, now=now)
- assert wire is None
- n_rows = mem_db.execute(
- "SELECT COUNT(*) AS n FROM traffic_events").fetchone()["n"]
- assert n_rows == 0
- n_log = mem_db.execute(
- "SELECT COUNT(*) AS n FROM event_log WHERE handled=0 "
- "AND source='itd_511'").fetchone()["n"]
- assert n_log == 1
-
-
-def test_v_state_511_id_skipped_at_handler_entrance(mem_db, no_photon):
- """state_511_atis with state_code='ID' is skipped at handler entrance --
- no parse, no traffic_events row, event_log records the skip."""
- env = _state_511_env(layer="Incidents", category_prefix="incident",
- event_sub_type="crash")
- # Override defaults (post-GAMMA helper defaults to WA) to ID for this test.
- env["data"]["data"]["state_code"] = "ID"
- env["data"]["data"]["state"] = "Idaho"
- env["data"]["geo"]["primary_region"] = "US-ID"
- wire = handle_incident(env, env["subject"], data={}, now=1_000_000)
- assert wire is None
- n_rows = mem_db.execute(
- "SELECT COUNT(*) AS n FROM traffic_events").fetchone()["n"]
- assert n_rows == 0
- row = mem_db.execute(
- "SELECT category, handled FROM event_log "
- "WHERE source='state_511_atis' ORDER BY id DESC LIMIT 1"
- ).fetchone()
- assert row is not None
- assert "|skip_id" in row["category"]
- assert row["handled"] == 0
-
-
-def test_v_state_511_non_id_still_processed(mem_db, no_photon):
- """Regression guard: state_511_atis with state_code='WA' (or anything
- that's not ID) keeps going through the handler. After Idaho cutover
- we still need state_511 for neighbor coverage."""
- env = _state_511_env(layer="Incidents", category_prefix="incident",
- event_sub_type="crash", county="Spokane")
- # Override state_code to WA.
- env["data"]["data"]["state_code"] = "WA"
- env["data"]["geo"]["primary_region"] = "US-WA"
- wire = handle_incident(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert wire.startswith("🚨 Crash")
- # traffic_events row written for WA event.
- row = mem_db.execute(
- "SELECT state FROM traffic_events WHERE source='state_511_atis'"
- ).fetchone()
- assert row is not None
- assert row["state"] == "WA"
-
-
-def test_w_itd_511_future_scheduled_dropped_via_start_epoch(mem_db, no_photon):
- """Direct exercise of the itd_511 start_epoch field path with a
- future-scheduled value (the Phase-1 leak source). Confirms the gate
- really uses start_epoch and the new two-sided check catches it."""
- import meshai.central.incident_handler as h
- env = _itd_511_env(category_prefix="incident",
- event_type_short="incident",
- event_sub_type="crash",
- external_id="ITD:future:work")
- env["data"]["data"]["start_epoch"] = 99_000_000_000 # year ~5108 -- definitely future
- se = h._extract_start_time_epoch(env, "itd_511")
- assert se == 99_000_000_000
- now = 2_000_000_000
- wire = handle_incident(env, env["subject"], data={}, now=now)
- assert wire is None
-
-
-# ============================================================================
-# Budget-fit worst case: longest plausible traffic payload must fit 140 chars
-# with critical fields (type, location, road, FULL direction word, lane
-# status, trimmed narrative with intact direction word + "milepost") present.
-# ============================================================================
-
-
-def test_incident_worst_case_fits_140():
- n = {
- "sub_type": "accident",
- "geocoder_city": None,
- "county": "Minidoka",
- "state": "ID",
- "road": "SH-27",
- "direction": "north", # abbreviation/short form -> MUST expand
- "mile_marker": None,
- "lanes_affected": "1 Right lane blocked",
- "comment": (
- "Southbound right lane at milepost 24 and westbound onramp to I-84 "
- "blocked due to a multi-vehicle collision, expect major delays "
- "through the evening commute and seek alternate routes tonight"
- ),
- }
- wire = _incident_render(n)
-
- # (a) fits one mesh packet
- assert len(wire) <= 140, f"{len(wire)} chars:\n{wire!r}"
-
- # (b) critical fields present
- assert wire.startswith("🚨 Crash") # type
- assert "Near Minidoka Co, ID" in wire # location
- assert "SH-27" in wire # road
- assert "Northbound" in wire # FULL direction (expanded)
- assert "1 Right lane blocked" in wire # lane status
- assert "SH-27 Northbound · 1 Right lane blocked" in wire # road·lane line
-
- # (c) direction is NEVER abbreviated anywhere in the wire
- for abbr in (" NB", " N ", "Sbound", "N/B"):
- assert abbr not in wire
-
- # (d) narrative present and word-boundary trimmed (no mid-word chop): the
- # intact direction word "Southbound" and the intact word "milepost" survive.
- assert "Southbound" in wire
- assert "milepost" in wire
- # trimmed from the END -> ends with the ellipsis, not raw text
- assert wire.endswith("…")
diff --git a/work/tests/test_incident_refactor.py b/work/tests/test_incident_refactor.py
index d0d9eda..811a2f0 100644
--- a/work/tests/test_incident_refactor.py
+++ b/work/tests/test_incident_refactor.py
@@ -1,27 +1,37 @@
"""Phase-2 incident/roads refactor tests — TIER-A (byte-identical goldens).
-Test strategy
--------------
-Golden (byte-identical) tests call the old per-source parsers and the legacy
-renderer directly, then compare byte-for-byte against the new
-formatters.incident.format() output. This bypasses the handle_incident
-freshness gate (which drops all captured fixtures as stale) and isolates the
-rendering logic — exactly the same pattern as test_quake_refactor.py.
+The Central `incident_handler` module (`_parse_tomtom_incident()`,
+`_parse_itd_511_incident()`, `_render()`) has been deleted along with the
+rest of the Central NATS consumer path. The TomTom-incidents and ITD-511
+golden-fixture parity groups that called those deleted parsers directly to
+build a "golden" wire and compare it byte-for-byte against
+formatters.incident.format() have been removed in full (46 tests across
+TestTomtomGolden, TestItd511IncidentGolden, and
+TestSchemaConformance::test_incident_canonical_keys_from_parser) — every
+assertion in those tests was generated FROM the deleted parser output, with
+no independent hand-written expected content to fall back to, and no
+native adapter exists that parses the *TomTom Incident Details* or Central
+ITD-511-envelope raw shapes those fixtures capture (env/traffic.py is the
+TomTom traffic-*flow* adapter, a different feed; env/roads511.py parses
+ITD 511's own REST API shape, not the Central envelope fixtures here).
+Original diffs are preserved in git history. This is a real production gap
+flagged for Matt: TomTom road-incident ingestion in particular has no
+native replacement.
+
+Work-zone parity is unaffected — `meshai.central_normalizer` (a top-level,
+non-Central-NATS module; note the name is legacy) and
+`meshai.notifications.renderers.work_zone` were never part of the deleted
+consumer path and remain live.
Groups
------
-1. Tomtom incident golden byte-parity (all 40 traffic/*.json fixtures with
- min_magnitude override=0 so the parser doesn't filter them; fixtures that
- the parser still rejects for other reasons are skipped gracefully).
-2. ITD-511 incident golden byte-parity (traffic/0032.json + the non-work-zone
- fixtures in traffic_last/).
-3. Work-zone golden byte-parity (traffic_last/0002 itd_511, traffic_last/0003
+1. Work-zone golden byte-parity (traffic_last/0002 itd_511, traffic_last/0003
wzdx) — calls normalize() directly, same as the production consumer.
-4. Gate sequence: decide() lifecycle transitions (new → cold-dup →
+2. Gate sequence: decide() lifecycle transitions (new → cold-dup →
suppress-on-update-False → magnitude-up → suppress-no-change).
-5. _anchor.resolve_anchor: DB hit and Photon fallback path.
-6. Schema conformance: canonical data dict has all expected keys.
-7. Cross-source identity: same render fields → same output regardless of
+3. _anchor.resolve_anchor: DB hit and Photon fallback path.
+4. Schema conformance: canonical data dict has all expected keys.
+5. Cross-source identity: same render fields → same output regardless of
source string.
"""
from __future__ import annotations
@@ -45,15 +55,6 @@ _FIXTURE_DIR = pathlib.Path(__file__).parent / "fixtures"
# Using captured_epoch of the traffic_last fixtures (1783206522).
_AT_WZ = 1_783_206_522.0
-# Expected canonical data keys for incident events.
-_INCIDENT_CANONICAL_KEYS = frozenset({
- "external_id", "source", "sub_type", "road", "direction",
- "from_loc", "to_loc", "mile_start", "mile_end", "mile_marker",
- "lanes_affected", "cause", "comment", "impact",
- "county", "state", "lat", "lon", "geocoder_city", "landclass",
- "start_at", "end_at", "magnitude", "delay_seconds", "icon_category",
-})
-
# Expected canonical data keys for work-zone events.
_WZ_CANONICAL_KEYS = frozenset({
"road", "direction", "mile_start", "mile_end", "sub_type", "impact",
@@ -75,42 +76,10 @@ def _load_dir(hazard: str):
return out
-_TRAFFIC_FX = _load_dir("traffic") # 40 files (mostly tomtom, one itd_511)
_TRAFFIC_LAST_FX = _load_dir("traffic_last") # 6 files (mixed adapters)
-# ── Helper: build canonical incident dict from parser output ─────────────────
-
-def _n_to_canonical_incident(n: dict) -> dict:
- """Mirror the cutover-branch canonical extraction in handle_incident."""
- return {
- "external_id": n.get("external_id"),
- "source": n.get("source"),
- "sub_type": n.get("sub_type"),
- "road": n.get("road"),
- "direction": n.get("direction"),
- "from_loc": n.get("from_loc"),
- "to_loc": n.get("to_loc"),
- "mile_start": n.get("mile_start"),
- "mile_end": n.get("mile_end"),
- "mile_marker": n.get("mile_marker"),
- "lanes_affected": n.get("lanes_affected"),
- "cause": n.get("cause"),
- "comment": n.get("comment"),
- "impact": n.get("impact"),
- "county": n.get("county"),
- "state": n.get("state"),
- "lat": n.get("lat"),
- "lon": n.get("lon"),
- "geocoder_city": n.get("geocoder_city"),
- "landclass": n.get("landclass"),
- "start_at": n.get("start_at"),
- "end_at": n.get("end_at"),
- "magnitude": n.get("magnitude"),
- "delay_seconds": n.get("delay_seconds"),
- "icon_category": n.get("icon_category"),
- }
-
+# ── Helper: build canonical work-zone dict from normalize() output ───────────
def _n_to_canonical_workzone(n: dict) -> dict:
"""Build canonical work-zone data dict from a normalize() result.
@@ -153,20 +122,6 @@ def _make_event(category: str, data: dict):
# ── pytest fixtures: adapter_config overrides ────────────────────────────────
-@pytest.fixture()
-def tomtom_min_mag_zero():
- """Set tomtom_incidents.min_magnitude=-999 via runtime override for one test.
-
- The parser uses `int(adapter_config.tomtom_incidents.min_magnitude or 4)`
- which treats 0 as falsy and falls back to 4. Using -999 (truthy) forces
- all magnitudes to pass the `magnitude < min_mag` filter.
- """
- from meshai.adapter_config._accessor import set_runtime_override, _overrides
- set_runtime_override("tomtom_incidents", "min_magnitude", -999)
- yield
- _overrides.pop(("tomtom_incidents", "min_magnitude"), None)
-
-
@pytest.fixture()
def broadcast_on_update_on():
"""Enable incident.broadcast_on_update for gate-sequence tests."""
@@ -176,143 +131,13 @@ def broadcast_on_update_on():
_overrides.pop(("incident", "broadcast_on_update"), None)
-# ── Helpers: determine adapter type and call the right parser ────────────────
+# ── Helper: determine adapter type ────────────────────────────────────────────
def _adapter_for(fx: dict) -> str:
return (fx["envelope"]["data"] or {}).get("adapter") or ""
-def _category_for(fx: dict) -> str:
- return (fx["envelope"]["data"] or {}).get("category") or ""
-
-
-# ── 1. Tomtom incident golden byte-parity ────────────────────────────────────
-
-class TestTomtomGolden:
- """All traffic/*.json tomtom fixtures rendered via old parser + _render()
- must produce byte-identical output from the new formatters.incident.format().
-
- min_magnitude is overridden to 0 so all fixtures (including mag=3 ones)
- exercise the renderer. The one itd_511 fixture (0032) is skipped here.
- """
-
- @pytest.mark.parametrize("name,fx", _TRAFFIC_FX)
- def test_golden_parity(self, name, fx, tomtom_min_mag_zero):
- from meshai.central.incident_handler import _parse_tomtom_incident, _render
- from meshai.notifications.formatters.incident import format as fmt
- from meshai.notifications.formatters._budget import budget_for
-
- adapter = _adapter_for(fx)
- if adapter != "tomtom_incidents":
- pytest.skip(f"{name}: adapter={adapter!r}, not tomtom")
-
- envelope = fx["envelope"]
- now = int(fx.get("captured_epoch", time.time()))
-
- n = _parse_tomtom_incident(envelope, now)
- if n is None:
- pytest.skip(f"{name}: parser returned None (filtered)")
-
- golden = _render(n)
- canonical = _n_to_canonical_incident(n)
-
- # Determine event category from category_kind
- kind_map = {
- "incident": "road_incident",
- "closure": "road_closure",
- "special_event": "road_incident",
- "work_zone": "work_zone",
- }
- cat = kind_map.get(n.get("category_kind", "incident"), "road_incident")
- event = _make_event(cat, canonical)
-
- budget = budget_for("incident")
- new_out = fmt(event, now=float(now), budget=budget)
-
- assert_byte_identical(new_out, golden)
-
-
-# ── 2. ITD-511 incident golden byte-parity ───────────────────────────────────
-
-class TestItd511IncidentGolden:
- """itd_511 incident/closure/special_event fixtures rendered via
- _parse_itd_511_incident + _render() must be byte-identical from formatter.
-
- Covers traffic/0032.json (itd_511) + non-work-zone traffic_last fixtures.
- """
-
- def _run_single(self, name: str, fx: dict):
- from meshai.central.incident_handler import _parse_itd_511_incident, _render
- from meshai.notifications.formatters.incident import format as fmt
- from meshai.notifications.formatters._budget import budget_for
-
- envelope = fx["envelope"]
- category_raw = _category_for(fx)
- now = int(fx.get("captured_epoch", time.time()))
-
- n = _parse_itd_511_incident(envelope, category_raw, now)
- if n is None:
- pytest.skip(f"{name}: parser returned None (filtered/work_zone)")
-
- golden = _render(n)
- canonical = _n_to_canonical_incident(n)
- kind_map = {
- "incident": "road_incident",
- "closure": "road_closure",
- "special_event": "road_incident",
- "work_zone": "work_zone",
- }
- cat = kind_map.get(n.get("category_kind", "incident"), "road_incident")
- event = _make_event(cat, canonical)
-
- budget = budget_for("incident")
- new_out = fmt(event, now=float(now), budget=budget)
- assert_byte_identical(new_out, golden)
-
- @pytest.mark.parametrize("name,fx", [
- (name, fx) for name, fx in _TRAFFIC_FX if (
- (fx["envelope"].get("data") or {}).get("adapter") == "itd_511"
- )
- ])
- def test_traffic_itd511(self, name, fx):
- self._run_single(name, fx)
-
- @pytest.mark.parametrize("name,fx", [
- (name, fx) for name, fx in _TRAFFIC_LAST_FX if (
- (fx["envelope"].get("data") or {}).get("adapter") == "itd_511"
- and not ((fx["envelope"].get("data") or {}).get("category") or "").startswith("work_zone.")
- )
- ])
- def test_traffic_last_itd511(self, name, fx):
- self._run_single(name, fx)
-
- @pytest.mark.parametrize("name,fx", [
- (name, fx) for name, fx in _TRAFFIC_LAST_FX if (
- (fx["envelope"].get("data") or {}).get("adapter") == "tomtom_incidents"
- )
- ])
- def test_traffic_last_tomtom(self, name, fx, tomtom_min_mag_zero):
- """traffic_last tomtom fixtures go through this group (min_mag override on)."""
- from meshai.central.incident_handler import _parse_tomtom_incident, _render
- from meshai.notifications.formatters.incident import format as fmt
- from meshai.notifications.formatters._budget import budget_for
-
- envelope = fx["envelope"]
- now = int(fx.get("captured_epoch", time.time()))
-
- n = _parse_tomtom_incident(envelope, now)
- if n is None:
- pytest.skip(f"{name}: parser returned None")
-
- golden = _render(n)
- canonical = _n_to_canonical_incident(n)
- event = _make_event("road_incident", canonical)
- budget = budget_for("incident")
- new_out = fmt(event, now=float(now), budget=budget)
- assert_byte_identical(new_out, golden)
-
-
-# ── 3. Work-zone golden byte-parity ─────────────────────────────────────────
+# ── 1. Work-zone golden byte-parity ─────────────────────────────────────────
class TestWorkZoneGolden:
"""traffic_last/0002 (itd_511 work_zone) and traffic_last/0003 (wzdx)
@@ -363,7 +188,7 @@ class TestWorkZoneGolden:
self._run_wz("0003.json", "wzdx")
-# ── 4. Gate sequence ──────────────────────────────────────────────────────────
+# ── 2. Gate sequence ──────────────────────────────────────────────────────────
class TestGateSequence:
"""decide() lifecycle transitions:
@@ -473,7 +298,7 @@ class TestGateSequence:
assert result.commit is None
-# ── 5. _anchor.resolve_anchor ────────────────────────────────────────────────
+# ── 3. _anchor.resolve_anchor ────────────────────────────────────────────────
class TestAnchorResolve:
"""resolve_anchor() returns a result from the town_anchors DB when a row
@@ -553,29 +378,12 @@ class TestAnchorResolve:
assert resolve_anchor(43.6, None, max_mi=50.0) is None
-# ── 6. Schema conformance ────────────────────────────────────────────────────
+# ── 4. Schema conformance ────────────────────────────────────────────────────
class TestSchemaConformance:
"""Canonical data dicts produced by to_event() and the bridge must
contain all expected keys."""
- def test_incident_canonical_keys_from_parser(self):
- """All canonical keys present in extraction from a tomtom parse."""
- from meshai.central.incident_handler import _parse_tomtom_incident
- from meshai.adapter_config._accessor import set_runtime_override, _overrides
-
- set_runtime_override("tomtom_incidents", "min_magnitude", -999)
- try:
- # Use fixture 0002 (mag=4, the minimal fixture that passes)
- with open(_FIXTURE_DIR / "traffic" / "0002.json", encoding="utf-8") as f:
- fx = json.load(f)
- n = _parse_tomtom_incident(fx["envelope"], int(fx.get("captured_epoch", 0)))
- assert n is not None
- canonical = _n_to_canonical_incident(n)
- assert _INCIDENT_CANONICAL_KEYS == set(canonical.keys())
- finally:
- _overrides.pop(("tomtom_incidents", "min_magnitude"), None)
-
def test_workzone_canonical_keys_from_normalize(self):
"""All work-zone canonical keys present in extraction from normalize()."""
from meshai.central_normalizer import normalize
@@ -621,7 +429,7 @@ class TestSchemaConformance:
assert key in d, f"Missing key {key!r} in Roads511Adapter canonical data"
-# ── 7. Cross-source identity ─────────────────────────────────────────────────
+# ── 5. Cross-source identity ─────────────────────────────────────────────────
class TestCrossSourceIdentity:
"""Same render-relevant canonical fields → same formatter output regardless
diff --git a/work/tests/test_nwis_handler.py b/work/tests/test_nwis_handler.py
deleted file mode 100644
index 25281dd..0000000
--- a/work/tests/test_nwis_handler.py
+++ /dev/null
@@ -1,277 +0,0 @@
-"""Tests for v0.5.12 usgs_nwis handler."""
-import pytest
-
-# v0.6-4: IDAHO_CURATED_SITES dict moved to gauge_sites SQLite table;
-# import the seed data from the curation module as a back-compat alias.
-from meshai.persistence.curation import _GAUGE_SITES_SEED as IDAHO_CURATED_SITES
-from meshai.central.nwis_handler import handle_nwis
-from meshai.persistence import close_thread_connection, init_db
-from meshai.persistence import db as persistence_db
-
-
-@pytest.fixture
-def mem_db(monkeypatch, tmp_path):
- db_path = str(tmp_path / "nwis-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)
-
-
-def _nwis_env(*, site_id="USGS-13186000",
- parameter_code="00065", value=13.0,
- unit="ft", time_iso="2026-06-05T15:00:00Z",
- lat=43.612, lon=-111.654,
- envelope_id=None):
- envelope_id = envelope_id or f"nwis_{site_id}_{time_iso}"
- return {
- "id": envelope_id, "subject": f"central.hydro.{parameter_code}.usgs.{site_id}.us.id",
- "data": {
- "id": envelope_id, "adapter": "nwis",
- "category": f"hydro.{parameter_code}", "severity": 0,
- "geo": {"centroid": [lon, lat], "primary_region": "US-ID"},
- "data": {
- "id": envelope_id,
- "monitoring_location_id": site_id,
- "parameter_code": parameter_code,
- "time": time_iso,
- "value": value,
- "unit_of_measure": unit,
- "latitude": lat, "longitude": lon,
- "_enriched": {"geocoder": {
- "name": IDAHO_CURATED_SITES.get(site_id, {}).get(
- "gauge_name", "?"),
- }},
- },
- },
- }
-
-
-def _commit(data, t):
- data["_on_broadcast_committed"](float(t))
-
-
-# ---- (a) curated site at action stage triggers broadcast -----------------
-
-
-def test_a_curated_site_action_stage_triggers(mem_db):
- # Snake River at Heise: action=12.0ft, broadcast at 12.5ft.
- env = _nwis_env(site_id="USGS-13186000", parameter_code="00065",
- value=12.5)
- data = {}
- wire = handle_nwis(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- assert wire.startswith("🌊 New:")
- assert "Snake River at Heise" in wire
- assert "action stage 12.5 ft" in wire
-
-
-# ---- (b) non-curated site no broadcast + event_log handled=0 ------------
-
-
-def test_b_non_curated_site_dropped(mem_db):
- env = _nwis_env(site_id="USGS-99999999", value=99.0)
- data = {}
- wire = handle_nwis(env, env["subject"], data=data, now=1_000_000)
- assert wire is None
- n_rows = mem_db.execute(
- "SELECT COUNT(*) AS n FROM gauge_readings").fetchone()["n"]
- assert n_rows == 0
- n_log = mem_db.execute(
- "SELECT COUNT(*) AS n FROM event_log WHERE source='nwis' AND handled=0"
- ).fetchone()["n"]
- assert n_log == 1
-
-
-# ---- (c) curated site at normal stage no broadcast ----------------------
-
-
-def test_c_curated_site_normal_stage_no_broadcast(mem_db):
- # Heise normal is below 12.0ft.
- env = _nwis_env(site_id="USGS-13186000", value=8.0)
- data = {}
- wire = handle_nwis(env, env["subject"], data=data, now=1_000_000)
- assert wire is None
- # The reading WAS persisted (time-series).
- row = mem_db.execute(
- "SELECT threshold_state FROM gauge_readings WHERE site_id=?",
- ("USGS-13186000",)).fetchone()
- assert row["threshold_state"] == "normal"
-
-
-# ---- (d) upward threshold crossing (normal -> action) triggers ---------
-
-
-def test_d_upward_crossing_normal_to_action_triggers(mem_db):
- # First reading at normal.
- env1 = _nwis_env(site_id="USGS-13186000", value=8.0,
- time_iso="2026-06-05T10:00:00Z")
- handle_nwis(env1, env1["subject"], data={}, now=1_000_000)
- # Now rises to action.
- env2 = _nwis_env(site_id="USGS-13186000", value=12.5,
- time_iso="2026-06-05T10:15:00Z",
- envelope_id="env_2")
- data = {}
- wire = handle_nwis(env2, env2["subject"], data=data, now=1_000_900)
- assert wire is not None
- assert "action stage 12.5 ft" in wire
-
-
-# ---- (e) downward crossing (action -> normal) does NOT broadcast -------
-
-
-def test_e_downward_crossing_does_not_broadcast(mem_db):
- env_high = _nwis_env(site_id="USGS-13186000", value=12.5,
- time_iso="2026-06-05T10:00:00Z")
- handle_nwis(env_high, env_high["subject"], data={}, now=1_000_000)
- env_low = _nwis_env(site_id="USGS-13186000", value=8.0,
- time_iso="2026-06-05T11:00:00Z",
- envelope_id="env_drop")
- wire = handle_nwis(env_low, env_low["subject"], data={}, now=1_003_600)
- assert wire is None
-
-
-def test_e_same_threshold_no_re_broadcast(mem_db):
- """Repeated readings at the same threshold (action -> action -> action)
- must NOT re-broadcast every 15-min poll."""
- env = _nwis_env(site_id="USGS-13186000", value=12.5,
- time_iso="2026-06-05T10:00:00Z")
- wire1 = handle_nwis(env, env["subject"], data={}, now=1_000_000)
- assert wire1 is not None
-
- env2 = _nwis_env(site_id="USGS-13186000", value=12.8,
- time_iso="2026-06-05T10:15:00Z",
- envelope_id="env_p2")
- wire2 = handle_nwis(env2, env2["subject"], data={}, now=1_000_900)
- assert wire2 is None # still in action band
-
-
-# ---- (f) flow_cfs included for 00060, dropped for 00065-only -----------
-
-
-def test_f_flow_cfs_segment_from_companion_discharge(mem_db):
- # First seed a stage reading at action.
- env_stage = _nwis_env(site_id="USGS-13186000",
- parameter_code="00065", value=12.5,
- time_iso="2026-06-05T10:00:00Z")
- wire1 = handle_nwis(env_stage, env_stage["subject"], data={}, now=1_000_000)
- assert wire1 is not None
- assert "flow" not in wire1 # no companion discharge yet
-
- # Now a discharge reading arrives -- the handler should pick up the
- # prior stage_ft for threshold context AND emit flow if upward crossing.
- # In this case the stage didn't change, so no broadcast.
- env_flow = _nwis_env(site_id="USGS-13186000",
- parameter_code="00060", value=8400,
- unit="ft^3/s",
- time_iso="2026-06-05T10:01:00Z",
- envelope_id="env_q")
- wire2 = handle_nwis(env_flow, env_flow["subject"], data={}, now=1_000_060)
- assert wire2 is None # same threshold; no re-broadcast
-
-
-# ---- (g) site missing coords drops @ tail ------------------------------
-
-
-def test_g_missing_coords_drops_at_tail(mem_db):
- # Build a Heise envelope but blank out the latitude in inner.data so
- # the handler must fall back to the curated coords (which DO exist).
- env = _nwis_env(site_id="USGS-13186000", value=12.5)
- env["data"]["data"]["latitude"] = None
- env["data"]["data"]["longitude"] = None
- wire = handle_nwis(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- # Curated coords kick in -> @ segment still present.
- assert "@ 43.612,-111.654" in wire
-
-
-# ---- (h) IDAHO_CURATED_SITES has all 9 starter sites populated ---------
-
-
-def test_h_curated_sites_count_and_required_fields():
- assert len(IDAHO_CURATED_SITES) == 9
- required_keys = {"gauge_name", "lat", "lon", "action_ft", "flood_minor_ft"}
- for site_id, meta in IDAHO_CURATED_SITES.items():
- assert site_id.startswith("USGS-"), site_id
- missing = required_keys - set(meta.keys())
- assert not missing, f"{site_id} missing {missing}"
- assert isinstance(meta["action_ft"], (int, float))
- assert isinstance(meta["flood_minor_ft"], (int, float))
-
-
-def test_h_curated_sites_listed_starter_set():
- """Spot-check the 9 starter sites are exactly what spec listed."""
- expected = {
- "USGS-13139510", "USGS-13186000", "USGS-13037500",
- "USGS-13135500", "USGS-13205000", "USGS-13247500",
- "USGS-13057000", "USGS-13162225", "USGS-13083000",
- }
- assert set(IDAHO_CURATED_SITES.keys()) == expected
-
-
-# ---- commit callback flips event_log.handled = 1 -----------------------
-
-
-def test_commit_callback_flips_event_log(mem_db):
- env = _nwis_env(site_id="USGS-13186000", value=12.5)
- data = {}
- wire = handle_nwis(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- pre = mem_db.execute(
- "SELECT handled FROM event_log WHERE source='nwis' ORDER BY id DESC LIMIT 1"
- ).fetchone()
- assert pre["handled"] == 0
- _commit(data, 1_000_001)
- post = mem_db.execute(
- "SELECT handled FROM event_log WHERE source='nwis' ORDER BY id DESC LIMIT 1"
- ).fetchone()
- assert post["handled"] == 1
-
-
-# ---- threshold escalation triggers a new broadcast --------------------
-
-
-def test_action_to_flood_minor_triggers_re_broadcast(mem_db):
- """Reading rises action -> flood_minor: this is an upward crossing,
- re-broadcast with the higher threshold label."""
- env1 = _nwis_env(site_id="USGS-13186000", value=12.5,
- time_iso="2026-06-05T10:00:00Z")
- wire1 = handle_nwis(env1, env1["subject"], data={}, now=1_000_000)
- assert wire1 is not None
- assert "action stage" in wire1
-
- env2 = _nwis_env(site_id="USGS-13186000", value=14.5,
- time_iso="2026-06-05T11:00:00Z",
- envelope_id="env_fm")
- wire2 = handle_nwis(env2, env2["subject"], data={}, now=1_003_600)
- assert wire2 is not None
- assert "minor flooding" in wire2
-
-
-# ---- precipitation events skipped (parameter_code=00045) --------------
-
-
-def test_precip_parameter_skipped(mem_db):
- env = _nwis_env(site_id="USGS-13186000", parameter_code="00045",
- value=0.5, unit="in")
- wire = handle_nwis(env, env["subject"], data={}, now=1_000_000)
- assert wire is None
- # No gauge_readings row written for precip.
- n_rows = mem_db.execute(
- "SELECT COUNT(*) AS n FROM gauge_readings").fetchone()["n"]
- assert n_rows == 0
-
-
-# ---- site_id normalization -----------------------------------------------
-
-
-def test_site_id_normalization_accepts_bare_id(mem_db):
- """'13186000' without USGS- prefix should still resolve to Heise."""
- env = _nwis_env(site_id="13186000", value=12.5)
- env["data"]["data"]["monitoring_location_id"] = "13186000"
- wire = handle_nwis(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert "Snake River at Heise" in wire
diff --git a/work/tests/test_nws_dedup_relaxation.py b/work/tests/test_nws_dedup_relaxation.py
deleted file mode 100644
index b60da18..0000000
--- a/work/tests/test_nws_dedup_relaxation.py
+++ /dev/null
@@ -1,140 +0,0 @@
-"""v0.6-phase3 NWS dedup-window relaxation tests.
-
-The same CAP id is now re-broadcast (with Active: prefix) when more than
-`nws.duplicate_allowed_after_seconds` (default 10800 = 3h) have elapsed
-since the last broadcast.
-"""
-from __future__ import annotations
-
-import time
-
-import pytest
-
-from meshai.central.nws_handler import handle_nws
-from meshai.persistence import get_db
-
-
-def _env(*, cap_id="urn:oid:dedup.001", event="Severe Thunderstorm Warning",
- severity="Severe", area="Ada County", county="Ada", state="ID",
- expires="2026-06-05T03:00:00Z", lat=43.6, lon=-116.2,
- category="wx.alert.severe_thunderstorm_warning"):
- return {
- "id": cap_id, "subject": "central.wx.alert.us.id",
- "data": {
- "id": cap_id, "adapter": "nws", "category": category,
- "severity": 2,
- "geo": {"centroid": [lon, lat], "primary_region": "US-ID"},
- "data": {
- "id": cap_id, "event": event, "severity": severity,
- "areaDesc": area, "msgType": "Alert",
- "headline": f"{event} for {area}",
- "description": "X", "expires": expires,
- "_enriched": {"geocoder": {"city": None,
- "county": county, "state": state}},
- },
- },
- }
-
-
-def _commit(data, ts):
- cb = data["_on_broadcast_committed"]
- cb(float(ts))
-
-
-def test_first_broadcast_no_active_prefix():
- """A first sighting renders without Active: prefix."""
- env = _env()
- data = {}
- wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- assert "Active:" not in wire
-
-
-def test_repeat_within_3h_suppressed():
- env = _env(cap_id="urn:oid:rep1")
- data = {}
- # First broadcast at t=0.
- wire1 = handle_nws(env, env["subject"], data=data, now=0)
- assert wire1 is not None
- _commit(data, 0)
-
- # Same CAP id again 2h later -- inside 3h window -> suppressed.
- env2 = _env(cap_id="urn:oid:rep1")
- data2 = {}
- wire2 = handle_nws(env2, env2["subject"], data=data2, now=2 * 3600)
- assert wire2 is None
-
-
-def test_repeat_after_3h_allowed_with_active_prefix():
- env = _env(cap_id="urn:oid:rep2")
- data = {}
- wire1 = handle_nws(env, env["subject"], data=data, now=0)
- assert wire1 is not None
- _commit(data, 0)
-
- # Same CAP id 4h later -> allowed, Active: prefix.
- env2 = _env(cap_id="urn:oid:rep2")
- data2 = {}
- wire2 = handle_nws(env2, env2["subject"], data=data2, now=4 * 3600)
- assert wire2 is not None
- assert "Active:" in wire2
-
-
-def test_dedup_window_respects_config_override():
- """Changing nws.duplicate_allowed_after_seconds via adapter_config takes effect."""
- from meshai.adapter_config import invalidate_cache
- conn = get_db()
- conn.execute(
- "UPDATE adapter_config SET value_json='3600' "
- "WHERE adapter='nws' AND key='duplicate_allowed_after_seconds'"
- )
- invalidate_cache()
-
- env = _env(cap_id="urn:oid:tunable")
- data = {}
- handle_nws(env, env["subject"], data=data, now=0)
- _commit(data, 0)
-
- # 90 min later -> still suppressed (under new 1h window).
- # Wait, 90 min > 60 min so it would BE allowed. Use 30 min instead.
- env2 = _env(cap_id="urn:oid:tunable")
- data2 = {}
- wire = handle_nws(env2, env2["subject"], data=data2, now=30 * 60)
- assert wire is None # 30 min < 60 min window
-
- # 2h later -> allowed (over 1h window now).
- env3 = _env(cap_id="urn:oid:tunable")
- data3 = {}
- wire3 = handle_nws(env3, env3["subject"], data=data3, now=2 * 3600)
- assert wire3 is not None
- assert "Active:" in wire3
-
-
-def test_handler_stamps_first_broadcast_at():
- """The commit callback writes first_broadcast_at via COALESCE -- only on
- the first commit, never overwriting it."""
- env = _env(cap_id="urn:oid:stamp")
- data = {}
- handle_nws(env, env["subject"], data=data, now=0)
- _commit(data, 100.0)
-
- conn = get_db()
- row = conn.execute(
- "SELECT first_broadcast_at, last_broadcast_at FROM nws_alerts "
- "WHERE event_id='urn:oid:stamp'"
- ).fetchone()
- assert row["first_broadcast_at"] == 100.0
- assert row["last_broadcast_at"] == 100.0
-
- # Second broadcast 4h later -> last_broadcast_at updates, first_broadcast_at preserved.
- env2 = _env(cap_id="urn:oid:stamp")
- data2 = {}
- wire2 = handle_nws(env2, env2["subject"], data=data2, now=4 * 3600)
- assert wire2 is not None
- _commit(data2, 4 * 3600.0)
- row2 = conn.execute(
- "SELECT first_broadcast_at, last_broadcast_at FROM nws_alerts "
- "WHERE event_id='urn:oid:stamp'"
- ).fetchone()
- assert row2["first_broadcast_at"] == 100.0 # unchanged
- assert row2["last_broadcast_at"] == 4 * 3600.0
diff --git a/work/tests/test_nws_handler.py b/work/tests/test_nws_handler.py
deleted file mode 100644
index 26ce80d..0000000
--- a/work/tests/test_nws_handler.py
+++ /dev/null
@@ -1,543 +0,0 @@
-"""Tests for v0.5.10 NWS handler."""
-import pytest
-
-from meshai.central.nws_handler import handle_nws, _emoji_for_event, _render
-from meshai.persistence import close_thread_connection, init_db
-from meshai.persistence import db as persistence_db
-
-
-@pytest.fixture
-def mem_db(monkeypatch, tmp_path):
- db_path = str(tmp_path / "nws-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)
-
-
-def _nws_env(*, cap_id="urn:oid:test.001",
- event="Severe Thunderstorm Warning",
- severity_str="Severe",
- area_desc="Twin Falls County",
- county="Twin Falls", state="ID",
- expires="2026-06-05T03:00:00Z",
- msg_type=None,
- lat=42.500, lon=-114.460,
- geocoder_city=None,
- category="wx.alert.severe_thunderstorm_warning"):
- return {
- "id": cap_id, "subject": "central.wx.alert.us.id",
- "data": {
- "id": cap_id, "adapter": "nws", "category": category,
- "severity": 2,
- "geo": {"centroid": [lon, lat], "primary_region": "US-ID"},
- "data": {
- "id": cap_id, "@type": "wx:Alert",
- "event": event, "severity": severity_str,
- "areaDesc": area_desc, "msgType": msg_type or "Alert",
- "headline": f"{event} for {area_desc}",
- "description": "Storm details.",
- "expires": expires,
- "_enriched": {"geocoder": {"city": geocoder_city,
- "county": county, "state": state}},
- },
- },
- }
-
-
-def _commit(data, t):
- data["_on_broadcast_committed"](float(t))
-
-
-# ---- severity gate ----
-
-
-def test_severe_thunderstorm_warning_broadcasts(mem_db):
- env = _nws_env(severity_str="Severe", event="Severe Thunderstorm Warning")
- data = {}
- wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- assert wire.startswith("🌩️")
- assert "Severe Thunderstorm Warning" in wire
-
-
-def test_extreme_emergency_broadcasts(mem_db):
- env = _nws_env(severity_str="Extreme", event="Tornado Warning",
- category="wx.alert.tornado_warning")
- data = {}
- wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- assert wire.startswith("🌪️")
-
-
-def test_special_weather_statement_passes_through(mem_db):
- # GATE A removed: Minor/SWS is no longer dropped on CAP severity alone.
- env = _nws_env(severity_str="Minor", event="Special Weather Statement",
- category="wx.alert.special_weather_statement")
- data = {}
- wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None, "SWS should now pass through (GATE A removed)"
- assert "Special Weather Statement" in wire
- # Row inserted in nws_alerts (not a warning category → no override).
- n_rows = mem_db.execute("SELECT COUNT(*) AS n FROM nws_alerts").fetchone()["n"]
- assert n_rows == 1
- # _severity_override should NOT be set for a non-warning category.
- assert data.get("_severity_override") is None
-
-
-def test_watch_severity_moderate_passes_through(mem_db):
- # GATE A removed: Moderate watches now pass through; dispatcher threshold governs.
- env = _nws_env(severity_str="Moderate", event="Severe Thunderstorm Watch",
- category="wx.alert.severe_thunderstorm_watch")
- data = {}
- wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None, "Moderate watch should now pass through (GATE A removed)"
- assert "Severe Thunderstorm Watch" in wire
- # Watches end in _watch, not _warning — no severity override.
- assert data.get("_severity_override") is None
-
-
-# ---- emoji map ----
-
-
-@pytest.mark.parametrize("event_type, expected_emoji", [
- ("Severe Thunderstorm Warning", "🌩️"),
- ("Tornado Warning", "🌪️"),
- ("Flash Flood Warning", "🌊"),
- ("Flood Warning", "🌊"),
- ("Winter Storm Warning", "❄️"),
- ("Blizzard Warning", "❄️"),
- ("Excessive Heat Warning", "🌡️"),
- ("High Wind Warning", "🌬️"),
- ("Red Flag Warning", "🔥"),
- ("Fire Weather Watch", "🔥"),
- ("Air Quality Alert", "😷"),
- ("Freeze Warning", "🥶"),
- ("Coastal Flood Warning", "🌊"),
- ("(some other warning)", "⚠️"),
-])
-def test_emoji_map(event_type, expected_emoji):
- assert _emoji_for_event(event_type) == expected_emoji
-
-
-# ---- tombstone ----
-
-
-def test_cancel_msgType_tombstone_skipped(mem_db):
- env = _nws_env(severity_str="Severe", event="Severe Thunderstorm Warning",
- msg_type="Cancel")
- data = {}
- wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
- assert wire is None
- n_log = mem_db.execute(
- "SELECT COUNT(*) AS n FROM event_log WHERE source='nws' AND handled=0"
- ).fetchone()["n"]
- assert n_log == 1
-
-
-def test_expire_msgType_tombstone_skipped(mem_db):
- env = _nws_env(severity_str="Severe", event="Tornado Warning",
- msg_type="Expire")
- wire = handle_nws(env, env["subject"], data={}, now=1_000_000)
- assert wire is None
-
-
-# ---- per-CAP-id dedup ----
-
-
-def test_per_cap_id_dedup_no_reissue(mem_db):
- env = _nws_env(severity_str="Severe")
- data1 = {}
- wire1 = handle_nws(env, env["subject"], data=data1, now=1_000_000)
- assert wire1 is not None
- _commit(data1, 1_000_001)
-
- # Same CAP id republishes (e.g. headline update). Should NOT re-broadcast.
- data2 = {}
- wire2 = handle_nws(env, env["subject"], data=data2, now=1_000_300)
- assert wire2 is None
-
-
-# ---- area_desc fallback ----
-
-
-def test_area_desc_used_when_geocoder_city_missing(mem_db):
- env = _nws_env(severity_str="Severe", area_desc="Twin Falls County",
- geocoder_city=None)
- wire = handle_nws(env, env["subject"], data={}, now=1_000_000)
- assert "Twin Falls" in wire
-
-
-def test_geocoder_city_preferred_over_area_desc(mem_db):
- env = _nws_env(severity_str="Severe", area_desc="Twin Falls County",
- geocoder_city="Twin Falls")
- wire = handle_nws(env, env["subject"], data={}, now=1_000_000)
- assert "Twin Falls" in wire # either source serves the same anchor
-
-
-# ---- commit callback ----
-
-
-def test_commit_callback_updates_last_broadcast(mem_db):
- env = _nws_env(severity_str="Severe")
- data = {}
- handle_nws(env, env["subject"], data=data, now=1_000_000)
- fr_pre = mem_db.execute(
- "SELECT last_broadcast_at FROM nws_alerts").fetchone()
- assert fr_pre["last_broadcast_at"] is None
- _commit(data, 1_000_001)
- fr_post = mem_db.execute(
- "SELECT last_broadcast_at FROM nws_alerts").fetchone()
- assert fr_post["last_broadcast_at"] == 1_000_001
- # event_log row flipped to handled=1.
- el = mem_db.execute(
- "SELECT handled FROM event_log WHERE source='nws' ORDER BY id DESC LIMIT 1"
- ).fetchone()
- assert el["handled"] == 1
-
-
-def test_wire_includes_event_and_headline(mem_db):
- env = _nws_env(severity_str="Severe", lat=42.500, lon=-114.460)
- wire = handle_nws(env, env["subject"], data={}, now=1_000_000)
- assert "Severe Thunderstorm Warning" in wire
- assert "Twin Falls County" in wire
-
-# ---- warning → immediate promotion (Step 2) ----
-
-
-def test_warning_category_sets_severity_override_immediate(mem_db):
- """A *_warning category sets data[_severity_override]='immediate'."""
- env = _nws_env(severity_str="Severe", event="Severe Thunderstorm Warning",
- category="wx.alert.severe_thunderstorm_warning")
- data = {}
- wire = handle_nws(env, env["subject"], data=data, now=1_000_000)
- assert wire is not None
- assert data.get("_severity_override") == "immediate"
-
-
-def test_tornado_warning_dotted_category_sets_severity_override(mem_db):
- """A category ending in .warning also sets _severity_override='immediate'."""
- env = _nws_env(severity_str="Extreme", event="Tornado Warning",
- category="wx.alert.tornado_warning")
- # Override the data.data.severity to use dotted-style category check
- env["data"]["category"] = "wx.alert.tornado.warning"
- env["data"]["data"]["severity"] = "Extreme"
- data = {}
- wire = handle_nws(env, env["subject"], data=data, now=2_000_000)
- assert wire is not None
- assert data.get("_severity_override") == "immediate"
-
-
-def test_non_warning_category_no_severity_override(mem_db):
- """A non-warning category (watch, advisory, statement) leaves no override."""
- env = _nws_env(severity_str="Severe", event="Severe Thunderstorm Watch",
- category="wx.alert.severe_thunderstorm_watch")
- data = {}
- wire = handle_nws(env, env["subject"], data=data, now=3_000_000)
- assert wire is not None
- assert "_severity_override" not in data
-
-
-# ---- packet-budget enforcement ----
-
-
-def test_svr_long_locations_path_sampled(mem_db):
- """SVR with a long town list: render must fit in 200 chars, and the town
- list must be represented as a PATH SAMPLE (first -> middle -> last) rather
- than a tail-drop. The old bug dropped the final town ('Shoshone')."""
- # Long list; first town "Buhl", last town "and Shoshone" (exercises the
- # leading-"and " strip on the tail element).
- 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}"
- )
- d = {
- "eventCode": {"SAME": ["SVR"]},
- "certainty": "Observed",
- "parameters": {
- "maxWindGust": ["60 MPH"],
- "maxHailSize": ["1.00"],
- # 254 DEG 35 KT -> "Moving W 40 mph"
- "eventMotionDescription": ["2200000T254DEG...35KT 42.5,-114.5"],
- },
- "description": description,
- }
- rendered = _render(
- event_type="Severe Thunderstorm Warning",
- area_desc="Twin Falls County",
- geocoder_city=None,
- county="Twin Falls",
- state="ID",
- expires_epoch=1_751_400_000,
- lat=42.5,
- lon=-114.46,
- now=1_751_400_000,
- d=d,
- )
-
- # (a) fits in one mesh packet (budget is now the 140-char LoRa max)
- assert len(rendered) <= 140, (
- f"rendered is {len(rendered)} chars (expected <= 140):\n{rendered!r}"
- )
-
- # (b) all data-point categories present, hazard wording TIGHTENED
- assert "Severe Thunderstorm Warning" in rendered, "event type missing"
- assert "Until" in rendered, "expiry time segment missing"
- assert "Twin Falls County" in rendered, "area missing"
- assert "60mph winds" in rendered, "wind hazard not tightened to '60mph winds'"
- assert '1" hail' in rendered, "hail hazard not rendered as numeric inches"
- assert "radar" in rendered, "certainty not collapsed to 'radar'"
- assert "Moving" in rendered, "motion segment missing"
-
- # (c) path-sampling applied (arrow) with the soonest-impact town retained.
- # At the 140 budget the farthest-along town may be trimmed by the final
- # backstop; the hard cap wins over endpoint preservation.
- assert "→" in rendered, "no arrow -> not path-sampled"
- assert "Buhl" in rendered, "first (soonest-impact) town missing"
-
-
-def test_svr_short_locations_shown_in_full(mem_db):
- """Short town list that fits in one packet: show the FULL comma-joined
- list, and never emit the path-sample arrow."""
- short_locations = "Buhl, Eden, and Hazelton"
- description = (
- "HAZARD...Damaging winds to 60 mph and quarter-size hail.\n\n"
- f"Locations impacted include...{short_locations}"
- )
- d = {
- "eventCode": {"SAME": ["SVR"]},
- "certainty": "Observed",
- "parameters": {
- "maxWindGust": ["60 MPH"],
- "maxHailSize": ["1.00"],
- "eventMotionDescription": ["2200000T254DEG...35KT 42.5,-114.5"],
- },
- "description": description,
- }
- rendered = _render(
- event_type="Severe Thunderstorm Warning",
- area_desc="Twin Falls County",
- geocoder_city=None,
- county="Twin Falls",
- state="ID",
- expires_epoch=1_751_400_000,
- lat=42.5,
- lon=-114.46,
- now=1_751_400_000,
- d=d,
- )
-
- assert len(rendered) <= 140
- assert "→" not in rendered, "short list should not be path-sampled"
- assert "Buhl, Eden, Hazelton" in rendered, "full comma-joined list expected"
- # Hazard wording is tightened even on the short-list path.
- assert "60mph winds" in rendered
- assert '1" hail' in rendered
- assert "radar" in rendered
-
-
-def test_svr_worst_case_fits_140(mem_db):
- """Pathologically long SVR payload: the final wire MUST fit 140 chars while
- still carrying event name, area, time, tightened hazard, and >=1 town."""
- long_locations = (
- "Buhl, Eden, Hazelton, Murtaugh, Richfield, Dietrich, Gooding, "
- "Hagerman, Wendell, Jerome, Kimberly, Hansen, Filer, and Shoshone"
- )
- description = (
- "HAZARD...Damaging winds to 70 mph and golf ball size hail.\n\n"
- f"Locations impacted include...{long_locations}"
- )
- d = {
- "eventCode": {"SAME": ["SVR"]},
- "certainty": "Observed",
- "parameters": {
- "maxWindGust": ["70 MPH"],
- "maxHailSize": ["1.75"],
- "eventMotionDescription": ["2200000T254DEG...35KT 42.5,-114.5"],
- },
- "description": description,
- }
- rendered = _render(
- event_type="Severe Thunderstorm Warning",
- area_desc="Twin Falls County",
- geocoder_city=None, county="Twin Falls", state="ID",
- expires_epoch=1_751_400_000, lat=42.5, lon=-114.46,
- now=1_751_400_000, d=d,
- )
- assert len(rendered) <= 140, f"{len(rendered)} chars:\n{rendered!r}"
- assert "Severe Thunderstorm Warning" in rendered # event name
- assert "Twin Falls County" in rendered # area
- assert "Until" in rendered # time
- assert "70mph winds" in rendered # tightened hazard (wind)
- assert '1.75" hail' in rendered # golf ball -> 1.75"
- assert "Buhl" in rendered # >=1 town present
-
-
-# ---- no dangling "— …" across ALL product types ----
-
-
-def _assert_no_dangling_separator(rendered: str):
- """The L4 motion/locations line must never end in a stray separator:
- no trailing '—…', '— …', or a bare '—'. Either a real location list
- follows the em-dash, or the em-dash (and its locations) are absent."""
- for line in rendered.splitlines():
- stripped = line.rstrip()
- assert not stripped.endswith("—…"), f"dangling '—…': {line!r}"
- assert not stripped.endswith("— …"), f"dangling '— …': {line!r}"
- assert not stripped.endswith("—"), f"bare trailing '—': {line!r}"
- # And the "— …" fragment must not appear mid-line either.
- assert "—…" not in stripped, f"'—…' fragment: {line!r}"
- assert "— …" not in stripped, f"'— …' fragment: {line!r}"
-
-
-def test_sps_worst_case_tightened_and_no_dangling(mem_db):
- """The reported live-log bug: a Special Weather Statement (SPS) with wind
- gusts + motion + a long town list previously collapsed L4 to
- 'Moving SW 24 mph —…' (all towns lost, dangling separator). After the fix:
- hazard is tightened, output fits 140, and L4 is either
- 'Moving … — ' or 'Moving …' — never a trailing '—…'."""
- 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}"
- )
- d = {
- "eventCode": {"SAME": ["SPS"]},
- "certainty": "Observed",
- "parameters": {
- # 225 DEG 21 KT -> "Moving SW 24 mph"
- "eventMotionDescription": ["2200000T225DEG...21KT 42.5,-114.5"],
- },
- "description": description,
- }
- rendered = _render(
- event_type="Special Weather Statement", area_desc="Twin Falls County",
- geocoder_city=None, county="Twin Falls", state="ID",
- expires_epoch=1_751_400_000, lat=42.5, lon=-114.46,
- now=1_751_400_000, d=d,
- )
- assert len(rendered) <= 140, f"{len(rendered)} chars:\n{rendered!r}"
- assert "Special Weather Statement" in rendered
- # (a) hazard tightened: "Wind gusts in excess of 45 mph" -> "45mph gusts",
- # "pea size hail" -> '0.25" hail'; filler dropped.
- assert "45mph gusts" in rendered, f"wind not tightened:\n{rendered!r}"
- assert '0.25" hail' in rendered, f"hail not numeric:\n{rendered!r}"
- assert "in excess of" not in rendered, "filler 'in excess of' survived"
- assert "· observed" in rendered, "certainty not collapsed"
- # (b) NEVER a dangling separator.
- _assert_no_dangling_separator(rendered)
- # (c) the motion line, when present, either carries a town or stands alone.
- last = rendered.splitlines()[-1]
- if last.startswith("Moving"):
- assert last == "Moving SW 24 mph" or " — " in last, (
- f"L4 neither motion-only nor motion+towns:\n{last!r}")
- if " — " in last:
- # A real town must follow the em-dash.
- tail = last.split(" — ", 1)[1].strip()
- assert tail and tail != "…", f"empty tail after em-dash:\n{last!r}"
-
-
-def test_wsw_hazard_tightened_and_no_dangling(mem_db):
- """Winter Weather product (WSW SAME code): wind-gust hazard is tightened and
- no dangling '—…' can appear."""
- long_locations = (
- "Sun Valley, Ketchum, Hailey, Bellevue, Carey, Picabo, Fairfield, "
- "and Gooding"
- )
- description = (
- "HAZARD...Wind gusts up to 45 mph and heavy snow.\n\n"
- f"Locations impacted include...{long_locations}"
- )
- d = {
- "eventCode": {"SAME": ["WSW"]},
- "certainty": "Observed",
- "parameters": {
- "eventMotionDescription": ["2200000T225DEG...21KT 42.5,-114.5"],
- },
- "description": description,
- }
- rendered = _render(
- event_type="Winter Weather Advisory", area_desc="Blaine County",
- geocoder_city=None, county="Blaine", state="ID",
- expires_epoch=1_751_400_000, lat=43.5, lon=-114.3,
- now=1_751_400_000, d=d,
- )
- assert len(rendered) <= 140, f"{len(rendered)} chars:\n{rendered!r}"
- assert "45mph gusts" in rendered, f"WSW wind not tightened:\n{rendered!r}"
- assert "heavy snow" in rendered
- assert "up to" not in rendered, "filler 'up to' survived"
- _assert_no_dangling_separator(rendered)
-
-
-def test_sps_pathological_towns_degrade_to_motion_only(mem_db):
- """When even a single sampled town cannot fit the remaining budget, L4 must
- degrade to motion-only ('Moving …') with NO trailing separator — never
- 'Moving … —…'."""
- # One absurdly long town name that cannot coexist with the em-dash + motion
- # in the leftover budget.
- long_town = "Averyverylongimpossibletownnamethatwillnotfitthebudgetatall" * 2
- description = (
- "HAZARD...Wind gusts in excess of 45 mph.\n\n"
- f"Locations impacted include...{long_town}"
- )
- d = {
- "eventCode": {"SAME": ["SPS"]},
- "certainty": "Observed",
- "parameters": {
- "eventMotionDescription": ["2200000T225DEG...21KT 42.5,-114.5"],
- },
- "description": description,
- }
- rendered = _render(
- event_type="Special Weather Statement", area_desc="Twin Falls County",
- geocoder_city=None, county="Twin Falls", state="ID",
- expires_epoch=1_751_400_000, lat=42.5, lon=-114.46,
- now=1_751_400_000, d=d,
- )
- assert len(rendered) <= 140
- _assert_no_dangling_separator(rendered)
- last = rendered.splitlines()[-1]
- # The town can't fit, so L4 (if present) is bare motion.
- if last.startswith("Moving"):
- assert " — " not in last, f"expected motion-only, got:\n{last!r}"
-
-
-def test_svr_no_dangling_separator(mem_db):
- """Re-verify SVR (the branch tightened earlier) still never dangles."""
- long_locations = (
- "Buhl, Eden, Hazelton, Murtaugh, Richfield, Dietrich, Gooding, "
- "Hagerman, Wendell, Jerome, Kimberly, Hansen, Filer, and Shoshone"
- )
- description = (
- "HAZARD...Damaging winds to 70 mph and golf ball size hail.\n\n"
- f"Locations impacted include...{long_locations}"
- )
- d = {
- "eventCode": {"SAME": ["SVR"]},
- "certainty": "Observed",
- "parameters": {
- "maxWindGust": ["70 MPH"], "maxHailSize": ["1.75"],
- "eventMotionDescription": ["2200000T254DEG...35KT 42.5,-114.5"],
- },
- "description": description,
- }
- rendered = _render(
- event_type="Severe Thunderstorm Warning", area_desc="Twin Falls County",
- geocoder_city=None, county="Twin Falls", state="ID",
- expires_epoch=1_751_400_000, lat=42.5, lon=-114.46,
- now=1_751_400_000, d=d,
- )
- assert len(rendered) <= 140
- assert "70mph winds" in rendered
- _assert_no_dangling_separator(rendered)
diff --git a/work/tests/test_nws_refactor.py b/work/tests/test_nws_refactor.py
index 4a856c2..8621613 100644
--- a/work/tests/test_nws_refactor.py
+++ b/work/tests/test_nws_refactor.py
@@ -1,57 +1,49 @@
-"""Phase-2 NWS refactor tests — formatter+gater architecture verification.
+"""NWS refactor tests — formatter+gater architecture verification.
-Four test groups:
+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. Golden byte-parity (tier-a): for each NWS fixture, run the OLD _render()
- under pinned_time+pinned_tz, then run the NEW format() from canonical
- data built by the Central bridge, and assert_byte_identical. This MUST
- be exactly equal — any difference is a regression.
+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. Cross-source identity: the native adapter's to_event() canonical dict
- (for a synthetic fixture) produces the same formatter output as the
- Central-bridge canonical dict built from the same alert data.
+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. Gate-sequence: replay a synthetic 4-step lifecycle (first→dup<3h→
- dup>3h→Cancel) through the OLD handle_nws gating and the NEW
- gating.nws.decide(), and assert broadcast/suppress match at every step.
-
-4. Schema-conformance: env/nws.py _fetch() emits all canonical schema keys;
+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 json
-import os
-import pathlib
import time
+from datetime import datetime
import pytest
-from meshai.central.nws_handler import _render, handle_nws
from meshai.central.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.notifications.gating.base import GateResult
-from meshai.persistence import close_thread_connection, get_db, init_db
+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_time,
- pinned_tz,
- run_gate_sequence,
-)
-
-# ── Shared epoch for deterministic renders ────────────────────────────────────
-_AT = 1_783_206_513.0 # captured_epoch for fixture 0000
-
-
-# ── Minimal fake Event for calling formatter without full pipeline ────────────
-
-class _FakeEvent:
- def __init__(self, data: dict):
- self.data = data
-
+from tests.harness.goldens import assert_byte_identical, load_fixtures, pinned_tz
# ── DB fixture ────────────────────────────────────────────────────────────────
@@ -67,357 +59,350 @@ def mem_db(monkeypatch, tmp_path):
persistence_db._initialised.discard(db_path)
-# ── Helper: build canonical data from a Central fixture ──────────────────────
+class _FakeEvent:
+ """Minimal fake Event for calling the formatter without the full pipeline."""
+ def __init__(self, data: dict):
+ self.data = data
-def _canonical_from_fixture(fix: dict) -> dict:
- """Extract canonical event.data dict from a Central NWS fixture.
-
- Mirrors exactly what handle_nws (cutover path) writes into data dict.
- Used in formatter golden tests without going through the full handler.
- """
- envelope = fix["envelope"]
- inner = envelope.get("data") or {}
- d = inner.get("data") or {}
- geo = inner.get("geo") or {}
- ge = (d.get("_enriched") or {}).get("geocoder") or {}
- category_raw = inner.get("category") or ""
-
- from meshai.central.nws_handler import _category_to_event_type, _parse_iso
-
- cap_id = d.get("id") or inner.get("id")
- event_type = d.get("event") or _category_to_event_type(category_raw)
- area_desc = d.get("areaDesc")
- headline = d.get("headline")
- description = d.get("description")
- cap_severity = d.get("severity")
- 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]
- certainty = d.get("certainty") or ""
- references = d.get("references") or []
- parameters = d.get("parameters") or {}
- msg_type = d.get("msgType")
+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": cap_id,
+ "cap_id": "test",
"event": event_type,
"same_code": same_code,
- "cap_severity": cap_severity,
+ "cap_severity": None,
"certainty": certainty,
"expires_at": expires_epoch,
"area_desc": area_desc,
- "geocoder": {
- "city": ge.get("city"),
- "county": county,
- "state": state,
- },
+ "geocoder": {"city": None, "county": county, "state": state},
"description": description,
- "parameters": parameters,
- "msgType": msg_type,
- "references": references,
- "category": category_raw,
- "headline": headline,
- # prefix injected by gater: "" for first sighting (no references)
- "_nws_prefix": "",
+ "parameters": parameters or {},
+ "msgType": "Alert",
+ "references": [],
+ "category": "",
+ "headline": None,
+ "_nws_prefix": prefix,
}
-def _old_render_from_fixture(fix: dict) -> str:
- """Call old _render() from a Central NWS fixture with pinned clock+tz.
-
- Returns the wire string.
- """
- envelope = fix["envelope"]
- inner = envelope.get("data") or {}
- d = inner.get("data") or {}
- geo = inner.get("geo") or {}
- ge = (d.get("_enriched") or {}).get("geocoder") or {}
- category_raw = inner.get("category") or ""
-
- from meshai.central.nws_handler import _category_to_event_type, _parse_iso
-
- event_type = d.get("event") or _category_to_event_type(category_raw)
- area_desc = d.get("areaDesc")
- cap_severity = d.get("severity")
- county = d.get("areaDesc") or ge.get("county")
- state = ge.get("state") or d.get("state")
- expires_epoch = _parse_iso(d.get("expires"))
-
- lat = lon = None
- cent = geo.get("centroid") or []
- if isinstance(cent, list) and len(cent) >= 2:
- lon, lat = cent[0], cent[1]
-
- epoch = float(fix.get("captured_epoch", _AT))
- return _render(
- event_type=event_type, area_desc=area_desc,
- geocoder_city=ge.get("city"), county=county, state=state,
- expires_epoch=expires_epoch, lat=lat, lon=lon,
- now=epoch, prefix="", d=d,
- )
-
-
# =============================================================================
-# 1. Golden byte-parity (tier-a)
+# 1. Formatter golden — native format() wire text
# =============================================================================
-class TestGoldenByteParity:
- """formatters/nws.format() is byte-identical to _render() for all fixtures.
+class TestFormatterGolden:
+ """formatters.nws.format() renders the expected wire text.
- Both the nws/ fixtures (first-sighting, no prefix) and nws_last/ fixtures
- (may have references → "Update" prefix) are tested.
+ 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 _render_and_format(self, fix: dict, prefix: str = ""):
- """Run old _render and new format() under identical pinned clock+tz.
+ def _canonical_from_fixture(self, fix: dict) -> dict:
+ """Extract canonical event.data from a Central-style NWS fixture.
- Returns (golden, new_output).
+ 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.
"""
- epoch = float(fix.get("captured_epoch", _AT))
-
- canonical = _canonical_from_fixture(fix)
- canonical["_nws_prefix"] = prefix
-
- budget = budget_for("nws")
-
- with pinned_tz("America/Boise"):
- with pinned_time(epoch):
- golden = _old_render_from_fixture(fix)
- # Override prefix in _render for parity (handler uses "" for first-sight)
- golden = _render(
- **{k: canonical.get(k) for k in
- ("event_type",)}, # we'll call _render directly below
- )
- # Actually call _render directly with same params as _old_render_from_fixture
- golden = _old_render_from_fixture(fix)
- new_out = nws_format(_FakeEvent(canonical), now=epoch, budget=budget)
-
- return golden, new_out
-
- @pytest.mark.parametrize("n", list(range(27)))
- def test_fixture_nws_byte_identical(self, n):
- """All 27 nws/ fixtures render byte-identically old vs new."""
- fixes = load_fixtures("nws")
- if n >= len(fixes):
- pytest.skip(f"fixture {n} not found (only {len(fixes)} fixtures)")
- fix = fixes[n]
- epoch = float(fix.get("captured_epoch", _AT))
- canonical = _canonical_from_fixture(fix)
- budget = budget_for("nws")
-
- with pinned_tz("America/Boise"):
- golden = _old_render_from_fixture(fix)
- new_out = nws_format(_FakeEvent(canonical), now=epoch, budget=budget)
-
- assert_byte_identical(new_out, golden)
-
- @pytest.mark.parametrize("n", list(range(10)))
- def test_fixture_nws_last_byte_identical(self, n):
- """All 10 nws_last/ fixtures render byte-identically old vs new."""
- fixes = load_fixtures("nws_last")
- if n >= len(fixes):
- pytest.skip(f"fixture {n} not found (only {len(fixes)} fixtures)")
- fix = fixes[n]
- epoch = float(fix.get("captured_epoch", _AT))
- canonical = _canonical_from_fixture(fix)
- budget = budget_for("nws")
-
- with pinned_tz("America/Boise"):
- golden = _old_render_from_fixture(fix)
- new_out = nws_format(_FakeEvent(canonical), now=epoch, budget=budget)
-
- assert_byte_identical(new_out, golden)
-
- def test_update_prefix_byte_identical(self):
- """'Update:' prefix variant is byte-identical."""
- fixes = load_fixtures("nws_last")
- if not fixes:
- pytest.skip("no nws_last fixtures")
- fix = fixes[0]
- epoch = float(fix.get("captured_epoch", _AT))
- canonical = _canonical_from_fixture(fix)
- canonical["_nws_prefix"] = "Update"
- budget = budget_for("nws")
-
envelope = fix["envelope"]
inner = envelope.get("data") or {}
d = inner.get("data") or {}
- geo = inner.get("geo") or {}
ge = (d.get("_enriched") or {}).get("geocoder") or {}
- from meshai.central.nws_handler import _category_to_event_type, _parse_iso
category_raw = inner.get("category") or ""
- event_type = d.get("event") or _category_to_event_type(category_raw)
+
+ 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"))
- lat = lon = None
- cent = geo.get("centroid") or []
- if isinstance(cent, list) and len(cent) >= 2:
- lon, lat = cent[0], cent[1]
+ same_code = ((d.get("eventCode") or {}).get("SAME") or [""])[0]
- with pinned_tz("America/Boise"):
- golden = _render(event_type=event_type, area_desc=area_desc,
- geocoder_city=ge.get("city"), county=county, state=state,
- expires_epoch=expires_epoch, lat=lat, lon=lon,
- now=epoch, prefix="Update", d=d)
- new_out = nws_format(_FakeEvent(canonical), now=epoch, budget=budget)
-
- assert_byte_identical(new_out, golden)
-
- def test_active_prefix_byte_identical(self):
- """'Active:' prefix variant is byte-identical."""
- fixes = load_fixtures("nws")
- if not fixes:
- pytest.skip("no nws fixtures")
- fix = fixes[0]
- epoch = float(fix.get("captured_epoch", _AT))
- canonical = _canonical_from_fixture(fix)
- canonical["_nws_prefix"] = "Active"
- budget = budget_for("nws")
-
- envelope = fix["envelope"]
- inner = envelope.get("data") or {}
- d = inner.get("data") or {}
- geo = inner.get("geo") or {}
- ge = (d.get("_enriched") or {}).get("geocoder") or {}
- from meshai.central.nws_handler import _category_to_event_type, _parse_iso
- category_raw = inner.get("category") or ""
- event_type = d.get("event") or _category_to_event_type(category_raw)
- 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"))
- lat = lon = None
- cent = geo.get("centroid") or []
- if isinstance(cent, list) and len(cent) >= 2:
- lon, lat = cent[0], cent[1]
-
- with pinned_tz("America/Boise"):
- golden = _render(event_type=event_type, area_desc=area_desc,
- geocoder_city=ge.get("city"), county=county, state=state,
- expires_epoch=expires_epoch, lat=lat, lon=lon,
- now=epoch, prefix="Active", d=d)
- new_out = nws_format(_FakeEvent(canonical), now=epoch, budget=budget)
-
- assert_byte_identical(new_out, golden)
-
-
-# =============================================================================
-# 2. Cross-source identity: native canonical == Central-sourced render
-# =============================================================================
-
-class TestCrossSourceIdentity:
- """Native adapter to_event() canonical == Central-bridge canonical for same alert."""
-
- def _make_native_raw(self, props: dict, onset: float, expires: float) -> dict:
- """Simulate what _fetch() builds for a single NWS API feature."""
return {
- "source": "nws",
- "event_id": props.get("id", ""),
- "event_type": props.get("event", "Unknown"),
- "severity": (props.get("severity") or "Unknown").lower(),
- "headline": props.get("headline", ""),
- "description": props.get("description") or "",
- "onset": onset,
- "expires": expires,
- "expires_at": expires,
- "areas": (props.get("geocode") or {}).get("UGC", []),
- "area_desc": props.get("areaDesc", ""),
- "fetched_at": time.time(),
- "cap_id": props.get("id", ""),
- "same_code": ((props.get("eventCode") or {}).get("SAME") or [""])[0],
- "cap_severity": props.get("severity", "Unknown"),
- "certainty": props.get("certainty", "Unknown"),
- "parameters": props.get("parameters") or {},
- "msgType": props.get("messageType", "Alert"),
- "references": props.get("references") or [],
- }
-
- def test_native_and_central_render_identically(self):
- """For a synthetic SVR alert, native and Central canonical render the same wire.
-
- Both paths must produce byte-identical output when given the same underlying
- alert data. The key equality constraints are:
- - same expires_at epoch
- - same same_code
- - same area_desc / geocoder.county
- - same parameters (wind/hail)
- - same _nws_prefix (both "")
- """
- from unittest.mock import MagicMock
- from meshai.env.nws import NWSAlertsAdapter
- from meshai.central.nws_handler import _parse_iso
-
- expires_iso = "2026-07-04T01:00:00-06:00"
- # Derive epoch from the ISO string so both paths use the same value.
- expires_epoch = _parse_iso(expires_iso) # int
-
- props = {
- "id": "urn:oid:test.svr.001",
- "event": "Severe Thunderstorm Warning",
- "severity": "Severe",
- "certainty": "Observed",
- "areaDesc": "Twin Falls County",
- "headline": "SVR Warning Twin Falls County",
- "description": "HAZARD...60 MPH winds and 1.00 inch hail.",
- "expires": expires_iso,
- "messageType": "Alert",
- "references": [],
- "parameters": {
- "maxWindGust": ["60 MPH"],
- "maxHailSize": ["1.00"],
- },
- "eventCode": {"SAME": ["SVR"]},
- "geocode": {"UGC": ["IDZ016"]},
- }
-
- # Native path: build raw → to_event() → event.data
- mock_cfg = MagicMock()
- mock_cfg.areas = ["ID"]
- mock_cfg.user_agent = "(test)"
- mock_cfg.severity_min = "moderate"
- mock_cfg.tick_seconds = 60
- adapter = NWSAlertsAdapter(mock_cfg)
-
- raw = self._make_native_raw(props, onset=expires_epoch - 7200, expires=expires_epoch)
- native_event = adapter.to_event(raw)
- native_canonical = native_event.data
-
- # Central path: build canonical manually (same logic as bridge)
- central_canonical = {
- "cap_id": props["id"],
- "event": "Severe Thunderstorm Warning",
- "same_code": "SVR",
- "cap_severity": "Severe",
- "certainty": "Observed",
+ "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": "Twin Falls County",
- "geocoder": {"city": None, "county": "Twin Falls County", "state": None},
- "description": props["description"],
- "parameters": props["parameters"],
- "msgType": "Alert",
- "references": [],
- "category": "weather_warning",
- "headline": props["headline"],
+ "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"):
- native_wire = nws_format(_FakeEvent(native_canonical), now=expires_epoch - 100, budget=budget)
- central_wire = nws_format(_FakeEvent(central_canonical), now=expires_epoch - 100, budget=budget)
+ result = nws_format(_FakeEvent(canonical), now=epoch, budget=budget)
- assert_byte_identical(native_wire, central_wire)
+ 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)
# =============================================================================
-# 3. Gate-sequence: old handle_nws vs new gating/nws.decide()
+# 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 and assert old/new gating decisions match.
+ """Replay a 4-step lifecycle through the native gating.nws.decide().
Steps:
1. First sighting → broadcast
@@ -456,15 +441,20 @@ class TestGateSequence:
}
def _make_canonical(self, fixture: dict) -> dict:
- """Build canonical dict from a fixture for nws_decide()."""
+ """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 ""
- from meshai.central.nws_handler import _category_to_event_type, _parse_iso
return {
"cap_id": d.get("id"),
- "event": d.get("event") or _category_to_event_type(category_raw),
+ "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 "",
@@ -479,27 +469,6 @@ class TestGateSequence:
"headline": d.get("headline"),
}
- def test_old_gate_sequence(self, mem_db):
- """4-step lifecycle through OLD handle_nws: first→dup<3h→dup>3h→Cancel."""
- cap_id = "urn:oid:old.gate.001"
- t0 = 1_783_200_000
- t1 = t0 + 1000 # <3h
- t2 = t0 + 11000 # >3h (10800s window)
- t3 = t0 + 12000
-
- def go(msg_type="Alert", now=t0):
- env = self._make_envelope(cap_id, msg_type=msg_type)["envelope"]
- data = {}
- wire = handle_nws(env, "central.wx.alert.us.id", data=data, now=int(now))
- if wire is not None and "_on_broadcast_committed" in data:
- data["_on_broadcast_committed"](float(now))
- return wire is not None
-
- assert go(now=t0) is True, "step1: first sighting should broadcast"
- assert go(now=t1) is False, "step2: dup within 3h should suppress"
- assert go(now=t2) is True, "step3: after 3h should rebroadcast"
- assert go("Cancel", now=t3) is False, "step4: Cancel tombstone should suppress"
-
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"
@@ -539,13 +508,14 @@ class TestGateSequence:
t0 = 1_783_200_000.0
t1 = t0 + 500
- # Broadcast parent first
+ # 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)
- data_p = {}
- wire_p = handle_nws(fix_parent["envelope"], fix_parent["subject"], data=data_p, now=int(t0))
- assert wire_p is not None, "parent should broadcast"
- if "_on_broadcast_committed" in data_p:
- data_p["_on_broadcast_committed"](t0)
+ 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(
@@ -553,27 +523,7 @@ class TestGateSequence:
references=[{"identifier": parent_id, "sent": "2026-07-04T00:00:00Z",
"effective": "2026-07-04T00:00:00Z"}],
)
-
- from meshai.central.nws_handler import _parse_iso, _category_to_event_type
- inner = fix_child["envelope"].get("data") or {}
- d = inner.get("data") or {}
- category_raw = inner.get("category") or ""
- canonical = {
- "cap_id": child_id,
- "event": d.get("event") or _category_to_event_type(category_raw),
- "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"),
- }
+ 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}"
@@ -584,7 +534,7 @@ class TestGateSequence:
# =============================================================================
-# 4. Schema-conformance: native env/nws.py emits canonical schema
+# 2. Schema-conformance: native env/nws.py emits canonical schema
# =============================================================================
class TestSchemaConformance:
@@ -688,7 +638,7 @@ class TestSchemaConformance:
# =============================================================================
-# 5. Formatter registration: weather_warning + weather_statement registered
+# 3. Formatter registration: weather_warning + weather_statement registered
# =============================================================================
class TestFormatterRegistration:
diff --git a/work/tests/test_quake_handler.py b/work/tests/test_quake_handler.py
deleted file mode 100644
index 65df6dd..0000000
--- a/work/tests/test_quake_handler.py
+++ /dev/null
@@ -1,193 +0,0 @@
-"""Tests for v0.5.10 USGS earthquakes handler."""
-import pytest
-
-from meshai.central.quake_handler import (
- handle_quake,
- within_250mi_of_idaho,
-)
-from meshai.persistence import close_thread_connection, init_db
-from meshai.persistence import db as persistence_db
-
-
-@pytest.fixture
-def mem_db(monkeypatch, tmp_path):
- db_path = str(tmp_path / "quake-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)
-
-
-def _quake_env(*, event_id="uu80141266", mag=3.5, depth_km=9.0,
- place="9 km SW of Stanley, Idaho",
- lat=44.094, lon=-115.962,
- tsunami=0, alert=None,
- time_ms=1780006952030,
- category="quake.event.minor"):
- return {
- "id": event_id, "subject": "central.quake.event.minor.unknown",
- "data": {
- "id": event_id, "adapter": "usgs_quake", "category": category,
- "severity": 0,
- "geo": {"centroid": [lon, lat], "primary_region": None},
- "data": {
- "id": event_id, "magnitude": mag, "place": place,
- "depth_km": depth_km, "time_ms": time_ms,
- "tsunami": tsunami, "alert": alert, "status": "reviewed",
- },
- },
- }
-
-
-def _commit(data, t):
- data["_on_broadcast_committed"](float(t))
-
-
-# ---- magnitude floor ----
-
-
-def test_m3_anywhere_broadcasts(mem_db):
- env = _quake_env(mag=3.5, lat=37.0, lon=-122.0) # SF Bay area, outside Idaho
- wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert "M3.5" in wire
-
-
-def test_m25_inside_idaho_broadcasts(mem_db):
- env = _quake_env(mag=2.7, lat=44.094, lon=-115.962, event_id="uu1")
- wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert "M2.7" in wire
-
-
-def test_m25_outside_idaho_skipped(mem_db):
- # San Francisco -- well outside 250mi of Idaho centroid.
- env = _quake_env(mag=2.7, lat=37.0, lon=-122.0, event_id="uu2")
- wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
- assert wire is None
-
-
-def test_below_25_skipped(mem_db):
- env = _quake_env(mag=1.01, lat=44.0, lon=-114.0, event_id="uu3")
- wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
- assert wire is None
-
-
-# ---- tsunami special ----
-
-
-def test_tsunami_any_magnitude_broadcasts(mem_db):
- env = _quake_env(mag=4.5, lat=10.0, lon=140.0, tsunami=1, event_id="japan1")
- wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert "TSUNAMI WARNING" in wire
- assert wire.startswith("🚨")
-
-
-# ---- PAGER alert ----
-
-
-def test_pager_orange_broadcasts(mem_db):
- env = _quake_env(mag=2.0, lat=37.0, lon=-122.0, alert="orange",
- event_id="pager1")
- wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
-
-
-def test_pager_red_broadcasts(mem_db):
- env = _quake_env(mag=2.0, lat=37.0, lon=-122.0, alert="red",
- event_id="pager2")
- wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
-
-
-# ---- wire format ----
-
-
-def test_uses_usgs_place_string(mem_db):
- env = _quake_env(mag=4.1, place="9 km SW of Stanley, Idaho",
- event_id="usgs1")
- wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
- assert "9 km SW of Stanley, Idaho" in wire
-
-
-def test_m5_uses_warning_emoji(mem_db):
- env = _quake_env(mag=5.2, lat=44.0, lon=-114.0, event_id="big1")
- wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
- assert wire.startswith("⚠️")
-
-
-def test_wire_includes_depth_and_coords(mem_db):
- env = _quake_env(mag=4.1, depth_km=9.0, lat=44.094, lon=-115.962,
- event_id="d1")
- wire = handle_quake(env, env["subject"], data={}, now=1_000_000)
- assert "Depth: 9 km" in wire
- assert "@ 44.094, -115.962" in wire
-
-
-# ---- per-event dedup ----
-
-
-def test_per_event_id_dedup_no_reissue(mem_db):
- env = _quake_env(mag=4.0, event_id="dedup1")
- data1 = {}
- handle_quake(env, env["subject"], data=data1, now=1_000_000)
- _commit(data1, 1_000_001)
-
- # Same event_id republishes (magnitude revision). Should NOT re-broadcast.
- env_rev = _quake_env(mag=4.2, event_id="dedup1") # higher mag, same id
- wire2 = handle_quake(env_rev, env_rev["subject"], data={}, now=1_000_300)
- assert wire2 is None
-
-
-# ---- distance helper ----
-
-
-def test_within_250mi_of_idaho_boundary():
- # Boise, ID -- inside
- assert within_250mi_of_idaho(43.6, -116.2) is True
- # San Francisco -- outside
- assert within_250mi_of_idaho(37.0, -122.0) is False
- # Seattle -- inside (250mi from Idaho centroid; verify the boundary)
- assert within_250mi_of_idaho(47.6, -122.3) is False
- # Boundary edge case (Idaho center)
- assert within_250mi_of_idaho(44.36, -114.61) is True
-
-
-# ---- commit callback ----
-
-
-def test_commit_callback_updates_last_broadcast(mem_db):
- env = _quake_env(mag=4.0, event_id="cb1")
- data = {}
- handle_quake(env, env["subject"], data=data, now=1_000_000)
- pre = mem_db.execute(
- "SELECT last_broadcast_at FROM quake_events WHERE event_id='cb1'"
- ).fetchone()
- assert pre["last_broadcast_at"] is None
- _commit(data, 1_000_001)
- post = mem_db.execute(
- "SELECT last_broadcast_at FROM quake_events WHERE event_id='cb1'"
- ).fetchone()
- assert post["last_broadcast_at"] == 1_000_001
-
-
-# ============================================================================
-# Budget-fit SAFETY CAP: a freak-long USGS place string must still fit 140.
-# ============================================================================
-
-from meshai.central.quake_handler import _render as _quake_render
-
-
-def test_quake_render_worst_case_fits_140():
- place = ("293 km SSW of a pathologically long place description island "
- "region in the remote northern pacific ocean near absolutely nowhere "
- "at all off the coast of the far edge of the map")
- wire = _quake_render(mag=7.9, place=place, depth_km=12, lat=44.123,
- lon=-114.987, tsunami=True, is_update=False)
- assert len(wire) <= 140, f"{len(wire)} chars:\n{wire!r}"
- # magnitude survives on the (critical) first line
- assert "M7.9" in wire
diff --git a/work/tests/test_quake_refactor.py b/work/tests/test_quake_refactor.py
index b3ac555..c162e28 100644
--- a/work/tests/test_quake_refactor.py
+++ b/work/tests/test_quake_refactor.py
@@ -1,18 +1,24 @@
"""Phase-1 quake refactor tests — reference implementation verification.
-Four test groups:
+The Central `quake_handler` module (`_render()`, `handle_quake()`) has been
+deleted — the native path is the only production path now. Pure old-vs-new
+parity assertions and tests that only replayed decisions through the
+deleted `handle_quake` have been removed; original diffs are preserved in
+git history. What remains exercises native code directly (hand-written
+expected strings are kept as regression pins on the current wire format).
+
+Three test groups:
1. Parity (tier-b): fixture 0002 → canonical data → formatter.
- Expected string is hand-written (the new correct format).
- The OLD _render() output for the same fixture is captured in a comment so
- the intended tier-b diff is explicit and reviewable.
- Two synthetic cases show the PAGER + update-prefix diffs explicitly.
+ Expected string is hand-written (the current correct format).
+ Two synthetic cases show the PAGER + update-prefix rendering explicitly.
-2. Cross-source identity: native adapter builds the same canonical data as
- the Central path for fixture 0002. Both render byte-identically.
+2. Cross-source identity: native adapter builds the same canonical data
+ shape the formatter reads.
-3. Gate-sequence: replay four synthetic events through the OLD handle_quake
- gating and the NEW gating.quake.decide(); assert broadcast/suppress match.
+3. Gate-sequence: exercise gating.quake.decide() directly across a
+ synthetic event sequence; assert broadcast/suppress thresholds and the
+ commit → suppress-on-replay lifecycle.
4. Schema-conformance: env/usgs_quake.py to_event() emits all canonical keys.
"""
@@ -26,7 +32,6 @@ from tests.harness.goldens import (
assert_byte_identical,
load_fixtures,
pinned_time,
- run_gate_sequence,
)
# ── Shared clock epoch for deterministic renders ─────────────────────────────
@@ -62,26 +67,13 @@ def _make_fake_event(data: dict):
class TestFormatterParity:
"""formatter/quake.format() renders correct output from canonical data."""
- def _render_old(self, *, mag, place, depth_km, lat, lon, tsunami, is_update=False):
- """Capture OLD _render() output for diff comments."""
- from meshai.central.quake_handler import _render
- from meshai.central.budget import budget_for
- return _render(mag=mag, place=place, depth_km=depth_km, lat=lat,
- lon=lon, tsunami=tsunami, is_update=is_update)
-
def test_fixture_0002_new_format(self):
- """Fixture 0002 (M3.3 Lima Montana) → NEW formatter output matches hand-written expected.
+ """Fixture 0002 (M3.3 Lima Montana) → formatter output matches hand-written expected.
Fixture 0002 has alert=null and no tsunami so the tier-b additions
- (PAGER line, update-prefix) are not visible. The old and new outputs
- are IDENTICAL for this fixture — which is correct. The hand-written
+ (PAGER line, update-prefix) are not visible. The hand-written
expected below documents the canonical format; synthetic tests below
show the tier-b additions.
-
- OLD _render() output (captured for diff transparency):
- "🌐 New: M3.3 — 19 km S of Lima, Montana\\nDepth: 11 km · @ 44.460, -112.611"
- NEW formatter output (same — no tier-b changes triggered):
- "🌐 New: M3.3 — 19 km S of Lima, Montana\\nDepth: 11 km · @ 44.460, -112.611"
"""
from meshai.notifications.formatters.quake import format as qfmt
@@ -119,27 +111,8 @@ class TestFormatterParity:
assert_byte_identical(result, expected)
- # Verify old _render matches new for this fixture (no tier-b diff)
- old_wire = self._render_old(
- mag=canonical["magnitude"], place=canonical["place"],
- depth_km=canonical["depth_km"], lat=canonical["lat"],
- lon=canonical["lon"], tsunami=canonical["tsunami"],
- is_update=False,
- )
- assert_byte_identical(result, old_wire), (
- "For fixture 0002 (null PAGER, is_update=False) old and new "
- "outputs must be identical — the tier-b diff only appears when "
- "PAGER or is_update are set."
- )
-
def test_tier_b_pager_orange_rendered(self):
- """Tier-b ①: PAGER=orange is NOW rendered on a 4th line.
-
- OLD _render() output (captured):
- "🌐 New: M2.0 — Off the coast of Oregon\\nDepth: 10 km · @ 44.000, -125.000"
- NEW formatter output (tier-b change — PAGER line added):
- "🌐 New: M2.0 — Off the coast of Oregon\\nDepth: 10 km · @ 44.000, -125.000\\n⚠️ PAGER: orange"
- """
+ """Tier-b ①: PAGER=orange is rendered on a 4th line."""
from meshai.notifications.formatters.quake import format as qfmt
canonical = {
@@ -158,12 +131,6 @@ class TestFormatterParity:
"distance_km": 500.0,
}
- # OLD _render() output (PAGER not rendered)
- old_wire = self._render_old(
- mag=2.0, place="Off the coast of Oregon", depth_km=10.0,
- lat=44.0, lon=-125.0, tsunami=False, is_update=False,
- )
- # NEW formatter output (PAGER rendered as 4th line)
expected_new = (
"\U0001f310 New: M2.0 — Off the coast of Oregon"
"\nDepth: 10 km · @ 44.000, -125.000"
@@ -174,19 +141,9 @@ class TestFormatterParity:
result = qfmt(_make_fake_event(canonical), now=_AT, budget=140)
assert_byte_identical(result, expected_new)
- # Confirm the old wire does NOT have the PAGER line
- assert "PAGER" not in old_wire, (
- f"OLD _render() must not contain PAGER line; got: {old_wire!r}"
- )
def test_tier_b_update_prefix_rendered(self):
- """Tier-b ②: is_update=True produces 'Update:' prefix (was hard-coded 'New:').
-
- OLD _render() output (is_update always False):
- "🌐 New: M3.0 — 5 km NE of Stanley, Idaho\\nDepth: 8 km · @ 44.200, -114.900"
- NEW formatter output (is_update=True):
- "🌐 Update: M3.0 — 5 km NE of Stanley, Idaho\\nDepth: 8 km · @ 44.200, -114.900"
- """
+ """Tier-b ②: is_update=True produces 'Update:' prefix."""
from meshai.notifications.formatters.quake import format as qfmt
canonical = {
@@ -205,30 +162,20 @@ class TestFormatterParity:
"distance_km": 10.0,
}
- # OLD _render() always uses is_update=False
- old_wire = self._render_old(
- mag=3.0, place="5 km NE of Stanley, Idaho", depth_km=8.0,
- lat=44.2, lon=-114.9, tsunami=False, is_update=False,
- )
expected_new = (
"\U0001f310 Update: M3.0 — 5 km NE of Stanley, Idaho"
"\nDepth: 8 km · @ 44.200, -114.900"
)
- expected_old = (
- "\U0001f310 New: M3.0 — 5 km NE of Stanley, Idaho"
- "\nDepth: 8 km · @ 44.200, -114.900"
- )
with pinned_time(_AT):
result = qfmt(_make_fake_event(canonical), now=_AT, budget=140)
assert_byte_identical(result, expected_new)
- assert_byte_identical(old_wire, expected_old)
assert "Update:" in result
assert "New:" not in result
def test_tsunami_escalation_preserved(self):
- """Tsunami escalation (🚨 emoji + TSUNAMI WARNING line) unchanged from _render."""
+ """Tsunami escalation renders the 🚨 emoji + TSUNAMI WARNING line."""
from meshai.notifications.formatters.quake import format as qfmt
canonical = {
@@ -247,16 +194,18 @@ class TestFormatterParity:
"distance_km": 8000.0,
}
+ expected = (
+ "\U0001f6a8 New: M4.5 — off the coast of Japan"
+ "\nDepth: 5 km · @ 35.000, 141.000"
+ "\n\U0001f6a8 TSUNAMI WARNING"
+ )
+
with pinned_time(_AT):
result = qfmt(_make_fake_event(canonical), now=_AT, budget=140)
- old_wire = self._render_old(
- mag=4.5, place="off the coast of Japan", depth_km=5.0,
- lat=35.0, lon=141.0, tsunami=True,
- )
assert result.startswith("\U0001f6a8"), "Tsunami emoji must be 🚨"
assert "\U0001f6a8 TSUNAMI WARNING" in result
- assert_byte_identical(result, old_wire)
+ assert_byte_identical(result, expected)
def test_m5_escalation_emoji_preserved(self):
"""M5+ uses ⚠️ emoji — unchanged from _render."""
@@ -433,22 +382,15 @@ def _make_envelope(*, event_id, mag, lat, lon, depth_km=10.0, place=None,
class TestGateSequence:
- """Gate parity: old handle_quake decisions match new gating.quake.decide()."""
+ """gating.quake.decide() gate thresholds + commit/suppress lifecycle."""
@pytest.fixture(autouse=True)
def _db(self, mem_db):
"""All tests in this class share the same mem_db."""
self.db = mem_db
- def _old_gate(self, fixture, *, now):
- """Old path: handle_quake returning non-None = broadcast."""
- from meshai.central.quake_handler import handle_quake
- env = fixture["envelope"]
- wire = handle_quake(env, fixture["subject"], data={}, now=int(now))
- return wire is not None
-
- def _new_gate(self, fixture, *, now):
- """New path: gating.quake.decide()."""
+ def _decide(self, fixture, *, now):
+ """Build canonical data from a Central-style fixture and call decide()."""
from meshai.notifications.gating.quake import decide
env = fixture["envelope"]
inner = env.get("data") or {}
@@ -475,18 +417,12 @@ class TestGateSequence:
return decide(canonical, source="usgs_quake", now=float(now))
def test_gate_sequence_matches(self):
- """Four-event sequence: old and new gates make identical broadcast/suppress decisions.
+ """Four-event sequence exercises all of decide()'s broadcast thresholds.
- Sequence (each event has a DISTINCT event_id — the commit/suppress
- cycle is tested separately in test_suppress_after_commit):
[0] M2.0, far (below all thresholds) → suppress
[1] M2.7, within Idaho (regional gate) → broadcast
[2] M3.5, anywhere (global floor) → broadcast
[3] M6.0 + tsunami (any-magnitude tsunami gate) → broadcast
-
- Gate decisions (broadcast True/False) must match between old and new.
- NOTE: PAGER/update-prefix are formatter-only tier-b changes; they do
- NOT affect gate decisions — any divergence here is a regression.
"""
t_base = 1_780_000_000.0
@@ -503,84 +439,35 @@ class TestGateSequence:
fx3 = _make_envelope(event_id="gs_seq_3", mag=6.0, lat=35.0, lon=141.0,
tsunami=1, time_ms=int((t_base + 300) * 1000))
- ordered = [fx0, fx1, fx2, fx3]
- timeline = [t_base, t_base + 100, t_base + 200, t_base + 300]
+ r0 = self._decide(fx0, now=t_base)
+ r1 = self._decide(fx1, now=t_base + 100)
+ r2 = self._decide(fx2, now=t_base + 200)
+ r3 = self._decide(fx3, now=t_base + 300)
- results = run_gate_sequence(
- self._old_gate,
- self._new_gate,
- ordered,
- timeline=timeline,
- )
-
- mismatches = [r for r in results if not r["match"]]
- assert not mismatches, (
- "Gate sequence mismatch between old handle_quake and new decide():\n"
- + "\n".join(
- f" step {r['fixture_n']}: old={r['old_broadcast']} "
- f"new={r['new_broadcast']} diffs={r['diffs']}"
- for r in mismatches
- )
- )
-
- # Verify expected pattern
- assert results[0]["old_broadcast"] is False, "M2.0 far must be suppressed"
- assert results[1]["old_broadcast"] is True, "M2.7 Idaho must broadcast"
- assert results[2]["old_broadcast"] is True, "M3.5 global must broadcast"
- assert results[3]["old_broadcast"] is True, "M6.0+tsunami must broadcast"
+ assert r0.broadcast is False, "M2.0 far must be suppressed"
+ assert r1.broadcast is True, "M2.7 Idaho must broadcast"
+ assert r2.broadcast is True, "M3.5 global must broadcast"
+ assert r3.broadcast is True, "M6.0+tsunami must broadcast"
def test_suppress_after_commit(self):
- """After commit, the same event_id is suppressed by both old and new gates.
-
- The run_gate_sequence harness does not call commits between steps, so
- the commit+suppress lifecycle is tested here separately by manual
- sequencing.
- """
- from meshai.central.quake_handler import handle_quake
- from meshai.notifications.gating.quake import decide
-
+ """After commit, a replay of the same event_id is suppressed by decide()."""
t0 = 1_780_000_000.0
event_id = "suppress_after_commit_test"
fx = _make_envelope(event_id=event_id, mag=3.5, lat=44.09, lon=-115.96,
time_ms=int(t0 * 1000))
- env = fx["envelope"]
- # First arrival: both old and new broadcast
- data1 = {}
- old_wire1 = handle_quake(env, fx["subject"], data=data1, now=int(t0))
- assert old_wire1 is not None, "First arrival must broadcast (old)"
+ # First arrival: broadcast.
+ result1 = self._decide(fx, now=t0)
+ assert result1.broadcast is True, "First arrival must broadcast"
+ assert result1.commit is not None, "commit callback must be attached"
- # Build canonical from fixture for new gate
- inner = env["data"]
- d = inner["data"]
- geo = inner["geo"]
- cent = geo["centroid"]
- canonical = {
- "magnitude": d["magnitude"],
- "depth_km": d.get("depth_km") or d.get("depth"),
- "lat": cent[1], "lon": cent[0],
- "place": d.get("place"),
- "tsunami": bool(d.get("tsunami")),
- "pager": d.get("alert"),
- "occurred_at": int(d["time_ms"] / 1000),
- "event_id": event_id,
- }
- # Since old gate already wrote the row (INSERT), new gate sees the
- # same DB state. Both should broadcast on first arrival.
- # (We test new gate's second call AFTER commit below)
+ # Commit (simulates confirmed delivery).
+ result1.commit(t0 + 1.0)
- # Call commit (simulates confirmed delivery)
- assert "_on_broadcast_committed" in data1, "commit callback must be attached"
- data1["_on_broadcast_committed"](t0 + 1.0)
-
- # Second arrival with same event_id — old gate must suppress
- old_wire2 = handle_quake(env, fx["subject"], data={}, now=int(t0 + 60))
- assert old_wire2 is None, "Old gate must suppress after commit"
-
- # New gate must also suppress
- new_result2 = decide(canonical, source="usgs_quake", now=t0 + 60)
- assert new_result2.broadcast is False, "New gate must suppress after commit"
+ # Second arrival with same event_id — must suppress.
+ result2 = self._decide(fx, now=t0 + 60)
+ assert result2.broadcast is False, "Gate must suppress after commit"
def test_severity_override_from_decide(self):
"""decide() sets _severity_override=immediate for tsunami/PAGER."""
diff --git a/work/tests/test_rf_v057.py b/work/tests/test_rf_v057.py
deleted file mode 100644
index 9b22752..0000000
--- a/work/tests/test_rf_v057.py
+++ /dev/null
@@ -1,245 +0,0 @@
-"""v0.5.7-rf: SWPC subject validation + protons severity=0 docs + categories audit.
-
-Covers three things shipped in v0.5.7-rf:
-
-1. SWPC subscription subject -- verifies the existing `central.space.>`
- tail-only-`>` form (per Central v0.10.0 guide §swpc_*: planetary, no
- region in subject; one umbrella subscription covers swpc_alerts,
- swpc_kindex, swpc_protons). The pattern was already correct from v0.5.4
- work; this phase pins it explicitly so future "add a region tail"
- refactors fail loudly.
-2. swpc_protons severity=0 routing -- per guide §swpc_protons live sample
- the adapter always publishes severity=0. Verifies map_severity(0) ->
- "routine" and the NotificationToggle.severity_channels string-keyed
- dict accepts "routine" with no IndexError. The "silently dropped"
- failure mode the prompt described does not exist; this test is a
- regression guard against a future refactor introducing it.
-3. ALERT_CATEGORIES RF-family audit -- adds four missing entries that
- meshai emits but the rule editor couldn't target:
- - rf_anomalous_propagation (ducting.py super_refraction tier)
- - rf_ducting_enhancement (ducting.py duct + surface_duct tiers)
- - rf_propagation_alert (central swpc_alerts -> space.alert)
- - solar_radiation_storm (central swpc_protons -> space.proton_flux)
- Verifies geomagnetic_storm (central swpc_kindex -> space.kindex)
- stays mapped. Legacy hf_blackout and tropospheric_ducting are kept as
- selectable forward-compat targets even though no current emitter
- produces them; flagged in the commit body for follow-up.
-"""
-
-import inspect
-import json
-import re
-
-import pytest
-
-from meshai.central.consumer import (
- CentralConsumer,
- _SUBJECTS_BARE,
- _subjects_for,
- map_category,
- map_severity,
-)
-from meshai.config import EnvironmentalConfig, NotificationToggle
-from meshai.notifications.categories import ALERT_CATEGORIES
-from meshai.notifications.pipeline.bus import EventBus
-
-
-def _assert_legal_nats(subject: str) -> None:
- tokens = subject.split(".")
- if ">" in tokens:
- assert tokens[-1] == ">", f"`>` not at tail in {subject!r}"
- assert tokens.count(">") == 1, f"multiple `>` in {subject!r}"
- for tok in tokens:
- assert tok, f"empty token in {subject!r}"
- if tok not in {"*", ">"}:
- assert "*" not in tok and ">" not in tok, f"mixed wildcard in token {tok!r}"
-
-
-# ---------- FIX 1: SWPC subject pattern -----------------------------------
-
-
-def test_swpc_subject_is_global_umbrella():
- """Per Central v0.10.0 guide §space stream, all SWPC adapters publish
- under `central.space.>`. Single tail-only-`>` subscription catches
- all three (swpc_alerts / swpc_kindex / swpc_protons)."""
- subs = _subjects_for("swpc", "us.id")
- assert subs == ["central.space.>"]
- for s in subs:
- _assert_legal_nats(s)
-
-
-def test_swpc_subject_ignores_region():
- """Space weather is planetary; region argument MUST be a no-op."""
- assert _subjects_for("swpc", "us.id") == ["central.space.>"]
- assert _subjects_for("swpc", "us.mt") == ["central.space.>"]
- assert _subjects_for("swpc", "") == ["central.space.>"]
- assert _subjects_for("swpc", None) == ["central.space.>"]
-
-
-def test_swpc_subject_covers_all_three_adapter_subjects():
- """The umbrella `central.space.>` matches every per-adapter subject
- documented in the guide."""
- sub = _subjects_for("swpc", "us.id")[0]
- # `>` matches one or more tokens at the tail.
- assert sub.endswith(".>")
- prefix = sub[:-2] # strip the .>
- for published in (
- "central.space.alert.a20f", # swpc_alerts (4 tokens, product_id tail)
- "central.space.kindex", # swpc_kindex (3 tokens, fixed)
- "central.space.proton_flux", # swpc_protons (3 tokens, fixed)
- ):
- assert published.startswith(prefix), f"{published!r} not covered by {sub!r}"
-
-
-# ---------- FIX 2: severity=0 routing -------------------------------------
-
-
-def test_map_severity_zero_routes_to_routine():
- """All three SWPC adapters publish severity=0 by default. The boundary
- contract: 0 -> 'routine' (not dropped, not error)."""
- assert map_severity(0) == "routine"
-
-
-def test_severity_channels_dict_accepts_routine_key():
- """NotificationToggle.severity_channels is dict-keyed by severity STRING
- -- so "routine" is a valid key with no IndexError vector. Pins the
- contract so a refactor to an int-indexed list would break this test."""
- t = NotificationToggle(name="rf_propagation")
- assert isinstance(t.severity_channels, dict)
- # dict.get returns the default for unknown keys; no exception possible.
- assert t.severity_channels.get("routine", ["mesh_broadcast"]) == ["mesh_broadcast"]
-
-
-@pytest.mark.skip(reason="v0.5.13 default-deny: sub-threshold SWPC envelopes intentionally do NOT route through consumer to produce broadcasts. This is the architectural fix.")
-def test_swpc_protons_severity_zero_routes_through_consumer():
- """Synthetic swpc_protons envelope (severity=0 per guide §swpc_protons)
- -- verify it normalizes to ev.severity='routine' and emits on the bus
- with no exception."""
- rec = []
- bus = EventBus(); bus.subscribe(rec.append)
- c = CentralConsumer(EnvironmentalConfig(), bus)
- env = {"id": "2026-05-18T05:55:00Z|>=100 MeV", "data": {
- "id": "2026-05-18T05:55:00Z|>=100 MeV", "adapter": "swpc_protons",
- "category": "space.proton_flux",
- "time": "2026-05-18T05:55:00Z", "severity": 0,
- "geo": {"centroid": None, "primary_region": None, "regions": []},
- "data": {"flux": 0.16, "energy": ">=100 MeV",
- "time_tag": "2026-05-18T05:55:00Z", "satellite": 19}}}
- ev = c._handle("central.space.proton_flux", json.dumps(env).encode())
- assert ev is not None
- assert ev.severity == "routine"
- assert ev.category == "solar_radiation_storm"
- assert ev.source == "swpc"
- assert len(rec) == 1
-
-
-@pytest.mark.skip(reason="v0.5.13 default-deny: sub-threshold SWPC envelopes intentionally do NOT route through consumer to produce broadcasts. This is the architectural fix.")
-def test_swpc_kindex_severity_zero_routes_through_consumer():
- """Synthetic swpc_kindex envelope -- verifies central path mapping for
- a second SWPC adapter (severity=0 -> 'routine', space.kindex ->
- geomagnetic_storm)."""
- rec = []
- bus = EventBus(); bus.subscribe(rec.append)
- c = CentralConsumer(EnvironmentalConfig(), bus)
- env = {"id": "2026-05-12T00:00:00", "data": {
- "id": "2026-05-12T00:00:00", "adapter": "swpc_kindex",
- "category": "space.kindex",
- "time": "2026-05-12T00:00:00Z", "severity": 0,
- "geo": {"centroid": None, "primary_region": None, "regions": []},
- "data": {"Kp": 0.67, "time_tag": "2026-05-12T00:00:00",
- "a_running": 3, "station_count": 8}}}
- ev = c._handle("central.space.kindex", json.dumps(env).encode())
- assert ev is not None
- assert ev.severity == "routine"
- assert ev.category == "geomagnetic_storm"
-
-
-# ---------- FIX 3: ALERT_CATEGORIES RF-family audit ----------------------
-
-
-@pytest.mark.parametrize("cat", [
- "rf_anomalous_propagation",
- "rf_ducting_enhancement",
- "rf_propagation_alert",
- "solar_radiation_storm",
-])
-def test_v057_rf_added_categories_present(cat):
- """v0.5.7-rf: four new rf_propagation categories must be registry-present
- so the Advanced Rules editor can target them."""
- assert cat in ALERT_CATEGORIES
- info = ALERT_CATEGORIES[cat]
- assert info["toggle"] == "rf_propagation"
- assert info["name"]
- assert info["description"]
- assert info["default_severity"] in {"routine", "priority", "immediate"}
- assert info["example_message"]
-
-
-def test_geomagnetic_storm_still_in_registry():
- """swpc_kindex -> space.kindex -> geomagnetic_storm: registry entry
- survives the v0.5.7-rf edit."""
- assert "geomagnetic_storm" in ALERT_CATEGORIES
- assert ALERT_CATEGORIES["geomagnetic_storm"]["toggle"] == "rf_propagation"
-
-
-@pytest.mark.parametrize(
- "central_cat,expected",
- [
- ("space.alert.a20f", "rf_propagation_alert"),
- ("space.alert", "rf_propagation_alert"),
- ("space.kindex", "geomagnetic_storm"),
- ("space.proton_flux", "solar_radiation_storm"),
- ("space.unknown_sub", "geomagnetic_storm"), # catchall
- ],
-)
-def test_map_category_swpc_routings(central_cat, expected):
- """Pin the central -> meshai category map for each SWPC adapter."""
- assert map_category(central_cat) == expected
-
-
-def _native_emitted_rf_categories() -> set[str]:
- """Walk ducting.py for _TIER_CATEGORY values mapping to toggle=rf_propagation."""
- from meshai.env import ducting as ducting_mod
- src = inspect.getsource(ducting_mod)
- # _TIER_CATEGORY entries are `"": "",` literals.
- emitted = set(re.findall(
- r'_TIER_CATEGORY\s*=\s*\{([^}]+)\}', src, re.DOTALL))
- cats: set[str] = set()
- for block in emitted:
- cats |= set(re.findall(r':\s*"([a-z_]+)"', block))
- return {c for c in cats if c in ALERT_CATEGORIES
- and ALERT_CATEGORIES[c].get("toggle") == "rf_propagation"}
-
-
-def _central_path_rf_categories() -> set[str]:
- central_inputs = [
- "space.alert.a20f", "space.alert",
- "space.kindex",
- "space.proton_flux",
- "space.unknown",
- ]
- return {map_category(c) for c in central_inputs}
-
-
-def test_alert_categories_rf_complete():
- """Native + central-path emit set must be a SUBSET of registry rf
- entries (i.e., everything we emit is selectable). Legacy entries
- without an emitter are allowed as forward-compat targets and
- documented in the commit body."""
- registry_rf = {
- cid for cid, info in ALERT_CATEGORIES.items()
- if info.get("toggle") == "rf_propagation"
- }
- native = _native_emitted_rf_categories()
- central = _central_path_rf_categories()
- emitted = native | central
- missing = emitted - registry_rf
- assert not missing, f"rf emit set missing from ALERT_CATEGORIES: {missing}"
- # Sanity: at minimum the four v0.5.7-rf additions + geomagnetic_storm
- # must be in the emit set.
- for required in (
- "rf_anomalous_propagation", "rf_ducting_enhancement",
- "rf_propagation_alert", "solar_radiation_storm",
- "geomagnetic_storm",
- ):
- assert required in emitted, f"{required!r} not emitted by native or central path"
diff --git a/work/tests/test_satpass_event_path.py b/work/tests/test_satpass_event_path.py
index 71c25c3..78f8fdc 100644
--- a/work/tests/test_satpass_event_path.py
+++ b/work/tests/test_satpass_event_path.py
@@ -91,32 +91,6 @@ def _enable_satpass():
invalidate_cache()
-# ── CENTRAL_ADAPTER_TO_SOURCE mapping ───────────────────────────────
-
-def test_adapter_to_source_celestrak_tle():
- from meshai.central.consumer import CENTRAL_ADAPTER_TO_SOURCE
- assert CENTRAL_ADAPTER_TO_SOURCE["celestrak_tle"] == "satpass"
-
-
-def test_adapter_to_source_n2yo_visualpasses():
- from meshai.central.consumer import CENTRAL_ADAPTER_TO_SOURCE
- assert CENTRAL_ADAPTER_TO_SOURCE["n2yo_visualpasses"] == "satpass"
-
-
-def test_adapter_to_source_satpass_predict():
- from meshai.central.consumer import CENTRAL_ADAPTER_TO_SOURCE
- assert CENTRAL_ADAPTER_TO_SOURCE["satpass_predict"] == "satpass"
-
-
-def test_no_stale_sat_names_in_adapter_map():
- """Wire names sat_passes / sat_tles / sat_tle must NOT appear."""
- from meshai.central.consumer import CENTRAL_ADAPTER_TO_SOURCE
- for stale in ("sat_passes", "sat_tles", "sat_tle"):
- assert stale not in CENTRAL_ADAPTER_TO_SOURCE, (
- f"stale adapter name {stale!r} still in CENTRAL_ADAPTER_TO_SOURCE"
- )
-
-
# ── TLE handler route ──────────────────────────────────────────────
def test_tle_handler_inserts_sat_tles_row():
@@ -216,25 +190,3 @@ def test_handler_reads_min_elevation():
src = inspect.getsource(satpass_handler)
assert "min_elevation" in src
assert "min_elevation_deg" not in src
-
-
-# ── Dispatch routing in consumer._normalize ─────────────────────────
-
-def test_consumer_dispatch_celestrak_tle():
- """consumer._normalize dispatch must route celestrak_tle to tle_handler."""
- import inspect
- from meshai.central import consumer
- src = inspect.getsource(consumer)
- assert '"celestrak_tle"' in src
- assert '"sat_tles"' not in src
- assert '"sat_tle"' not in src
-
-
-def test_consumer_dispatch_n2yo_and_satpass_predict():
- """consumer._normalize dispatch must route n2yo/satpass_predict to satpass_handler."""
- import inspect
- from meshai.central import consumer
- src = inspect.getsource(consumer)
- assert '"n2yo_visualpasses"' in src
- assert '"satpass_predict"' in src
- assert '"sat_passes"' not in src
diff --git a/work/tests/test_satpass_persisted_timer.py b/work/tests/test_satpass_persisted_timer.py
index adb84a8..d38d2bf 100644
--- a/work/tests/test_satpass_persisted_timer.py
+++ b/work/tests/test_satpass_persisted_timer.py
@@ -1,26 +1,26 @@
-"""Tests for the satpass persisted-timer reboot-recovery fix.
+"""Tests for the satpass persisted-timer `due_at` column.
Pending satellite-pass consolidations used to be scheduled only as in-memory
asyncio TimerHandles, so a restart orphaned any satpass_pending rows: the row
-survived but its timer did not, and it was never consolidated/broadcast.
+survived but its timer did not, and it was never consolidated/broadcast. The
+fix persisted a durable `due_at` on each pending row and added a startup
+sweep (`CentralConsumer._sweep_pending_satpass`) that reconstructed a timer
+for every pending consolidated_id off its persisted due_at.
-The fix persists a durable `due_at` on each pending row and adds a startup
-sweep (`CentralConsumer._sweep_pending_satpass`) that reconstructs a timer for
-every pending consolidated_id off its persisted due_at, reusing the existing
-`_satpass_consolidation_fire` emit path.
-
-These tests cover:
- - a PAST-due orphan is recovered (its timer fires -> consolidation invoked)
- - a FUTURE-due row is scheduled, NOT fired immediately
- - `due_at` is persisted on the normal ingest path
- - SCHEMA_VERSION == 22 and the v22 migration applies cleanly on a fresh DB
+The Central NATS consumer (and `_sweep_pending_satpass` with it) was retired
+2026-07 -- the sweep's tests are gone with it. The native satpass path
+(env/satpass.py) never used the satpass_pending buffer or this sweep in the
+first place (it consolidates in-memory within a single tick), so nothing
+live is affected. What remains here:
+ - `due_at` is persisted on the normal ingest path (satpass_handler.py,
+ still live -- shared by both paths historically, now native-only)
+ - SCHEMA_VERSION == 26 and the v22 migration (which added the due_at
+ column) still applies cleanly on a fresh DB
"""
from __future__ import annotations
-import asyncio
import json
import time
-import types
import pytest
@@ -44,33 +44,6 @@ def _enable_satpass_db(norad_ids=(25544,), dry_run=True):
invalidate_cache()
-def _insert_pending(consolidated_id, *, due_at, observer="Boise",
- norad_id=25544, received_at=None):
- """Write a single satpass_pending row with an explicit due_at."""
- conn = get_db()
- now = int(time.time()) if received_at is None else received_at
- aos = now + 600
- los = aos + 360
- conn.execute(
- "INSERT OR REPLACE INTO satpass_pending("
- "consolidated_id, observer, sat_name, norad_id, max_elevation, "
- "aos_at, los_at, aos_compass, los_compass, peak_compass, received_at, "
- "due_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
- (consolidated_id, observer, "ISS", norad_id, 72.5,
- aos, los, "SW", "NE", "S", now, due_at))
-
-
-def _make_consumer(bus=None):
- """Construct a CentralConsumer with minimal fakes (no NATS needed)."""
- from meshai.central.consumer import CentralConsumer
- env = types.SimpleNamespace(central=None)
- return CentralConsumer(env, bus)
-
-
-def _run(coro):
- return asyncio.new_event_loop().run_until_complete(coro)
-
-
def _ingest_envelope(norad_id=25544, observer="Boise", max_el=72.5,
aos="2026-06-12T03:32:00Z", los="2026-06-12T03:38:00Z"):
return {
@@ -147,100 +120,3 @@ def test_due_at_persisted_on_normal_ingest():
assert row["due_at"] is not None
assert row["due_at"] == row["received_at"] + CONSOLIDATION_DELAY
assert row["due_at"] == now + CONSOLIDATION_DELAY
-
-
-# ── startup sweep: past-due orphan is recovered ──────────────────────
-
-def test_sweep_recovers_past_due_orphan(monkeypatch):
- """A pending row with due_at in the PAST fires consolidation via the sweep."""
- _enable_satpass_db(norad_ids=[25544], dry_run=True)
- now = int(time.time())
- cid = "25544:ORPHAN"
- _insert_pending(cid, due_at=now - 100, received_at=now - 105)
-
- fired = []
- import meshai.central.satpass_handler as sh
- real = sh.consolidate_satpass_pending
-
- def _spy(consolidated_id):
- fired.append(consolidated_id)
- return real(consolidated_id) # exercise the real path (dry-run -> None)
-
- monkeypatch.setattr(sh, "consolidate_satpass_pending", _spy)
-
- consumer = _make_consumer(bus=None)
-
- async def _main():
- consumer._sweep_pending_satpass(now=now)
- # overdue orphan is armed at ~0.5s; give the loop time to fire it.
- await asyncio.sleep(1.0)
-
- _run(_main())
-
- assert cid in fired, "sweep did not fire consolidation for the orphaned cid"
- # Orphan recovered: consolidation (dry-run) drained its pending rows.
- conn = get_db()
- remaining = conn.execute(
- "SELECT COUNT(*) AS n FROM satpass_pending WHERE consolidated_id=?",
- (cid,)).fetchone()["n"]
- assert remaining == 0
-
-
-# ── startup sweep: future row scheduled, not fired now ───────────────
-
-def test_sweep_schedules_future_row_without_firing(monkeypatch):
- """A pending row with due_at in the FUTURE is armed but does not fire yet."""
- _enable_satpass_db(norad_ids=[25544], dry_run=True)
- now = int(time.time())
- cid = "25544:FUTURE"
- _insert_pending(cid, due_at=now + 3600, received_at=now)
-
- fired = []
- import meshai.central.satpass_handler as sh
- monkeypatch.setattr(sh, "consolidate_satpass_pending",
- lambda c: fired.append(c))
-
- consumer = _make_consumer(bus=None)
-
- async def _main():
- consumer._sweep_pending_satpass(now=now)
- await asyncio.sleep(0.3)
-
- _run(_main())
-
- assert cid not in fired, "future row fired immediately"
- assert cid in consumer._pending_satpass_timers, "future row was not armed"
- # Pending row untouched (still awaiting its future fire).
- conn = get_db()
- remaining = conn.execute(
- "SELECT COUNT(*) AS n FROM satpass_pending WHERE consolidated_id=?",
- (cid,)).fetchone()["n"]
- assert remaining == 1
-
-
-# ── sweep does not double-schedule an already-armed cid ──────────────
-
-def test_sweep_does_not_double_schedule(monkeypatch):
- """A cid already armed by the live path is skipped by the sweep."""
- _enable_satpass_db(norad_ids=[25544], dry_run=True)
- now = int(time.time())
- cid = "25544:ARMED"
- _insert_pending(cid, due_at=now - 10, received_at=now - 15)
-
- consumer = _make_consumer(bus=None)
-
- fired = []
- import meshai.central.satpass_handler as sh
- monkeypatch.setattr(sh, "consolidate_satpass_pending",
- lambda c: fired.append(c))
-
- async def _main():
- sentinel = object()
- consumer._pending_satpass_timers[cid] = sentinel # live path owns it
- consumer._sweep_pending_satpass(now=now)
- # The sweep must not have replaced the live handle.
- assert consumer._pending_satpass_timers[cid] is sentinel
- await asyncio.sleep(0.1)
-
- _run(_main())
- assert cid not in fired, "sweep double-scheduled an already-armed cid"
diff --git a/work/tests/test_satpass_registration.py b/work/tests/test_satpass_registration.py
index e652ed6..1d3464d 100644
--- a/work/tests/test_satpass_registration.py
+++ b/work/tests/test_satpass_registration.py
@@ -43,28 +43,6 @@ def test_environmental_satpass_default_central():
assert env.satpass.feed_source == "central"
-# -- _subject_owned() integration ---------------------------------------------
-
-def test_subject_owned_includes_satpass_subjects():
- """When EnvironmentalConfig has satpass with feed_source='central',
- _subject_owned() must return subjects containing 'central.sat.'."""
- from meshai.config import EnvironmentalConfig
- from meshai.central.consumer import _SUBJECTS_BARE
-
- env = EnvironmentalConfig()
-
- # Simulate what _subject_owned does for the satpass attr
- cfg = getattr(env, "satpass", None)
- assert cfg is not None, "satpass attr missing from EnvironmentalConfig"
- assert getattr(cfg, "feed_source", "native") == "central"
-
- # Verify _SUBJECTS_BARE has satpass entry
- assert "satpass" in _SUBJECTS_BARE, "satpass missing from _SUBJECTS_BARE"
- subjects = _SUBJECTS_BARE["satpass"]
- assert any("central.sat.pass" in s for s in subjects)
- assert any("central.sat.tle" in s for s in subjects)
-
-
# -- adapter_config REGISTRY --------------------------------------------------
def test_registry_has_satpass_enabled():
@@ -116,32 +94,3 @@ def test_yaml_parsing_satpass():
assert env.satpass.enabled is True
assert env.satpass.feed_source == "central"
-
-
-# -- _subjects_for() region rewrite table ------------------------------------
-
-def test_subjects_for_satpass_with_region():
- """_subjects_for('satpass', 'us.id') must return region-scoped pass
- subjects and global TLE subject."""
- from meshai.central.consumer import _subjects_for
- result = _subjects_for('satpass', 'us.id')
- assert len(result) == 2, f'Expected 2 subjects, got {len(result)}: {result}'
- assert result[0] == 'central.sat.pass.us.id.>'
- assert result[1] == 'central.sat.tle.>'
-
-
-def test_subjects_for_satpass_no_region_falls_back():
- """_subjects_for('satpass', None) must return bare-wildcard forms
- from _SUBJECTS_BARE."""
- from meshai.central.consumer import _subjects_for
- result = _subjects_for('satpass', None)
- assert len(result) == 2, f'Expected 2 subjects, got {len(result)}: {result}'
- assert result[0] == 'central.sat.pass.>'
- assert result[1] == 'central.sat.tle.>'
-
-
-def test_subjects_for_satpass_empty_region_falls_back():
- """_subjects_for('satpass', '') must behave like None — bare wildcards."""
- from meshai.central.consumer import _subjects_for
- result = _subjects_for('satpass', '')
- assert result == _subjects_for('satpass', None)
diff --git a/work/tests/test_satpass_wire_fields.py b/work/tests/test_satpass_wire_fields.py
index 9467fcb..5bc5fd0 100644
--- a/work/tests/test_satpass_wire_fields.py
+++ b/work/tests/test_satpass_wire_fields.py
@@ -260,30 +260,3 @@ def test_observer_fallback_to_slug():
).fetchone()
assert row is not None
assert row["observer"] == "filer"
-
-
-
-# ── Consumer category mapping ──────────────────────────────────────
-
-def test_category_map_pass_prefix():
- """pass.n2yo_visualpasses must map to sat_pass, not other."""
- from meshai.central.consumer import map_category
- assert map_category("pass.n2yo_visualpasses") == "sat_pass"
-
-
-def test_category_map_pass_satpass_predict():
- """pass.satpass_predict must map to sat_pass."""
- from meshai.central.consumer import map_category
- assert map_category("pass.satpass_predict") == "sat_pass"
-
-
-def test_category_map_sat_prefix_still_works():
- """sat.pass must still map to sat_pass (backward compat)."""
- from meshai.central.consumer import map_category
- assert map_category("sat.pass") == "sat_pass"
-
-
-def test_subject_domain_sat_fallback():
- """Subject central.sat.pass.* must map to sat_pass via domain fallback."""
- from meshai.central.consumer import category_from_subject
- assert category_from_subject("central.sat.pass.us.id.filer") == "sat_pass"
diff --git a/work/tests/test_seismic_v057.py b/work/tests/test_seismic_v057.py
deleted file mode 100644
index e6f5acd..0000000
--- a/work/tests/test_seismic_v057.py
+++ /dev/null
@@ -1,199 +0,0 @@
-"""v0.5.7-seismic: USGS quake NATS pattern + severity clamp + categories audit.
-
-Covers three things shipped in v0.5.7-seismic:
-
-1. USGS quake subject pattern -- per Central v0.10.0 guide §usgs_quake the
- pattern is `central.quake.event.` (4 tokens, NO region). Pre-v0.5.7
- we shipped `central.quake.event.>.us.id` which is invalid NATS (`>`
- mid-subject) AND wouldn't have matched anything Central publishes.
-2. Severity clamp -- documents/regression-tests the existing `map_severity`
- behavior. The v0.5.7-seismic prompt described a "severity=5 great-quake
- IndexError / drop" bug; investigation confirmed that bug does NOT exist:
- - map_severity already clamps any int >= 3 to "immediate"
- (so severity=5, 99, etc. all map safely).
- - NotificationToggle.severity_channels is dict-keyed by severity STRING
- ({"routine","priority","immediate"}), not int -- IndexError is
- structurally impossible from this boundary.
- - Per the guide §5b severity vocabulary is documented as 0-4 only;
- severity=5 is not in Central's contract. The clamp is defensive
- padding against contract drift.
- These tests pin the clamp so a future regression doesn't introduce the
- bug Matt was guarding against.
-3. ALERT_CATEGORIES seismic-family audit -- earthquake_event was MISSING
- from the registry. Native usgs_quake.py emits it and the central path
- maps every quake.event. to it via map_category, but the
- Advanced Rules editor couldn't select it (it fell through to
- get_category's mesh_health default). Added in v0.5.7-seismic. The
- hydro entries (stream_flood_warning / stream_high_water under
- toggle='seismic' from v0.5.2) are out of scope; this audit only adds
- the quake side and verifies hydro toggles are unchanged.
-"""
-
-import inspect
-import json
-import re
-
-import pytest
-
-from meshai.central.consumer import (
- CentralConsumer,
- _SUBJECTS_BARE,
- _subjects_for,
- map_category,
- map_severity,
-)
-from meshai.config import EnvironmentalConfig
-from meshai.notifications.categories import ALERT_CATEGORIES
-from meshai.notifications.pipeline.bus import EventBus
-
-
-def _assert_legal_nats(subject: str) -> None:
- tokens = subject.split(".")
- if ">" in tokens:
- assert tokens[-1] == ">", f"`>` not at tail in {subject!r}"
- assert tokens.count(">") == 1, f"multiple `>` in {subject!r}"
- for tok in tokens:
- assert tok, f"empty token in {subject!r}"
- if tok not in {"*", ">"}:
- assert "*" not in tok and ">" not in tok, f"mixed wildcard in token {tok!r}"
-
-
-# ---------- FIX 1: USGS quake subject pattern -----------------------------
-
-
-def test_usgs_quake_subject_uses_tail_only_wildcard():
- """Per Central v0.10.0 guide §usgs_quake: `central.quake.event.`,
- 4 tokens, no region. Tail-only `>` is the legal wildcard form."""
- subs = _subjects_for("usgs_quake", "us.id")
- assert subs == ["central.quake.event.>"]
- for s in subs:
- _assert_legal_nats(s)
-
-
-def test_usgs_quake_subject_has_no_mid_subject_wildcard():
- """Belt-and-braces NATS-syntax check."""
- for s in _subjects_for("usgs_quake", "us.id"):
- tokens = s.split(".")
- for tok in tokens[:-1]:
- assert tok != ">", f"`>` mid-subject in {s!r}"
-
-
-def test_usgs_quake_bare_form_unchanged():
- """Empty region falls back to the broader bare wildcard for backward compat."""
- assert _subjects_for("usgs_quake", "") == ["central.quake.>"]
-
-
-# ---------- FIX 2: severity clamp regression guard ------------------------
-
-
-@pytest.mark.parametrize("sev,expected", [
- (0, "routine"),
- (1, "routine"),
- (2, "priority"),
- (3, "immediate"),
- (4, "immediate"),
- # v0.5.7-seismic regression guard: hypothetical "great quake" severity=5
- # (not in the Central v0.10.0 contract, but defensible if it ever appears)
- # MUST clamp to "immediate", not raise / not drop.
- (5, "immediate"),
- (10, "immediate"),
- (99, "immediate"),
- # Edge cases that previously degraded to "routine".
- (None, "routine"),
- ("nonsense", "routine"),
- (-1, "routine"),
-])
-def test_map_severity_handles_full_range(sev, expected):
- assert map_severity(sev) == expected
-
-
-def test_severity_5_quake_routes_through_consumer_without_crashing():
- """Inject a synthetic Central quake envelope with severity=5 (out-of-
- contract great-quake hypothetical) and verify it normalizes cleanly
- into an Event with severity='immediate' -- no IndexError, no drop."""
- rec = []
- bus = EventBus(); bus.subscribe(rec.append)
- c = CentralConsumer(EnvironmentalConfig(), bus)
- env = {"id": "us8000mc12", "data": {
- "id": "us8000mc12", "adapter": "usgs_quake",
- "category": "quake.event.great",
- "time": "2026-05-19T02:50:39+00:00",
- "severity": 5, # the out-of-contract value
- "geo": {"centroid": [-148.93, 61.32], "primary_region": "US-AK", "regions": ["US-AK"]},
- "data": {"title": "M 8.2 - 23 km ESE of Anchorage, AK",
- "magnitude": 8.2, "depth": 32.0, "magType": "mw",
- "alert": "red", "tsunami": 1, "type": "earthquake"}}}
- ev = c._handle("central.quake.event.great", json.dumps(env).encode())
- assert ev is not None
- assert ev.severity == "immediate"
- assert ev.category == "earthquake_event"
- assert ev.source == "usgs_quake"
- assert len(rec) == 1
-
-
-def test_severity_channels_is_string_keyed_no_int_indexerror_risk():
- """The shape that would make severity=5 dangerous is an int-indexed
- list; ours is a dict keyed by severity STRING. This pins that contract
- so a refactor can't quietly introduce the IndexError vector."""
- from meshai.config import NotificationToggle
- t = NotificationToggle(name="seismic")
- assert isinstance(t.severity_channels, dict)
- # dict.get with an unknown key returns None / default, never raises.
- assert t.severity_channels.get("any_string", []) == []
-
-
-# ---------- FIX 3: seismic-family categories audit ------------------------
-
-
-def test_earthquake_event_in_registry():
- """v0.5.7-seismic: registry now has earthquake_event so the Advanced
- Rules editor can target it. Pre-v0.5.7-seismic it was missing entirely
- and fell through to the mesh_health default via get_category()."""
- assert "earthquake_event" in ALERT_CATEGORIES
- assert ALERT_CATEGORIES["earthquake_event"]["toggle"] == "seismic"
-
-
-def test_hydro_entries_still_seismic_toggle():
- """The v0.5.2 USGS-water migration to toggle='seismic' (geohazards
- family in the GUI) must survive the v0.5.7-seismic edit. Out of scope
- for THIS phase to modify; in scope to verify-unchanged."""
- assert ALERT_CATEGORIES["stream_flood_warning"]["toggle"] == "seismic"
- assert ALERT_CATEGORIES["stream_high_water"]["toggle"] == "seismic"
-
-
-def _native_emitted_quake_categories() -> set[str]:
- """Walk usgs_quake.py for category= literals routing to toggle=seismic."""
- from meshai.env import usgs_quake as quake_mod
- src = inspect.getsource(quake_mod)
- emitted = set(re.findall(r'category="([a-z_]+)"', src))
- return {c for c in emitted if c in ALERT_CATEGORIES
- and ALERT_CATEGORIES[c].get("toggle") == "seismic"}
-
-
-def _central_path_quake_categories() -> set[str]:
- central_inputs = [
- "quake.event.minor", "quake.event.light", "quake.event.moderate",
- "quake.event.strong", "quake.event.major", "quake.event.great",
- ]
- return {map_category(c) for c in central_inputs}
-
-
-def test_alert_categories_quake_complete():
- """Every quake-side category that meshai emits (native or central path)
- must have an ALERT_CATEGORIES entry under toggle='seismic'. Hydro
- entries are out of scope for this audit but kept as a control."""
- native = _native_emitted_quake_categories()
- central = _central_path_quake_categories()
- emitted = native | central
- # All six tiers should fold to earthquake_event via the central path.
- assert emitted == {"earthquake_event"}, f"unexpected quake emit set: {emitted}"
- assert "earthquake_event" in ALERT_CATEGORIES
-
-
-def test_seismic_family_required_fields():
- info = ALERT_CATEGORIES["earthquake_event"]
- assert info["toggle"] == "seismic"
- assert info["name"]
- assert info["description"]
- assert info["default_severity"] in {"routine", "priority", "immediate"}
- assert info["example_message"]
diff --git a/work/tests/test_swpc_handler.py b/work/tests/test_swpc_handler.py
deleted file mode 100644
index 5ea0d65..0000000
--- a/work/tests/test_swpc_handler.py
+++ /dev/null
@@ -1,228 +0,0 @@
-"""Tests for v0.5.10 SWPC space-weather handler."""
-import pytest
-
-from meshai.central.swpc_handler import handle_swpc
-from meshai.persistence import close_thread_connection, init_db
-from meshai.persistence import db as persistence_db
-
-
-@pytest.fixture
-def mem_db(monkeypatch, tmp_path):
- db_path = str(tmp_path / "swpc-test.sqlite")
- monkeypatch.setenv("MESHAI_DB_PATH", db_path)
- persistence_db._initialised.clear()
- close_thread_connection()
- conn = init_db()
- # Clear module-level geomag dedup caches between tests.
- # Phase-1: _geomag_recent moved to gating.swpc._geomag_window.
- from meshai.central import swpc_handler as _swpc_mod
- if hasattr(_swpc_mod, '_geomag_recent'):
- _swpc_mod._geomag_recent.clear()
- from meshai.notifications.gating import swpc as _swpc_gate
- _swpc_gate._geomag_window.clear()
- yield conn
- close_thread_connection()
- persistence_db._initialised.discard(db_path)
-
-
-def _kindex_env(*, kp=3.0, event_id="kp_2026_06_05_15Z"):
- return {
- "id": event_id, "subject": "central.space.kindex",
- "data": {
- "id": event_id, "adapter": "swpc_kindex",
- "category": "space.kindex", "severity": 0,
- "geo": {},
- "data": {"id": event_id, "kp_index": kp,
- "time": "2026-06-05T15:00:00Z"},
- },
- }
-
-
-def _protons_env(*, flux=1.0, event_id="p_2026_06_05_15Z"):
- return {
- "id": event_id, "subject": "central.space.proton_flux",
- "data": {
- "id": event_id, "adapter": "swpc_protons",
- "category": "space.proton_flux", "severity": 0,
- "geo": {},
- "data": {"id": event_id, "p10mev": flux,
- "time": "2026-06-05T15:00:00Z"},
- },
- }
-
-
-def _alert_env(*, flare_class=None, kp=None, pfu=None,
- event_id="alert_001", product_id="ALTPRO"):
- d = {"id": event_id, "product_id": product_id,
- "time": "2026-06-05T15:00:00Z"}
- if flare_class: d["flare_class"] = flare_class
- if kp: d["kp_index"] = kp
- if pfu: d["p10mev"] = pfu
- return {
- "id": event_id, "subject": "central.space.alert.xrayflare",
- "data": {
- "id": event_id, "adapter": "swpc_alerts",
- "category": "space.alert", "severity": 1,
- "geo": {}, "data": d,
- },
- }
-
-
-def _commit(data, t):
- cb = data.get("_on_broadcast_committed")
- if cb is not None:
- cb(float(t))
-
-
-# ---- geomagnetic storm ----
-
-
-def test_kp_below_7_skipped(mem_db):
- env = _kindex_env(kp=4.0, event_id="kp_low")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is None
- # Row persisted for trending, not broadcast.
- row = mem_db.execute(
- "SELECT last_broadcast_at FROM swpc_events WHERE event_id='kp_low'"
- ).fetchone()
- assert row is not None
- assert row["last_broadcast_at"] is None
-
-
-def test_kp7_g3_broadcasts(mem_db):
- env = _kindex_env(kp=7.0, event_id="kp_g3")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert wire.startswith("🧲")
- assert "G3" in wire
- assert "Kp7" in wire
- assert "Geomagnetic Storm" in wire
-
-
-def test_kp9_g5_broadcasts_with_extreme_label(mem_db):
- env = _kindex_env(kp=9.0, event_id="kp_g5")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert "G5" in wire
- assert "Kp9" in wire
-
-
-# ---- solar flares ----
-
-
-def test_m_class_flare_skipped(mem_db):
- env = _alert_env(flare_class="M5.5", event_id="m55_flare")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is None
-
-
-def test_x1_flare_r3_broadcasts(mem_db):
- env = _alert_env(flare_class="X1.2", event_id="x1_flare")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert wire.startswith("☀️")
- assert "R3" in wire
- assert "X1.2" in wire
-
-
-def test_x10_flare_r4_broadcasts(mem_db):
- env = _alert_env(flare_class="X10", event_id="x10_flare")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert "R4" in wire or "R5" in wire
-
-
-def test_flare_class_in_product_id(mem_db):
- """Some swpc_alerts encode the class in product_id rather than flare_class."""
- env = _alert_env(event_id="prod_id_flare", product_id="X2.1 FLARE EVENT")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert "R3" in wire
-
-
-# ---- proton events ----
-
-
-def test_proton_below_threshold_skipped(mem_db):
- env = _protons_env(flux=0.5, event_id="p_low")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is None
- row = mem_db.execute(
- "SELECT last_broadcast_at FROM swpc_events WHERE event_id='p_low'"
- ).fetchone()
- assert row is not None
- assert row["last_broadcast_at"] is None
-
-
-def test_proton_s1_threshold_broadcasts(mem_db):
- env = _protons_env(flux=15, event_id="p_s1")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert wire.startswith("☢️")
- assert "S1" in wire
-
-
-def test_proton_s2_broadcasts(mem_db):
- env = _protons_env(flux=200, event_id="p_s2")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is not None
- assert "S2" in wire
-
-
-# ---- wire format ----
-
-
-def test_wire_has_scale_code_and_scalar_tail(mem_db):
- env = _kindex_env(kp=7.0, event_id="fmt1")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- # Wire format: "🧲 New: G3 Geomagnetic Storm — Kp7\nHF degraded, ..."
- assert "G3" in wire
- assert "Kp7" in wire
- assert "\n" in wire
-
-
-# ---- per-event dedup ----
-
-
-def test_per_event_dedup_no_reissue(mem_db):
- env = _kindex_env(kp=7.0, event_id="dedup_kp")
- data1 = {}
- handle_swpc(env, env["subject"], data=data1, now=1_000_000)
- _commit(data1, 1_000_001)
- # Re-publish with same id and same Kp -- should not re-broadcast.
- wire2 = handle_swpc(env, env["subject"], data={}, now=1_000_300)
- assert wire2 is None
-
-
-# ---- commit callback ----
-
-
-def test_commit_callback_updates_last_broadcast(mem_db):
- env = _kindex_env(kp=7.0, event_id="cb_swpc")
- data = {}
- handle_swpc(env, env["subject"], data=data, now=1_000_000)
- pre = mem_db.execute(
- "SELECT last_broadcast_at FROM swpc_events WHERE event_id='cb_swpc'"
- ).fetchone()
- assert pre["last_broadcast_at"] is None
- _commit(data, 1_000_001)
- post = mem_db.execute(
- "SELECT last_broadcast_at FROM swpc_events WHERE event_id='cb_swpc'"
- ).fetchone()
- assert post["last_broadcast_at"] == 1_000_001
-
-
-# ---- routine readings persist but never broadcast ----
-
-
-def test_routine_kp_reading_persists_no_broadcast(mem_db):
- """Sub-G3 Kp must still be saved for trending queries."""
- env = _kindex_env(kp=4.5, event_id="routine_kp")
- wire = handle_swpc(env, env["subject"], data={}, now=1_000_000)
- assert wire is None
- row = mem_db.execute(
- "SELECT event_type, payload_json FROM swpc_events "
- "WHERE event_id='routine_kp'").fetchone()
- assert row is not None
- assert row["event_type"] == "swpc_kindex"
- assert "kp_index" in row["payload_json"]
diff --git a/work/tests/test_swpc_refactor.py b/work/tests/test_swpc_refactor.py
index e59b0a0..0d21f3c 100644
--- a/work/tests/test_swpc_refactor.py
+++ b/work/tests/test_swpc_refactor.py
@@ -1,9 +1,20 @@
"""Phase-1 SWPC refactor tests.
-Six test groups:
+The Central `swpc_handler` module (`_render()`, `handle_swpc()`) has been
+deleted — the native path is the only production path now. Pure old-vs-new
+parity assertions, the flare_class-string-parsing tests that only existed
+to exercise the deleted handler's Central-only mapping, and the
+`solar_radiation_storm`/proton "legacy path" test (which imported the now
+also-deleted `swpc_handler` -- proton events have no broadcast path at all
+post-excision, native or otherwise; flagged for Matt, not fixed here) have
+been removed. Original diffs are preserved in git history. What remains
+exercises native code directly (hand-written expected strings are kept as
+regression pins on the current wire format).
-1. Parity — for a kindex-style fixture and a flare fixture, the new formatter
- produces output equivalent to old _render() (noting tier-b severity fix).
+Five test groups:
+
+1. Parity — for a kindex-style fixture and a flare fixture, the formatter
+ produces the expected wire text (noting tier-b severity fix).
2. Cross-source identity — same Kp from swpc_kindex and swpc_alerts shares
the 600s geomag dedup window (committed broadcast suppresses the second).
@@ -108,20 +119,13 @@ def _commit(data: dict, t: float) -> None:
# ─────────────────────────────────────────────────────────────────────────────
class TestFormatterParity:
- """formatters/swpc.format() renders equivalent output to swpc_handler._render()."""
-
- def _render_old(self, event_kind: str, scale_code: str, label: str,
- scalar_str: str, *, detail: str = "", time_tag: str = "") -> str:
- from meshai.central.swpc_handler import _render
- return _render(event_kind, scale_code, label, scalar_str,
- is_update=False, detail=detail, time_tag=time_tag)
+ """formatters/swpc.format() renders the expected wire text from canonical data."""
def test_kindex_g3_parity(self, mem_db):
- """Kp=7 (G3) kindex envelope → new formatter ≈ old _render.
+ """Kp=7 (G3) kindex envelope → formatter output matches hand-written expected.
- Tier-b note: the only intentional delta is _severity_override (now
- "priority" instead of missing/routine), which does NOT affect the
- wire text — parity is exact for the text body.
+ Tier-b note: _severity_override (now "priority" instead of
+ missing/routine) does NOT affect the wire text.
"""
from meshai.notifications.formatters.swpc import format as sfmt
@@ -135,29 +139,24 @@ class TestFormatterParity:
"issued_at": "2026-07-04T05:00:00Z",
}
- old_wire = self._render_old(
- "geomag", "G3", "strong", "Kp7",
- detail="HF degraded, aurora possible",
- time_tag="2026-07-04 05:00",
+ expected = (
+ "🧲 New: G3 Geomagnetic Storm — Kp7"
+ "\nHF degraded, aurora possible"
+ "\nSWPC · 2026-07-04 05:00"
)
with pinned_time(_AT):
new_wire = sfmt(_make_fake_event(canonical), now=_AT, budget=140)
- # Content must match: same line 1 and line 2.
assert "G3" in new_wire, f"scale_code missing from wire: {new_wire!r}"
assert "Kp7" in new_wire, f"scalar 'Kp7' missing from wire: {new_wire!r}"
assert "Geomagnetic Storm" in new_wire
-
- # Old wire content also present
- assert "G3" in old_wire
- assert "Kp7" in old_wire
- assert new_wire == old_wire, (
- f"Parity failure for G3/Kp7:\n old: {old_wire!r}\n new: {new_wire!r}"
+ assert new_wire == expected, (
+ f"Wire mismatch for G3/Kp7:\n expected: {expected!r}\n got: {new_wire!r}"
)
def test_flare_x1_r3_parity(self, mem_db):
- """X1.0 flare (R3) alert → new formatter ≈ old _render.
+ """X1.0 flare (R3) alert → formatter output matches hand-written expected.
Fixture mirrors swpc_last/0003.json (XX0S, X1.0 flare, R3 Strong).
"""
@@ -172,10 +171,10 @@ class TestFormatterParity:
"issued_at": "2026-06-03T11:59:00Z",
}
- old_wire = self._render_old(
- "flare", "R3", "strong", "X1.0",
- detail="HF radio fading, GPS may glitch",
- time_tag="2026-06-03 11:59",
+ expected = (
+ "☀️ New: X1.0 Solar Flare — R3"
+ "\nHF radio fading, GPS may glitch"
+ "\nSWPC · 2026-06-03 11:59"
)
with pinned_time(_AT):
@@ -184,8 +183,8 @@ class TestFormatterParity:
assert "R3" in new_wire
assert "X1.0" in new_wire
assert "Solar Flare" in new_wire
- assert new_wire == old_wire, (
- f"Parity failure for X1.0/R3:\n old: {old_wire!r}\n new: {new_wire!r}"
+ assert new_wire == expected, (
+ f"Wire mismatch for X1.0/R3:\n expected: {expected!r}\n got: {new_wire!r}"
)
def test_g5_kp9_parity(self, mem_db):
@@ -201,10 +200,10 @@ class TestFormatterParity:
"issued_at": "2026-07-04T08:00:00Z",
}
- old_wire = self._render_old(
- "geomag", "G5", "extreme", "Kp9",
- detail="Widespread power disruptions possible",
- time_tag="2026-07-04 08:00",
+ expected = (
+ "🧲 New: G5 Geomagnetic Storm — Kp9"
+ "\nWidespread power disruptions possible"
+ "\nSWPC · 2026-07-04 08:00"
)
with pinned_time(_AT):
@@ -212,8 +211,8 @@ class TestFormatterParity:
assert "G5" in new_wire
assert "Kp9" in new_wire
- assert new_wire == old_wire, (
- f"G5 parity failure:\n old: {old_wire!r}\n new: {new_wire!r}"
+ assert new_wire == expected, (
+ f"G5 wire mismatch:\n expected: {expected!r}\n got: {new_wire!r}"
)
def test_null_scalar_renders_without_dash_tail(self, mem_db):
@@ -444,48 +443,6 @@ class TestFlareRScaleFloor:
assert gate.broadcast, "R5 must broadcast"
assert gate.data_patch.get("_severity_override") == "immediate"
- def test_m5_flare_suppressed_via_handler(self, mem_db):
- """M5.5 flare maps to R2 via old path → new arch suppresses at R2 floor."""
- from meshai.central.swpc_handler import handle_swpc
-
- env = {
- "id": "m55_new_arch",
- "subject": "central.space.alert.m55",
- "data": {
- "id": "m55_new_arch",
- "adapter": "swpc_alerts",
- "category": "space.alert",
- "severity": 0,
- "geo": {},
- "data": {"id": "m55_new_arch", "flare_class": "M5.5",
- "time": "2026-07-04T06:00:00Z"},
- },
- }
- wire = handle_swpc(env, env["subject"], data={}, now=int(_AT))
- assert wire is None, "M5.5 (R2) must be suppressed"
-
- def test_x1_flare_broadcasts_via_handler(self, mem_db):
- """X1.0 flare maps to R3 → broadcasts via new arch."""
- from meshai.central.swpc_handler import handle_swpc
-
- env = {
- "id": "x10_new_arch",
- "subject": "central.space.alert.x10",
- "data": {
- "id": "x10_new_arch",
- "adapter": "swpc_alerts",
- "category": "space.alert",
- "severity": 0,
- "geo": {},
- "data": {"id": "x10_new_arch", "flare_class": "X1.0",
- "time": "2026-07-04T06:00:00Z"},
- },
- }
- wire = handle_swpc(env, env["subject"], data={}, now=int(_AT))
- assert wire is not None, "X1.0 (R3) must broadcast"
- assert "R3" in wire
- assert "X1.0" in wire
-
# ─────────────────────────────────────────────────────────────────────────────
# 5. Schema conformance — to_event() emits required canonical fields
@@ -646,52 +603,3 @@ class TestProtonNotRegistered:
assert "rf_propagation_alert" in DECIDERS, (
"rf_propagation_alert must be in DECIDERS"
)
-
- def test_proton_stays_on_legacy_path(self):
- """Proton events (S1+) still broadcast via legacy path in swpc_handler.
-
- Uses swpc_protons adapter with 15 pfu (S1 threshold). The legacy path
- must still work — no regression from the new arch changes.
- """
- import pytest
- pytest.importorskip("meshai.central.swpc_handler")
-
- # This test needs a DB fixture — create one inline
- import tempfile, os
- from meshai.persistence import close_thread_connection, init_db
- from meshai.persistence import db as persistence_db
-
- with tempfile.TemporaryDirectory() as tmp:
- db_path = os.path.join(tmp, "proton-test.sqlite")
- old_env = os.environ.get("MESHAI_DB_PATH")
- os.environ["MESHAI_DB_PATH"] = db_path
- persistence_db._initialised.clear()
- close_thread_connection()
- try:
- init_db()
- from meshai.central.swpc_handler import handle_swpc
-
- env = {
- "id": "p_s1_legacy",
- "subject": "central.space.proton_flux",
- "data": {
- "id": "p_s1_legacy",
- "adapter": "swpc_protons",
- "category": "space.proton_flux",
- "severity": 0,
- "geo": {},
- "data": {"id": "p_s1_legacy", "p10mev": 15.0,
- "time": "2026-07-04T06:00:00Z"},
- },
- }
- wire = handle_swpc(env, env["subject"], data={}, now=int(_AT))
- assert wire is not None, "S1 proton must still broadcast via legacy path"
- assert "S1" in wire
- assert "☢️" in wire
- finally:
- close_thread_connection()
- persistence_db._initialised.discard(db_path)
- if old_env is None:
- os.environ.pop("MESHAI_DB_PATH", None)
- else:
- os.environ["MESHAI_DB_PATH"] = old_env
diff --git a/work/tests/test_tail_followups.py b/work/tests/test_tail_followups.py
index 86f64bd..974bd71 100644
--- a/work/tests/test_tail_followups.py
+++ b/work/tests/test_tail_followups.py
@@ -334,14 +334,3 @@ def test_reminder_fires_when_fire_not_tombstoned():
import asyncio
fired = asyncio.run(sch.tick_once())
assert fired == 1
-
-
-# ============================================================================
-# Item 5 -- dead-code removal
-# ============================================================================
-
-
-def test_incident_broadcast_heartbeat_constant_gone():
- """The dead constant is not importable anymore."""
- from meshai.central import incident_handler
- assert not hasattr(incident_handler, "INCIDENT_BROADCAST_HEARTBEAT_S")
diff --git a/work/tests/test_tracking_v057.py b/work/tests/test_tracking_v057.py
deleted file mode 100644
index 148a450..0000000
--- a/work/tests/test_tracking_v057.py
+++ /dev/null
@@ -1,189 +0,0 @@
-"""v0.5.7-tracking: Central tracking adapter check + categories audit.
-
-The tracking family is a PLACEHOLDER for Phase 7 (per
-meshai/notifications/categories.py:19 header comment: "tracking - ADS-B,
-AIS, satellite passes (Phase 7)"). As of v0.5.7-tracking it has:
-
- - "tracking" in VALID_TOGGLES (reserved toggle name)
- - dashboard-frontend/src/pages/Environment.tsx FAMILIES list entry with
- label="Tracking", icon=Satellite, adapters=['satpass'] (a pre-existing
- UI-only grouping of the native satpass adapter under the "Tracking"
- display section; satpass has its own "satpass" backend toggle and is
- NOT itself a Phase-7 tracking-family adapter -- the guard below allows
- only this one known entry and fails on anything else)
- - ZERO native adapter files in meshai/env/
- - ZERO ALERT_CATEGORIES entries with toggle="tracking"
- - ZERO Central wires (no central.tracking.* / central.aprs.* / etc.;
- no entries in _SUBJECTS_BARE; no entries in CENTRAL_ADAPTER_TO_SOURCE)
-
-This file pins all of those invariants as regression guards. The intent
-is that a future Phase 7 commit that flips any of these (e.g. adds an
-APRS adapter, wires a Central tracking subject) will fail these tests
-and FORCE the implementer to come back and complete the family-audit
-shape (registry entries with required fields, composer emoji/labels,
-test file refresh) the same way every other family in v0.5.7 was done.
-
-Central v0.10.0 cross-check
----------------------------
-The Central v0.10.0 guide (docs/CONSUMER-INTEGRATION.md at v0.10.0-itd-511)
-documents 22 per-adapter sections covering wx / fire / quake / space /
-disaster / traffic / hydro -- NONE are tracking-related. The producer
-source tree src/central/adapters/ contains 24 adapter files; NONE are
-named for any tracking concept (aprs / adsb / opensky / satellite /
-position). Subject prefixes used: central.{disaster,fire,fires,hydro,
-meta,models,quake,space,traffic,traffic_cameras,traffic_flow,wx}.> --
-no central.tracking.* / central.aprs.*.
-
-Same shape as v0.5.7-avalanche: no Central counterpart, native-only
-(currently native-empty).
-"""
-
-import os
-from pathlib import Path
-
-import pytest
-
-from meshai.central.consumer import (
- CENTRAL_ADAPTER_TO_SOURCE,
- CentralConsumer,
- _SUBJECTS_BARE,
- _subjects_for,
-)
-from meshai.config import EnvironmentalConfig
-from meshai.notifications.categories import ALERT_CATEGORIES, VALID_TOGGLES
-
-
-# ---------- FIX 1: Central has no tracking adapter -----------------------
-
-
-def test_central_has_no_tracking_subject_prefix():
- """No Central stream/subject namespace uses a tracking-style prefix."""
- for adapter, subs in _SUBJECTS_BARE.items():
- for s in subs:
- assert "tracking" not in s.lower(), \
- f"unexpected tracking subject for adapter {adapter}: {s!r}"
- for needle in ("aprs", "adsb", "opensky", "ads_b"):
- assert needle not in s.lower(), \
- f"unexpected {needle!r} subject for adapter {adapter}: {s!r}"
-
-
-def test_central_adapter_remap_has_no_tracking_entries():
- """No Central adapter name remaps to a tracking source on either side."""
- for src_name, mesh_name in CENTRAL_ADAPTER_TO_SOURCE.items():
- for needle in ("tracking", "aprs", "adsb", "opensky"):
- assert needle not in src_name.lower(), \
- f"unexpected Central adapter name: {src_name}"
- assert needle not in mesh_name.lower(), \
- f"unexpected meshai source name: {mesh_name}"
-
-
-def test_tracking_source_is_unknown_to_subjects_for():
- """Asking the consumer for tracking-source subjects returns empty for
- every region -- the source isn't in the table at all."""
- for region in ("us.id", "us.mt", "", None):
- assert _subjects_for("tracking", region) == [], \
- f"_subjects_for('tracking', {region!r}) should be []"
- assert _subjects_for("aprs", region) == []
- assert _subjects_for("adsb", region) == []
-
-
-# ---------- meshai-side placeholder invariants ---------------------------
-
-
-def test_tracking_toggle_is_reserved_in_valid_toggles():
- """The toggle name 'tracking' is reserved (placeholder for Phase 7)
- even though no categories use it yet."""
- assert "tracking" in VALID_TOGGLES
-
-
-def test_alert_categories_has_zero_tracking_entries():
- """v0.5.7-tracking placeholder check: registry has no toggle='tracking'
- entries. If Phase 7 lands, this test should be updated alongside the
- new entries -- not silently deleted."""
- tracking_entries = [
- cid for cid, info in ALERT_CATEGORIES.items()
- if info.get("toggle") == "tracking"
- ]
- assert tracking_entries == [], \
- f"unexpected tracking-family entries: {tracking_entries}"
-
-
-def test_no_native_tracking_adapter_files():
- """meshai/env/ has no tracking-related adapter files. Phase 7 will
- add at least one (likely aprs.py / adsb.py / opensky.py); when it
- does, this test should be updated to point at the new adapter."""
- env_dir = Path("meshai/env")
- if not env_dir.is_dir():
- pytest.skip("meshai/env not present in this working tree")
- files = {p.name for p in env_dir.iterdir() if p.suffix == ".py"}
- for needle in ("aprs", "adsb", "ads_b", "opensky", "tracking", "satellite"):
- for fname in files:
- assert needle not in fname.lower(), \
- f"unexpected env adapter file {fname!r} hints at a tracking adapter"
-
-
-# ---------- frontend placeholder invariant -------------------------------
-
-
-def test_environment_tsx_tracking_family_has_only_the_satpass_preview():
- """Environment.tsx FAMILIES entry for 'tracking' must have adapters
- limited to the pre-existing ['satpass'] preview grouping -- it must
- NOT gain any actual Phase-7 tracking adapter (aprs/adsb/opensky/etc).
-
- This guard originally required adapters=[] outright, but Environment.tsx
- has grouped the native satpass (SGP4) adapter under the "Tracking" UI
- section (icon: Satellite) since before this test's earliest visible
- history -- satellite-pass tracking is thematically "tracking" for
- display purposes, even though satpass has always had its OWN dedicated
- backend registry toggle ("satpass", see
- meshai/notifications/categories.py) rather than "tracking". All 7 other
- guards in this file (zero Central subjects, zero ALERT_CATEGORIES
- entries, zero native adapter files, etc.) confirm the backend-side
- Phase-7 tracking family genuinely has not landed; only this test's
- stricter-than-reality assumption about the frontend grouping was wrong.
- If Phase 7 lands for real, update this test together with the new
- ALERT_CATEGORIES entries + adapter files + composer glyphs.
- """
- tsx = Path("dashboard-frontend/src/pages/Environment.tsx")
- if not tsx.is_file():
- pytest.skip("Environment.tsx not present in this working tree")
- text = tsx.read_text()
- # Look for the FAMILIES line for tracking; the adapter list must be
- # limited to the known satpass preview entry.
- assert "key: 'tracking'" in text or 'key: "tracking"' in text, \
- "Environment.tsx FAMILIES is missing the tracking placeholder entry"
- # Pattern: `key: 'tracking', label: 'Tracking', icon: Satellite, adapters: [...]`
- # Accept any quote style + minor formatting tolerance.
- import re
- m = re.search(
- r"""key:\s*['"]tracking['"]\s*,\s*"""
- r"""label:\s*['"]Tracking['"]\s*,\s*"""
- r"""icon:\s*\w+\s*,\s*"""
- r"""adapters:\s*\[([^\]]*)\]""",
- text, re.DOTALL,
- )
- assert m, (
- "Environment.tsx FAMILIES tracking entry not found in the expected shape"
- )
- adapters = {a.strip().strip("'\"") for a in m.group(1).split(",") if a.strip()}
- assert adapters == {"satpass"}, (
- f"Environment.tsx tracking-family adapter list is {sorted(adapters)!r}, "
- "expected only the pre-existing {'satpass'} preview entry. "
- "If you're landing Phase 7, update this test together with the "
- "new ALERT_CATEGORIES entries + adapter files + composer glyphs."
- )
-
-
-# ---------- safety: no orphan routing into tracking ----------------------
-
-
-def test_no_prefix_fallback_routes_to_tracking():
- """The _TOGGLE_PREFIX_FALLBACK chain in categories.py has no rule that
- silently routes unknown categories to toggle='tracking'. Adding such
- a rule without paired registry entries would create orphan tracking
- events -- the v0.5.7 audit purity rule (every emitted = selectable)
- forbids this."""
- from meshai.notifications.categories import _TOGGLE_PREFIX_FALLBACK
- for prefix, toggle in _TOGGLE_PREFIX_FALLBACK:
- assert toggle != "tracking", \
- f"prefix-fallback {prefix!r} -> tracking without registry entries"
diff --git a/work/tests/test_traffic_v057.py b/work/tests/test_traffic_v057.py
deleted file mode 100644
index ae1e1d3..0000000
--- a/work/tests/test_traffic_v057.py
+++ /dev/null
@@ -1,177 +0,0 @@
-"""v0.5.7-traffic: NATS pattern fix + itd_511 sub-adapter routing + categories audit.
-
-Covers four things shipped in v0.5.7-traffic:
-
-1. NATS pattern syntax — `>` is legal only at the tail. Pre-v0.5.7-traffic
- we shipped `central.traffic.>.` (mid-subject `>`), invalid per
- NATS rules. Now: `central.traffic.*.` (Convention B, bare state)
- for traffic; roads511 dual-subscribes both Convention B and
- `central.traffic.*.us.` (Convention A, itd_511 form).
-2. roads511 dual subscription — owns both shared bare-state and us.
- subjects so itd_511 events route to the roads511 source in meshai.
-3. CENTRAL_ADAPTER_TO_SOURCE['itd_511'] == 'roads511'.
-4. ALERT_CATEGORIES roads-family parity — every category we can emit
- (native + central path post-map_category) has a registry entry.
-"""
-
-import inspect
-
-import pytest
-
-from meshai.central.consumer import (
- CENTRAL_ADAPTER_TO_SOURCE,
- _SUBJECTS_BARE,
- _subjects_for,
- map_category,
-)
-from meshai.notifications.categories import ALERT_CATEGORIES
-
-
-# ---------- NATS pattern validation (Convention A / B) ---------------------
-
-
-def _assert_legal_nats(subject: str) -> None:
- """Assert NATS multi-level wildcard `>` only appears at the tail token."""
- tokens = subject.split(".")
- if ">" in tokens:
- assert tokens[-1] == ">", f"`>` not at tail in {subject!r}"
- assert tokens.count(">") == 1, f"multiple `>` in {subject!r}"
- for tok in tokens:
- # `*` and `>` are wildcards; everything else must be a non-empty
- # token without further wildcard characters mixed in.
- assert tok, f"empty token in {subject!r}"
- if tok not in {"*", ">"}:
- assert "*" not in tok and ">" not in tok, f"mixed wildcard in token {tok!r}"
-
-
-def test_subjects_for_traffic_uses_convention_b():
- """traffic adapter -> bare-state Convention B; no `>` anywhere."""
- subs = _subjects_for("traffic", "us.id")
- assert subs == ["central.traffic.*.id"]
- for s in subs:
- _assert_legal_nats(s)
- assert ">" not in s, f"`>` in {s!r}"
-
-
-def test_subjects_for_roads511_dual_subscribes():
- """roads511 owns bare-state (shared with traffic) AND us. (itd_511)."""
- subs = _subjects_for("roads511", "us.id")
- assert subs == ["central.traffic.*.id", "central.traffic.*.us.id"]
- for s in subs:
- _assert_legal_nats(s)
- assert ">" not in s, f"`>` in {s!r}"
-
-
-def test_traffic_and_roads511_share_convention_b_subject():
- """The bare-state subject is shared so sub-adapter routing kicks in."""
- traffic_subs = set(_subjects_for("traffic", "us.id"))
- roads511_subs = set(_subjects_for("roads511", "us.id"))
- shared = traffic_subs & roads511_subs
- assert shared == {"central.traffic.*.id"}
-
-
-def test_no_invalid_mid_subject_wildcards_in_traffic_family():
- """Sanity sweep, scoped to this phase: traffic + roads511 region-aware
- subjects are NATS-legal (no `>` mid-subject). Other adapters (firms,
- usgs, usgs_quake, fires, nws) carry the v0.5.4 mid-`>` patterns and
- are intentionally OUT OF SCOPE for v0.5.7-traffic -- they'll be fixed
- per-family later in the v0.5.7 campaign."""
- for adapter in ("traffic", "roads511"):
- for s in _subjects_for(adapter, "us.id"):
- _assert_legal_nats(s)
- assert ">" not in s, f"`>` still present in {adapter} subject {s!r}"
-
-
-def test_bare_form_unchanged_when_region_empty():
- """Empty region returns _SUBJECTS_BARE for backward compat."""
- assert _subjects_for("traffic", "") == ["central.traffic.>"]
- assert _subjects_for("roads511", None) == ["central.traffic.>"]
-
-
-# ---------- itd_511 -> roads511 remap --------------------------------------
-
-
-def test_itd_511_remaps_to_roads511():
- assert CENTRAL_ADAPTER_TO_SOURCE.get("itd_511") == "roads511"
-
-
-def test_state_511_atis_still_remaps_to_roads511():
- """v0.5.3 mapping must survive the v0.5.7-traffic edit."""
- assert CENTRAL_ADAPTER_TO_SOURCE.get("state_511_atis") == "roads511"
-
-
-# ---------- map_category preserves event_type distinctions -----------------
-
-
-@pytest.mark.parametrize("central_cat,expected", [
- ("work_zone.wzdx", "work_zone"),
- ("work_zone", "work_zone"),
- ("incident.tomtom_incidents", "road_incident"),
- ("incident", "road_incident"),
- ("closure.itd_511", "road_closure"),
- ("closure", "road_closure"),
- # The catchall still flattens unknown traffic.* shapes.
- ("traffic.unknown_thing", "traffic_congestion"),
-])
-def test_map_category_traffic_event_types(central_cat, expected):
- assert map_category(central_cat) == expected
-
-
-# ---------- ALERT_CATEGORIES roads-family parity ---------------------------
-
-
-def _native_emitted_roads_categories() -> set[str]:
- """Walk traffic.py and roads511.py for category= literals."""
- import re
- from meshai.env import traffic as traffic_mod
- from meshai.env import roads511 as roads511_mod
- emitted: set[str] = set()
- for mod in (traffic_mod, roads511_mod):
- src = inspect.getsource(mod)
- emitted |= set(re.findall(r'category="([a-z_]+)"', src))
- return emitted
-
-
-def _central_path_roads_categories() -> set[str]:
- """Categories the central path can deliver into the roads family.
-
- Drives off map_category() so the test breaks if the routing changes.
- """
- central_inputs = [
- "work_zone.wzdx",
- "incident.tomtom_incidents",
- "closure.itd_511",
- "closure",
- "incident",
- "traffic.flow_slow",
- ]
- return {map_category(c) for c in central_inputs}
-
-
-def test_alert_categories_roads_complete():
- """Every category emitted by native traffic/roads511 OR delivered via
- the central path (post-map_category) must have an ALERT_CATEGORIES
- entry with toggle='roads'. No orphans.
- """
- registry_roads = {
- cid for cid, info in ALERT_CATEGORIES.items()
- if info.get("toggle") == "roads"
- }
- emitted = _native_emitted_roads_categories() | _central_path_roads_categories()
- missing = emitted - registry_roads
- orphans = registry_roads - emitted
- assert not missing, f"emit set has roads categories missing from ALERT_CATEGORIES: {missing}"
- assert not orphans, f"ALERT_CATEGORIES has orphan roads entries: {orphans}"
-
-
-@pytest.mark.parametrize(
- "cat",
- ["road_closure", "traffic_congestion", "work_zone", "road_incident"],
-)
-def test_roads_categories_have_required_fields(cat):
- info = ALERT_CATEGORIES[cat]
- assert info["toggle"] == "roads"
- assert info["name"]
- assert info["description"]
- assert info["default_severity"] in {"routine", "priority", "immediate"}
- assert info["example_message"]
diff --git a/work/tests/test_water_v057.py b/work/tests/test_water_v057.py
deleted file mode 100644
index 899e255..0000000
--- a/work/tests/test_water_v057.py
+++ /dev/null
@@ -1,197 +0,0 @@
-"""v0.5.7-water: USGS NWIS hydro NATS pattern + water/hydro categories audit.
-
-Covers two things shipped in v0.5.7-water:
-
-1. USGS NWIS hydro subject pattern -- per Central v0.10.0-itd-511 nwis.py
- producer subject_for() body, the actual published subject is
- `central.hydro....` where is
- `us.` (7 tokens) or `unknown` (6 tokens). The pre-v0.5.7-water
- `central.hydro.>.us.id` was invalid NATS (`>` mid-subject) -- replaced
- with three single-token `*` wildcards in the param/agency/site slots
- plus the bare region tail.
-
- Note on guide vs code: the Central guide §nwis text shows only the
- 4-token category-shape stem `central.hydro...
- ` without the regional suffix. That doc text is stale
- w.r.t. the producer code. The producer code is the ground truth (it's
- what NATS actually delivers); we follow the code.
-
-2. ALERT_CATEGORIES water/hydro audit -- pre-v0.5.7-water the registry had
- `stream_flood_warning` and `stream_high_water` (both toggle=seismic from
- the v0.5.2 geohazards migration). The central path's
- `("hydro.", "stream_flow")` _CATEGORY_MAP entry produced a category
- `stream_flow` that had no registry entry -- the rule editor couldn't
- target it. Added `stream_flow` (toggle=seismic) so central-delivered
- raw gauge readings are UI-selectable. The native usgs.py threshold-
- classified categories are unchanged.
-"""
-
-import inspect
-import re
-
-import pytest
-
-from meshai.central.consumer import (
- _SUBJECTS_BARE,
- _subjects_for,
- map_category,
- map_severity,
-)
-from meshai.notifications.categories import ALERT_CATEGORIES
-
-
-def _assert_legal_nats(subject: str) -> None:
- tokens = subject.split(".")
- if ">" in tokens:
- assert tokens[-1] == ">", f"`>` not at tail in {subject!r}"
- assert tokens.count(">") == 1, f"multiple `>` in {subject!r}"
- for tok in tokens:
- assert tok, f"empty token in {subject!r}"
- if tok not in {"*", ">"}:
- assert "*" not in tok and ">" not in tok, f"mixed wildcard in token {tok!r}"
-
-
-# ---------- FIX 1: USGS NWIS hydro subject pattern ------------------------
-
-
-def test_usgs_subjects_are_nats_legal():
- """No `>` mid-subject; all wildcards are single-token `*`."""
- subs = _subjects_for("usgs", "us.id")
- assert subs == [
- "central.hydro.*.*.*.us.id",
- "central.hydro.*.*.*.unknown",
- ]
- for s in subs:
- _assert_legal_nats(s)
- # Per-state filter has 7 tokens; .unknown has 6.
- assert ">" not in s, f"`>` should not appear in fixed-token form: {s!r}"
-
-
-def test_usgs_subjects_match_producer_published_shape():
- """Sanity: the subscription patterns match what nwis.py actually
- publishes. Producer publishes:
- central.hydro....
- where is us. (2 tokens) or unknown (1 token).
- """
- sub_state, sub_unknown = _subjects_for("usgs", "us.id")
- # Per-state form: matches a 7-token published subject.
- sample_published_state = "central.hydro.00060.usgs.06898000.us.id"
- sample_published_unknown = "central.hydro.00060.usgs.06898000.unknown"
- # Token-count check (NATS `*` matches exactly one token).
- assert len(sub_state.split(".")) == len(sample_published_state.split("."))
- assert len(sub_unknown.split(".")) == len(sample_published_unknown.split("."))
- # Per-state must end with the requested region, .unknown with literal.
- assert sub_state.endswith(".us.id")
- assert sub_unknown.endswith(".unknown")
-
-
-def test_usgs_bare_form_unchanged():
- """Empty region falls back to the bare wildcard (backward compat)."""
- assert _subjects_for("usgs", "") == ["central.hydro.>"]
- assert _subjects_for("usgs", None) == ["central.hydro.>"]
-
-
-def test_usgs_per_state_filter_does_not_match_wrong_state():
- """Sanity: a Montana-region subscription wouldn't match an Idaho subject.
- (Just verifies the substitution flows through cleanly per region.)"""
- mt_subs = _subjects_for("usgs", "us.mt")
- assert mt_subs == [
- "central.hydro.*.*.*.us.mt",
- "central.hydro.*.*.*.unknown",
- ]
-
-
-# ---------- FIX 2: ALERT_CATEGORIES water/hydro audit ---------------------
-
-
-def test_stream_flow_in_registry():
- """v0.5.7-water: central path's `hydro.* -> stream_flow` mapping now has
- a corresponding ALERT_CATEGORIES entry under toggle='seismic'."""
- assert "stream_flow" in ALERT_CATEGORIES
- assert ALERT_CATEGORIES["stream_flow"]["toggle"] == "seismic"
- assert ALERT_CATEGORIES["stream_flow"]["default_severity"] == "routine"
-
-
-def test_existing_hydro_entries_unchanged():
- """v0.5.2 USGS-water -> toggle='seismic' migration must survive."""
- for cat in ("stream_flood_warning", "stream_high_water"):
- assert cat in ALERT_CATEGORIES
- assert ALERT_CATEGORIES[cat]["toggle"] == "seismic"
-
-
-def _native_emitted_water_categories() -> set[str]:
- """Walk usgs.py for category= literals routing to toggle=seismic."""
- from meshai.env import usgs as usgs_mod
- src = inspect.getsource(usgs_mod)
- emitted = set(re.findall(r'category\s*=\s*"([a-z_]+)"', src))
- return {c for c in emitted if c in ALERT_CATEGORIES
- and ALERT_CATEGORIES[c].get("toggle") == "seismic"}
-
-
-def _central_path_water_categories() -> set[str]:
- """Map a representative set of central hydro category strings through
- map_category() to see what meshai categories we'd emit downstream.
- Per the guide §nwis, every NWIS event has category
- `hydro...`."""
- central_inputs = [
- "hydro.00060.usgs.06898000", # discharge
- "hydro.00065.usgs.06898000", # gage height
- "hydro.00010.usgs.06898000", # water temperature
- "hydro.00060.mo005.0000123", # cooperator agency
- ]
- return {map_category(c) for c in central_inputs}
-
-
-def test_alert_categories_water_complete():
- """Native + central-path water emit must equal registry's water-side
- subset of toggle='seismic'. (The quake-side earthquake_event added in
- v0.5.7-seismic is also under toggle='seismic' but emitted by a
- different adapter — exclude it from this water-only audit.)"""
- registry_water = {
- cid for cid, info in ALERT_CATEGORIES.items()
- if info.get("toggle") == "seismic"
- and (cid.startswith("stream_") or cid == "stream_flow")
- }
- native = _native_emitted_water_categories()
- central = _central_path_water_categories()
- emitted = native | central
- missing = emitted - registry_water
- orphans = registry_water - emitted
- assert not missing, f"water emit set missing from ALERT_CATEGORIES: {missing}"
- assert not orphans, f"ALERT_CATEGORIES has orphan water entries: {orphans}"
-
-
-def test_native_threshold_categories_still_emitted():
- """Spot-check that usgs.py still has the two threshold-classified
- categories (regression guard against accidental removal)."""
- native = _native_emitted_water_categories()
- assert "stream_flood_warning" in native
- assert "stream_high_water" in native
-
-
-def test_central_hydro_pcode_strings_all_map_to_stream_flow():
- """Every realistic central hydro category collapses to stream_flow
- via the catchall `("hydro.", "stream_flow")` _CATEGORY_MAP entry."""
- for pcode in ("00060", "00065", "00010", "00045", "00095"):
- assert map_category(f"hydro.{pcode}.usgs.12345678") == "stream_flow"
-
-
-@pytest.mark.parametrize(
- "cat", ["stream_flow", "stream_flood_warning", "stream_high_water"],
-)
-def test_water_categories_have_required_fields(cat):
- info = ALERT_CATEGORIES[cat]
- assert info["toggle"] == "seismic"
- assert info["name"]
- assert info["description"]
- assert info["default_severity"] in {"routine", "priority", "immediate"}
- assert info["example_message"]
-
-
-# ---------- Severity sanity for central NWIS events -----------------------
-
-
-def test_central_nwis_severity_zero_routes_to_routine():
- """Central NWIS publishes severity=0 (no threshold classification).
- Confirm that becomes 'routine' in meshai's three-level scale."""
- assert map_severity(0) == "routine"