chore: excise the dead Central NATS consumer path (-11,328 LOC) (#144)

* chore: excise the dead Central NATS consumer path

Central was retired and its database dropped 2026-07-15; its NATS broker no
longer exists. Verified against the live CT108 deployment: all 12 adapters
run feed_source=native, zero on central, and central.enabled is False
(default, never overridden). The consumer and its handlers were unreachable.

Removed:
- central/consumer.py and 6 dead handlers (nws, quake, swpc, nwis, avy,
  incident) -- their handle_* entrypoints were reachable only from the
  consumer's dispatch
- the Central wiring in main.py (init, guarded start, retry loop, stop path)
- the dead config surface: CentralConsumerConfig, EnvironmentalConfig.central,
  adapter_config ("central","severity_thresholds") and its display block
- the nats-py dependency (consumer.py was its only importer)
- 19 test files that exercised only the dead path

KEPT -- these live under central/ but are imported directly by native
adapters, and deleting them would break production:
- wfigs_handler.py: firms_handler._handle_pass_boundary() calls its _render()
  on the live FIRMS growth-fire path (env/firms.py -> ingest_hotspot_pixel)
- firms_handler, satpass_handler, tle_handler: split files whose handle_*
  entrypoints are dead but whose engines are live. Left intact; splitting
  them is separate work.
- pass_predictor, budget, idaho_gauge_sites: fully live.

The usgs_quake keys global_mag_floor / regional_mag_floor / regional_centroid
/ regional_radius_mi / broadcast_pager_alerts are NOT removed despite comments
labelling them "CENTRAL-PATH ONLY" -- notifications/gating/quake.py reads them
unconditionally in the native path. Those comments are corrected separately.

Test-count note: the suite drops ~425 tests. Most were migration PARITY tests
whose sole purpose was proving the native rewrite byte-matched the Central
handler (golden byte-parity, cross-source identity, gate-sequence replay).
With the handler deleted there is nothing left to compare against, so they
cannot exist. Native-only tests were kept and reworked where a test reached
for a central symbol incidentally. This is a real coverage loss, accepted
deliberately: the parity harness proved the refactor faithful, and git
history preserves the originals.

Suite: 1984 passed, 6 failed -- the same 6 pre-existing failures as main
(stale SCHEMA_VERSION x3, expired TLE fixtures x2, one order-dependent),
all being fixed on fix/green-test-suite. No new failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(nws): restore native-only golden coverage for the wire formatter

Commit ca751fb5 deleted the Central nws_handler parity harness along with
the handler itself, which took the ONLY tests that pinned formatters.nws
.format()'s literal wire output. Gate-sequence and schema-conformance
tests already survived natively; the formatter's actual rendered text did
not have any native-only regression net.

Add TestFormatterGolden to test_nws_refactor.py: 3 real-fixture cases plus
6 hand-built pathological cases mined from the deleted test_nws_handler.py
(SVR path-sampling, the "no dangling separator" regression, TOR on-ground
vs radar-indicated, FFW flood-cause detection). Every literal was verified
by temporarily restoring the pre-excision central.nws_handler._render()
from git history (ca751fb5^) in a throwaway, uncommitted script, confirming
byte-identical output against the current native format() for all 37 real
fixtures (nws/ + nws_last/) and all 9 pathological cases, then pinning the
confirmed-matching string as the literal -- not a blind snapshot of
current behavior.

quake/swpc/avalanche/hydro/incident/fire were checked and already carry
equivalent native-only golden coverage (added directly in ca751fb5), so no
changes were needed there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
malice 2026-07-17 14:03:40 -06:00 committed by GitHub
commit 9f06930a0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 643 additions and 11275 deletions

View file

@ -194,49 +194,3 @@ def test_missing_event_id_returns_none(adapter):
def test_does_not_raise_on_corrupted_dict(adapter):
"""Corrupted dict returns None without raising."""
assert adapter.to_event({"garbage": True}) is None
# ============================================================================
# Central avy_handler._render (mesh broadcast wire) -- budget-fit format.
# advice -> FIRST SENTENCE only; zone/level/source kept FULL; fits 140.
# ============================================================================
from meshai.central.avy_handler import _render as _avy_render
def test_avy_render_advice_first_sentence_only():
wire = _avy_render(
danger_level=4, danger_name="High",
zone_name="Western Mountains",
center_id="SNFAC",
travel=("Avoid all avalanche terrain today. Natural and human-triggered "
"avalanches are likely on steep slopes."),
)
# first sentence retained (with its period), the rest dropped
assert "Avoid all avalanche terrain today." in wire
assert "Natural and human-triggered" not in wire
# zone / level / source kept FULL (never abbreviated)
assert "Western Mountains" in wire
assert "High (4)" in wire
assert "SNFAC" in wire
def test_avy_render_worst_case_fits_140():
# Long multi-sentence advice paragraph -- only the first sentence is kept,
# which lets the full zone / level / source survive under the 140 budget.
wire = _avy_render(
danger_level=5, danger_name="Extreme",
zone_name="Western Mountains",
center_id="Sawtooth Avalanche Center",
travel=("Avoid all avalanche terrain today! Very dangerous conditions "
"exist across all elevations and aspects with widespread natural "
"avalanche activity likely through the afternoon and overnight."),
)
assert len(wire) <= 140, f"{len(wire)} chars:\n{wire!r}"
# zone, level, source, and the first-sentence advice all present
assert "Western Mountains" in wire
assert "Extreme (5)" in wire
assert "Sawtooth Avalanche Center" in wire
assert "Avoid all avalanche terrain today!" in wire
# first sentence terminates at the '!' -> the rest is gone
assert "Very dangerous conditions" not in wire

View file

@ -208,11 +208,16 @@ def test_put_json_accepts_list(client):
def test_put_json_accepts_dict(client):
# central/severity_thresholds was removed with the Central handler path;
# any surviving "json"-type key accepts dict values equally (the API
# only checks JSON-serializability, not shape) -- use reminders_wfigs/
# terminate_when (default is a list) to exercise the dict-value path.
r = client.put(
"/api/adapter-config/central/severity_thresholds",
"/api/adapter-config/reminders_wfigs/terminate_when",
json={"value": {"routine_max": 0, "priority_max": 1, "immediate_min": 2}},
)
assert r.status_code == 200
assert r.json()["value"] == {"routine_max": 0, "priority_max": 1, "immediate_min": 2}
def test_put_json_accepts_none(client):
@ -291,8 +296,8 @@ def test_list_meta(client):
body = r.json()
assert "wfigs" in body
assert body["wfigs"]["include_in_llm_context"] is True
# central / geocoder default to False
assert body["central"]["include_in_llm_context"] is False
# geocoder defaults to False (central adapter meta was removed with the
# Central handler path)
assert body["geocoder"]["include_in_llm_context"] is False

View file

@ -153,9 +153,9 @@ def test_registry_has_no_duplicate_keys():
def test_adapter_meta_at_19(fresh_db):
# Count sentinel — bump when an adapter row is added. 23 -> 24 with the
# IPAWS civil-alert adapter (adapter_config/defaults.py ADAPTER_META["ipaws"]).
assert len(ADAPTER_META) == 24
# Count sentinel — bump when an adapter row is added/removed. 24 -> 23
# with the removal of ADAPTER_META["central"] (dead NATS consumer excised).
assert len(ADAPTER_META) == 23
# ---------- seed ----------------------------------------------------------
@ -303,12 +303,6 @@ def test_accessor_returns_json_list(fresh_db):
assert adapter_config.nws.tombstone_msgtypes == ["Cancel", "Expire"]
def test_accessor_returns_json_dict(fresh_db):
invalidate_cache()
v = adapter_config.central.severity_thresholds
assert v == {"routine_max": 1, "priority_max": 2, "immediate_min": 3}
def test_accessor_returns_json_none(fresh_db):
invalidate_cache()
assert adapter_config.firms.bbox is None

View file

@ -1,17 +1,20 @@
"""Phase-1 avalanche refactor tests — formatter+decider architecture.
Four test groups:
The Central `avy_handler` module (and its centralseverityNAADS remap,
`handle_avy()`, `_render()`) has been deleted the native path is the only
production path now. Pure old-vs-new parity tests and tests of
Central-only logic (`_remap_centralseverity`, `handle_avy`) 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 (tier-b): formatter renders from canonical data.
Expected strings are hand-written (the new correct format).
OLD _render() output for the same fixture is captured in comments so the
intended tier-b diff is explicit and reviewable.
The centralseverity=2 (Considerable) case is OLD-vs-NEW identical.
The is_update=True case shows the Update: prefix diff.
Three test groups:
2. Cross-source identity: native AvalancheAdapter.to_event() builds the same
canonical data as the Central path for fixture 0000. Both render
byte-identically via the registered formatter.
1. Parity (tier-b): formatter renders from canonical data. Expected
strings are hand-written (the current correct format).
2. Cross-source identity: native AvalancheAdapter.to_event() canonical data
renders correctly via the registered formatter.
3. Gate-sequence: replay canonical data through gating.avalanche.decide().
Verify danger-level gate (below / at / above threshold) and firstupdate
@ -66,16 +69,6 @@ def _canonical_from_fixture(fixture: dict, *, is_update: bool = False) -> dict:
}
def _avy_render_old(*, danger_level: int, danger_name: str, zone_name: str,
center_id: str, travel: str) -> str:
"""Capture OLD _render() output from avy_handler for diff comments."""
from meshai.central.avy_handler import _render
return _render(
danger_level=danger_level, danger_name=danger_name,
zone_name=zone_name, center_id=center_id, travel=travel,
)
# ─────────────────────────────────────────────────────────────────────────────
# 1. Parity (tier-b) — formatter renders from canonical data
# ─────────────────────────────────────────────────────────────────────────────
@ -117,17 +110,6 @@ class TestFormatterParity:
assert_byte_identical(result, expected)
# Confirm old _render() is identical for this case (no tier-b diff)
old_wire = _avy_render_old(
danger_level=3, danger_name="Considerable",
zone_name="Sawtooth Mountains", center_id="SNFAC",
travel="Dangerous conditions on steep slopes. Conservative decision-making is advised.",
)
assert_byte_identical(result, old_wire), (
"For fixture 0000 (Considerable, is_update=False) old and new "
"outputs must be identical — tier-b diff only appears for is_update=True."
)
def test_fixture_0001_high_warning_prefix(self):
"""Fixture 0001 (High, NAADS 4) → formatter uses 'WARNING:' prefix.
@ -157,15 +139,6 @@ class TestFormatterParity:
assert_byte_identical(result, expected)
old_wire = _avy_render_old(
danger_level=4, danger_name="High",
zone_name="Banner Summit", center_id="SNFAC",
travel="Avoid all avalanche terrain today. Natural avalanches are likely on steep slopes.",
)
assert_byte_identical(result, old_wire), (
"For High (level=4, is_update=False) old and new must be identical."
)
def test_fixture_0002_extreme_warning_prefix(self):
"""Fixture 0002 (Extreme, NAADS 5) → formatter uses 'WARNING:' prefix.
@ -196,17 +169,7 @@ class TestFormatterParity:
assert_byte_identical(result, expected)
def test_tier_b_update_prefix_rendered(self):
"""Tier-b: is_update=True produces 'AVY Update:' prefix.
OLD _render() output (no is_update path):
'⛷ AVY Watch: Sawtooth Mountains — Considerable (3)
Dangerous conditions on steep slopes.
SNFAC · valid today'
NEW formatter output (is_update=True):
'⛷ AVY Update: Sawtooth Mountains — Considerable (3)
Dangerous conditions on steep slopes.
SNFAC · valid today'
"""
"""Tier-b: is_update=True produces 'AVY Update:' prefix."""
from meshai.notifications.formatters.avalanche import format as avyfmt
fixtures = load_fixtures("avalanche")
@ -214,17 +177,6 @@ class TestFormatterParity:
# Extract with is_update=True
canonical = _canonical_from_fixture(fx, is_update=True)
# OLD _render() output (no is_update support)
old_wire = _avy_render_old(
danger_level=3, danger_name="Considerable",
zone_name="Sawtooth Mountains", center_id="SNFAC",
travel="Dangerous conditions on steep slopes. Conservative decision-making is advised.",
)
expected_old = (
"⛷ AVY Watch: Sawtooth Mountains — Considerable (3)"
"\nDangerous conditions on steep slopes."
"\nSNFAC · valid today"
)
expected_new = (
"⛷ AVY Update: Sawtooth Mountains — Considerable (3)"
"\nDangerous conditions on steep slopes."
@ -234,7 +186,6 @@ class TestFormatterParity:
with pinned_time(_AT):
result = avyfmt(_make_fake_event(canonical), now=_AT, budget=140)
assert_byte_identical(old_wire, expected_old)
assert_byte_identical(result, expected_new)
assert "Update:" in result
assert "Watch:" not in result
@ -435,39 +386,6 @@ class TestCrossSourceIdentity:
"the formatter registry governs rendering"
)
def test_handle_avy_writes_canonical_into_data(self):
"""handle_avy() writes canonical fields into the shared data dict on broadcast."""
from meshai.central.avy_handler import handle_avy
envelope = {
"id": "avy-snfac-sawtooth-test",
"data": {
"adapter": "avalanche_org",
"category": "advisory.us.id",
"severity": 2,
"geo": {"centroid": [-114.9, 43.8]},
"data": {
"danger_level": 3,
"danger_name": "Considerable",
"zone_name": "Sawtooth Mountains",
"center_id": "SNFAC",
"travel_advice": "Dangerous conditions.",
},
},
}
data: dict = {}
wire = handle_avy(envelope, "central.avy.advisory.us.id", data=data)
assert wire is not None, "handle_avy must return a wire string on broadcast"
# Canonical fields must be in the shared data dict
assert data.get("danger_level") == 3
assert data.get("danger_name") == "Considerable"
assert data.get("zone_name") == "Sawtooth Mountains"
assert data.get("center_id") == "SNFAC"
assert "_on_broadcast_committed" in data
assert "_broadcast_audit" in data
# ─────────────────────────────────────────────────────────────────────────────
# 3. Gate-sequence — danger-level gate + first→update trend
# ─────────────────────────────────────────────────────────────────────────────
@ -541,86 +459,6 @@ class TestGateSequence:
result = decide({"zone_name": "test"}, source="avalanche", now=_AT)
assert result.broadcast is False
def test_centralseverity_remap_considerable(self):
"""centralseverity=2 → NAADS 3 (Considerable) → broadcast at min_level=3.
This tests the remap logic in _remap_centralseverity() as used by
handle_avy() when data.data.danger_level is absent.
Mapping table (needs live validation in-season Oct+):
centralseverity 2 NAADS 3 (Considerable) [documented]
centralseverity 3 NAADS 4 (High) [documented]
centralseverity 4 NAADS 5 (Extreme) [documented]
centralseverity 0 NAADS 1 (Low) [inferred]
centralseverity 1 NAADS 2 (Moderate) [inferred]
"""
from meshai.central.avy_handler import _remap_centralseverity
# Documented values
assert _remap_centralseverity(2) == 3, "centralseverity 2 → NAADS 3 (Considerable)"
assert _remap_centralseverity(3) == 4, "centralseverity 3 → NAADS 4 (High)"
assert _remap_centralseverity(4) == 5, "centralseverity 4 → NAADS 5 (Extreme)"
# Inferred values
assert _remap_centralseverity(0) == 1, "centralseverity 0 → NAADS 1 (Low) [inferred]"
assert _remap_centralseverity(1) == 2, "centralseverity 1 → NAADS 2 (Moderate) [inferred]"
# Out-of-range → 0 (No Rating)
assert _remap_centralseverity(5) == 0, "centralseverity 5 → 0 (No Rating)"
assert _remap_centralseverity(-1) == 0
assert _remap_centralseverity("?") == 0
def test_centralseverity_gate_sequence_via_fixtures(self):
"""Gate-sequence via fixtures: centralseverity 2/3/4 all broadcast; 1 suppresses.
Uses handle_avy to exercise the full Central ingestion path including
the centralseverity NAADS remap and the decide() call.
"""
from meshai.central.avy_handler import handle_avy
def _make_envelope(centralseverity: int, naads_level: int,
danger_name: str) -> dict:
return {
"data": {
"adapter": "avalanche_org",
"category": "advisory.us.id",
"severity": centralseverity,
"geo": {"centroid": [-114.9, 43.8]},
"data": {
"danger_level": naads_level,
"danger_name": danger_name,
"zone_name": "Test Zone",
"center_id": "SNFAC",
"travel_advice": "Test advice.",
},
}
}
# centralseverity=1 → NAADS 2 (Moderate) → suppressed (below min_level=3)
env_moderate = _make_envelope(1, 2, "Moderate")
data_m: dict = {}
wire_m = handle_avy(env_moderate, "central.avy.advisory.us.id", data=data_m)
assert wire_m is None, "Moderate (NAADS 2) must be suppressed"
# centralseverity=2 → NAADS 3 (Considerable) → broadcast
env_considerable = _make_envelope(2, 3, "Considerable")
data_c: dict = {}
wire_c = handle_avy(env_considerable, "central.avy.advisory.us.id", data=data_c)
assert wire_c is not None, "Considerable (NAADS 3) must broadcast"
assert "Watch" in wire_c
# centralseverity=3 → NAADS 4 (High) → broadcast with WARNING
env_high = _make_envelope(3, 4, "High")
data_h: dict = {}
wire_h = handle_avy(env_high, "central.avy.advisory.us.id", data=data_h)
assert wire_h is not None, "High (NAADS 4) must broadcast"
assert "WARNING" in wire_h
# centralseverity=4 → NAADS 5 (Extreme) → broadcast with WARNING
env_extreme = _make_envelope(4, 5, "Extreme")
data_e: dict = {}
wire_e = handle_avy(env_extreme, "central.avy.advisory.us.id", data=data_e)
assert wire_e is not None, "Extreme (NAADS 5) must broadcast"
assert "WARNING" in wire_e
def test_is_update_propagated_into_data_patch(self):
"""decide() data_patch.is_update reflects the incoming is_update flag."""
from meshai.notifications.gating.avalanche import decide

View file

@ -1,140 +0,0 @@
"""v0.5.7-avalanche: Central avalanche check + categories audit.
Covers two things shipped in v0.5.7-avalanche:
1. Central avalanche adapter check -- VERIFIED ABSENT in Central v0.10.0.
The guide (docs/CONSUMER-INTEGRATION.md at v0.10.0-itd-511) has zero
`avalanche` / `NWAC` / `CAIC` references, and the producer source tree
(src/central/adapters/) has no avalanche-named adapter files. meshai's
consumer already documents this explicitly: _subjects_for("avalanche", *)
returns [], and _subject_owned() logs a warning if someone flips
avalanche.feed_source=central. This phase pins those invariants so a
future refactor that introduces an avalanche Central wire breaks
loudly here.
2. ALERT_CATEGORIES avalanche-family audit. Native avalanche.py emits two
categories based on NWAC/CAIC danger_level:
danger_level >= 4 (High, Extreme) -> avalanche_warning
danger_level == 3 (Considerable) -> avalanche_watch
danger_level <= 2 (Low, Moderate) -> silently dropped
Pre-v0.5.7-avalanche the registry had avalanche_warning +
avalanche_considerable. avalanche_considerable was a legacy name for
the Considerable-danger tier; native code now emits avalanche_watch
for the same semantic. Added avalanche_watch in v0.5.7-avalanche;
kept avalanche_considerable as a forward-compat target (no migration
churn).
"""
import inspect
import re
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
# ---------- FIX 1: Central has no avalanche adapter -----------------------
def test_avalanche_has_no_central_subscription():
"""_subjects_for returns empty for the avalanche source regardless of
region (no Central counterpart exists in v0.10.0)."""
for region in ("us.id", "us.mt", "us.co", "", None):
# Avalanche was added to central pipeline; verify it has subjects.
assert _subjects_for("avalanche", region) != [], \
f"expected subjects for region={region!r}"
def test_avalanche_absent_from_subjects_bare():
"""The bare-wildcard table also has no avalanche entry."""
assert "avalanche" in _SUBJECTS_BARE
def test_avalanche_absent_from_central_adapter_remap():
"""No Central adapter name remaps to meshai's 'avalanche' source."""
assert "avalanche" in CENTRAL_ADAPTER_TO_SOURCE.values(), \
f"avalanche should have a remap entry: {CENTRAL_ADAPTER_TO_SOURCE}"
def test_avalanche_feed_source_central_subscribes_nothing():
"""If a user accidentally sets avalanche.feed_source=central, the
subject_owned() builder must not emit a subscription (and the
consumer logs a warning -- documented in consumer.py)."""
env = EnvironmentalConfig()
env.avalanche.feed_source = "central"
so = CentralConsumer(env, None)._subject_owned()
# No subjects added for avalanche; nothing to subscribe to.
assert not any("avalanche" in s.lower() for s in so.keys())
# ---------- FIX 2: ALERT_CATEGORIES avalanche-family audit ---------------
def test_avalanche_watch_in_registry():
"""v0.5.7-avalanche: avalanche_watch is now registry-present so the
Advanced Rules editor can target Considerable-tier emissions."""
assert "avalanche_watch" in ALERT_CATEGORIES
info = ALERT_CATEGORIES["avalanche_watch"]
assert info["toggle"] == "avalanche"
assert info["default_severity"] == "routine"
assert info["name"]
assert info["description"]
assert info["example_message"]
def test_avalanche_warning_still_in_registry():
"""Pre-v0.5.7-avalanche entry survives the edit."""
assert "avalanche_warning" in ALERT_CATEGORIES
assert ALERT_CATEGORIES["avalanche_warning"]["toggle"] == "avalanche"
def test_avalanche_considerable_legacy_kept():
"""avalanche_considerable kept as forward-compat / legacy target even
though no current code path emits it. Documented in the commit body
and categories.py inline note for future cleanup."""
assert "avalanche_considerable" in ALERT_CATEGORIES
assert ALERT_CATEGORIES["avalanche_considerable"]["toggle"] == "avalanche"
def _native_emitted_avalanche_categories() -> set[str]:
"""Walk avalanche.py for category= literals routing to toggle=avalanche."""
from meshai.env import avalanche as aval_mod
src = inspect.getsource(aval_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") == "avalanche"}
def test_alert_categories_avalanche_complete():
"""Every category native avalanche.py emits must have a registry entry
under toggle='avalanche'. Legacy entries without an emitter are
allowed (subset assertion, not equality)."""
registry_avalanche = {
cid for cid, info in ALERT_CATEGORIES.items()
if info.get("toggle") == "avalanche"
}
native = _native_emitted_avalanche_categories()
missing = native - registry_avalanche
assert not missing, f"avalanche emit set missing from ALERT_CATEGORIES: {missing}"
# Sanity: the two v0.5.7-avalanche-recognized categories are both there.
assert "avalanche_warning" in native, "native should emit avalanche_warning"
assert "avalanche_watch" in native, "native should emit avalanche_watch"
@pytest.mark.parametrize(
"cat", ["avalanche_warning", "avalanche_watch", "avalanche_considerable"],
)
def test_avalanche_categories_have_required_fields(cat):
info = ALERT_CATEGORIES[cat]
assert info["toggle"] == "avalanche"
assert info["name"]
assert info["description"]
assert info["default_severity"] in {"routine", "priority", "immediate"}
assert info["example_message"]

View file

@ -1,310 +0,0 @@
"""Tests for Central NATS boot-time grace + retry (fix/central-boot-guard).
meshai.main cannot be imported in the test environment (missing runtime deps:
openai, aiosqlite, meshtastic, ). The spec allows unit-testing the guard
helper in isolation. We do this by:
1. Embedding the exact method bodies from main.py into a minimal async class
(BootGuard) that exposes only what the methods need. If the method body
in main.py changes, the test will naturally drift it exists to catch
regressions in the guarded-connect-and-retry contract.
2. Separately testing CentralConsumer.start() no-op guard (already
exercised in test_central_consumer.py; duplicated here as a sanity check
that the NATS connect path is never reached when nothing is configured).
The methods under test (copied verbatim from meshai/main.py):
_start_central_consumer_guarded
_central_retry_loop
"""
import asyncio
import logging
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Minimal class that replicates the two new methods from MeshAI, verbatim.
# This is intentional: if the logic in main.py changes, the copyed body here
# drifts and the test surfaces the mismatch.
# ---------------------------------------------------------------------------
class BootGuard:
"""Thin stand-in for the two guard methods on MeshAI."""
def __init__(self, consumer, running=True):
self._central_consumer = consumer
self._central_retry_task = None
self._running = running
async def _start_central_consumer_guarded(self) -> None:
try:
await self._central_consumer.start()
except Exception as exc:
logger.warning(
"Central unreachable at startup (%s); continuing without hazard "
"firehose, will retry in background", exc,
)
if self._central_consumer.subjects():
self._central_retry_task = asyncio.create_task(
self._central_retry_loop()
)
async def _central_retry_loop(self) -> None:
delay = 30.0
max_delay = 300.0
while self._running:
try:
await asyncio.sleep(delay)
except asyncio.CancelledError:
return
if not self._running:
return
if self._central_consumer._nc is not None:
logger.info("Central retry: already connected, stopping retry loop")
return
try:
await self._central_consumer.start()
logger.info(
"Central connected after delayed boot (retry backoff was %.0fs)", delay
)
return
except asyncio.CancelledError:
return
except Exception as exc:
next_delay = min(delay * 2, max_delay)
logger.warning(
"Central retry failed (%s); next attempt in %.0fs", exc, next_delay,
)
delay = next_delay
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _consumer(subjects=None, nc=None):
c = MagicMock()
c.subjects.return_value = subjects if subjects is not None else []
c._nc = nc
return c
# ---------------------------------------------------------------------------
# _start_central_consumer_guarded: success path
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_guarded_start_success_does_not_raise():
"""When start() succeeds, no exception propagates and no retry is created."""
c = _consumer(subjects=["central.quake.>"])
c.start = AsyncMock()
g = BootGuard(c)
await g._start_central_consumer_guarded()
c.start.assert_awaited_once()
assert g._central_retry_task is None
# ---------------------------------------------------------------------------
# _start_central_consumer_guarded: failure paths
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_guarded_start_failure_does_not_raise():
"""Exception from start() is swallowed — boot continues."""
c = _consumer(subjects=["central.wx.alert.us.id.>"])
c.start = AsyncMock(side_effect=Exception("nats: no servers"))
g = BootGuard(c)
await g._start_central_consumer_guarded() # must NOT raise
@pytest.mark.asyncio
async def test_guarded_start_failure_schedules_retry_when_subjects_nonempty():
"""Exception + non-empty subjects → retry task is created."""
c = _consumer(subjects=["central.wx.alert.us.id.>"])
c.start = AsyncMock(side_effect=Exception("nats: no servers"))
g = BootGuard(c)
await g._start_central_consumer_guarded()
assert g._central_retry_task is not None
g._central_retry_task.cancel()
try:
await g._central_retry_task
except (asyncio.CancelledError, Exception):
pass
@pytest.mark.asyncio
async def test_guarded_start_failure_no_retry_when_no_subjects():
"""Exception + empty subjects (all-native/disabled config) → no retry task."""
c = _consumer(subjects=[])
c.start = AsyncMock(side_effect=Exception("unexpected"))
g = BootGuard(c)
await g._start_central_consumer_guarded()
assert g._central_retry_task is None
# ---------------------------------------------------------------------------
# _central_retry_loop
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_retry_loop_exits_on_success():
"""Loop calls start(), which succeeds by setting _nc, then exits."""
c = _consumer(subjects=["central.quake.>"])
async def succeed():
c._nc = MagicMock()
c.start = AsyncMock(side_effect=succeed)
g = BootGuard(c)
with patch("asyncio.sleep", new=AsyncMock()):
await g._central_retry_loop()
c.start.assert_awaited_once()
@pytest.mark.asyncio
async def test_retry_loop_exits_on_cancel_during_sleep():
"""CancelledError from sleep causes clean exit without calling start()."""
c = _consumer(subjects=["central.quake.>"])
c.start = AsyncMock()
g = BootGuard(c)
async def raise_cancel(*_):
raise asyncio.CancelledError
with patch("asyncio.sleep", new=AsyncMock(side_effect=raise_cancel)):
await g._central_retry_loop() # must not raise
c.start.assert_not_awaited()
@pytest.mark.asyncio
async def test_retry_loop_skips_if_already_connected():
"""If _nc is already set when the retry fires, loop exits without start()."""
c = _consumer(subjects=["central.quake.>"], nc=MagicMock())
c.start = AsyncMock()
g = BootGuard(c)
with patch("asyncio.sleep", new=AsyncMock()):
await g._central_retry_loop()
c.start.assert_not_awaited()
@pytest.mark.asyncio
async def test_retry_loop_stops_when_not_running():
"""_running=False causes the loop to exit after the first sleep."""
c = _consumer(subjects=["central.quake.>"])
c.start = AsyncMock()
g = BootGuard(c, running=False)
with patch("asyncio.sleep", new=AsyncMock()):
await g._central_retry_loop()
c.start.assert_not_awaited()
@pytest.mark.asyncio
async def test_retry_loop_backoff_accumulates():
"""Delay doubles each cycle, capped at 300s."""
c = _consumer(subjects=["central.quake.>"])
call_count = 0
async def start_on_third():
nonlocal call_count
call_count += 1
if call_count < 3:
raise Exception("still down")
c._nc = MagicMock()
c.start = AsyncMock(side_effect=start_on_third)
g = BootGuard(c)
sleep_delays = []
async def record_sleep(d):
sleep_delays.append(d)
with patch("asyncio.sleep", new=AsyncMock(side_effect=record_sleep)):
await g._central_retry_loop()
assert call_count == 3
assert sleep_delays == [30.0, 60.0, 120.0]
@pytest.mark.asyncio
async def test_retry_loop_caps_delay_at_max():
"""Delay is capped at 300s after enough failures."""
c = _consumer(subjects=["central.quake.>"])
delays_seen = []
call_count = 0
async def always_fail():
nonlocal call_count
call_count += 1
if call_count >= 8:
# Eventually succeed so the test terminates
c._nc = MagicMock()
return
raise Exception("still down")
c.start = AsyncMock(side_effect=always_fail)
g = BootGuard(c)
async def record_sleep(d):
delays_seen.append(d)
with patch("asyncio.sleep", new=AsyncMock(side_effect=record_sleep)):
await g._central_retry_loop()
# After several doublings, delay must be capped at 300s, not grow unbounded
assert max(delays_seen) == 300.0
# ---------------------------------------------------------------------------
# CentralConsumer.start() no-op guard (component-level sanity check)
# ---------------------------------------------------------------------------
def test_consumer_start_is_noop_when_unconfigured():
"""start() must not attempt NATS connect when no adapter is central-sourced.
This is the direct regression guard: if CentralConsumer.start() were to
call nats.connect() unconditionally it would fail in this environment
(no real NATS server), proving the guard works.
Note: the conftest seeds adapter_config from the DB which may flip satpass
to feed_source=central. We override all adapters to native explicitly so
subjects() is empty and start() must be a pure no-op.
"""
from meshai.config import EnvironmentalConfig
from meshai.central.consumer import CentralConsumer, _SUBJECTS_BARE
from meshai.notifications.pipeline.bus import EventBus
env = EnvironmentalConfig()
# Force all known adapters to native so subjects() returns []
for attr in list(_SUBJECTS_BARE.keys()) + ["avalanche", "ducting"]:
cfg = getattr(env, attr, None)
if cfg is not None and hasattr(cfg, "feed_source"):
cfg.feed_source = "native"
bus = EventBus()
c = CentralConsumer(env, bus)
assert c.subjects() == [], f"Expected no subjects, got: {c.subjects()}"
asyncio.run(c.start()) # must not raise, must not touch NATS
assert c._nc is None

View file

@ -1,221 +0,0 @@
"""v0.4 C.1: Central connector backend — normalization, lifecycle, source gate."""
import asyncio
import json
import pytest
from meshai.config import EnvironmentalConfig
from meshai.central.consumer import CentralConsumer, map_category, map_severity
from meshai.notifications.pipeline.bus import EventBus
pytestmark = pytest.mark.skip(
reason="v0.5.13 default-deny: consumer-level tests assumed envelopes without a handler-synthesized wire still emit an Event with title fallback. New architecture (test_consumer_default_deny.py) verifies the inverse: default-deny when no handler synthesized. v0.6 will rebuild source-remap tests.")
def make_consumer():
env = EnvironmentalConfig()
bus = EventBus()
rec = []
bus.subscribe(rec.append)
return CentralConsumer(env, bus), env, rec
def envelope(adapter="usgs_quake", category="quake.event", severity=2,
eid="us6000abcd", centroid=(-114.5, 42.6), upstream=None,
time="2026-05-27T12:00:00Z", expires=None):
return {
"id": eid, "source": "central.echo6.co",
"type": f"central.{category}.v1", "time": time,
"centralcategory": category, "centralseverity": severity,
"specversion": "1.0", "datacontenttype": "application/json",
"data": {
"id": eid, "adapter": adapter, "category": category,
"time": time, "expires": expires, "severity": severity,
"geo": {"centroid": list(centroid), "bbox": None,
"regions": ["US-ID"], "primary_region": "US-ID"},
"data": upstream if upstream is not None else {"magnitude": 4.2, "place": "near Twin Falls"},
},
}
class FakeMsg:
def __init__(self, subject, env):
self.subject = subject
self.data = json.dumps(env).encode()
self.acked = False
async def ack(self):
self.acked = True
# ---- subject derivation / source gate ----
def test_no_subjects_when_all_native():
c, env, rec = make_consumer()
assert c.subjects() == []
def test_subjects_when_central():
# v0.5.4: assert the legacy bare-wildcard form by clearing region.
# Region-aware subject shapes are covered by test_central_region_routing.py.
c, env, rec = make_consumer()
env.central.region = ""
env.usgs_quake.feed_source = "central"
assert "central.quake.>" in c.subjects()
def test_source_central_skips_native_instantiation():
from meshai.env.store import EnvironmentalStore
env = EnvironmentalConfig()
env.enabled = True
env.usgs_quake.enabled = True
env.usgs_quake.feed_source = "central" # should be skipped natively
env.nws.enabled = True # native -> present
store = EnvironmentalStore(config=env, region_anchors=[], event_bus=None)
assert "usgs_quake" not in store._adapters
assert "nws" in store._adapters
# ---- normalization ----
def test_normalize_and_emit():
c, env, rec = make_consumer()
ev = c._handle("central.quake.event.moderate", json.dumps(envelope()).encode())
assert ev is not None
assert len(rec) == 1
e = rec[0]
assert e.source == "usgs_quake"
assert e.category == "earthquake_event"
assert e.severity == "priority" # central severity 2
assert e.lat == 42.6 and e.lon == -114.5 # [lon,lat] -> (lat,lon)
assert e.group_key == "us6000abcd"
assert e.region == "US-ID"
assert e.data.get("magnitude") == 4.2 # upstream preserved verbatim
def test_enriched_preserved_verbatim():
c, env, rec = make_consumer()
up = {"magnitude": 5.1, "_enriched": {"geocoder": {"state": "Idaho"}, "usgs_stats": {"x": 1}}}
ev = c._handle("central.quake.event.strong", json.dumps(envelope(severity=4, upstream=up)).encode())
assert ev.severity == "immediate"
assert ev.data["_enriched"]["geocoder"]["state"] == "Idaho"
assert ev.data["_enriched"]["usgs_stats"] == {"x": 1}
def test_tombstone_translates_to_clear():
c, env, rec = make_consumer()
msg = envelope(adapter="gdacs", category="disaster.fl.removed", severity=0, eid="FL1103885:removed")
ev = c._handle("central.disaster.fl.removed.austria", json.dumps(msg).encode())
assert ev is not None
assert ev.group_key == "FL1103885" # ':removed' stripped -> matches original
assert ev.data.get("_central_tombstone") is True
def test_severity_mapping():
assert map_severity(0) == "routine"
assert map_severity(1) == "routine"
assert map_severity(2) == "priority"
assert map_severity(3) == "immediate"
assert map_severity(4) == "immediate"
assert map_severity(None) == "routine"
def test_category_mapping():
assert map_category("wx.alert.severe_thunderstorm_warning") == "weather_warning"
assert map_category("quake.event") == "earthquake_event"
assert map_category("fire.hotspot.viirs_noaa20.high") == "wildfire_hotspot"
assert map_category("hydro.00060.usgs.06901250") == "stream_flow"
# ---- async callback path ----
def test_on_message_emits_and_acks():
c, env, rec = make_consumer()
msg = FakeMsg("central.quake.event.moderate", envelope())
asyncio.run(c._on_message(msg))
assert msg.acked is True
assert len(rec) == 1
def test_start_no_op_when_all_native():
"""start() is a no-op (no NATS connect) when no adapter is central."""
c, env, rec = make_consumer()
asyncio.run(c.start()) # must not raise / must not require NATS
assert c._nc is None
def test_consumer_config_uses_deliver_policy_new():
"""C.3.1: Central subscriptions use deliver_policy=NEW (no full-backlog replay)."""
from meshai.central.consumer import consumer_config
from nats.js.api import DeliverPolicy
assert consumer_config().deliver_policy == DeliverPolicy.NEW
def test_subject_domain_fallback_for_unmapped_category():
"""D.1: an unmapped category falls back to the subject domain instead
of returning 'other'.
v0.5.7-traffic note: 'work_zone.wzdx' is now MAPPED (-> 'work_zone'),
so we use a genuinely-unmapped category string here to exercise the
fallback path. The subject-domain fallback for central.traffic.* is
still 'traffic_congestion'.
"""
import json
from meshai.central.consumer import CentralConsumer, category_from_subject
from meshai.config import EnvironmentalConfig
from meshai.notifications.pipeline.bus import EventBus
assert category_from_subject("central.traffic.work_zone.ok") == "traffic_congestion"
rec = []
bus = EventBus(); bus.subscribe(rec.append)
c = CentralConsumer(EnvironmentalConfig(), bus)
env = {"id": "wz1", "data": {"id": "wz1", "adapter": "wzdx",
"category": "telematics.unknown_thing", "time": "2026-05-28T00:00:00Z", "severity": 1,
"geo": {"centroid": [-96.2, 36.15], "primary_region": "US-OK", "regions": ["US-OK"]},
"data": {"road": "I-44"}}}
ev = c._handle("central.traffic.work_zone.ok", json.dumps(env).encode())
assert ev is not None and ev.category == "traffic_congestion"
def test_v057_traffic_work_zone_now_mapped():
"""v0.5.7-traffic: 'work_zone.wzdx' maps to the new 'work_zone' meshai
category (not flattened to traffic_congestion)."""
import json
from meshai.central.consumer import CentralConsumer
from meshai.config import EnvironmentalConfig
from meshai.notifications.pipeline.bus import EventBus
rec = []
bus = EventBus(); bus.subscribe(rec.append)
c = CentralConsumer(EnvironmentalConfig(), bus)
env = {"id": "wz2", "data": {"id": "wz2", "adapter": "wzdx",
"category": "work_zone.wzdx", "time": "2026-05-28T00:00:00Z", "severity": 1,
"geo": {"centroid": [-114.0, 42.0], "primary_region": "US-ID", "regions": ["US-ID"]},
"data": {"road": "I-84"}}}
ev = c._handle("central.traffic.work_zone.id", json.dumps(env).encode())
assert ev is not None and ev.category == "work_zone"
@pytest.mark.parametrize("adapter,expected", [
("wfigs_incidents", "fires"),
("nwis", "usgs"),
("swpc_alerts", "swpc"),
("wzdx", "traffic"),
("nws", "nws"), # 1:1 passthrough
("experimental_foo", "experimental_foo"), # unknown -> passthrough
])
def test_central_adapter_source_remap(adapter, expected):
"""D.2: Central adapter names map to meshai source names (unknown passes through)."""
import json
from meshai.central.consumer import CentralConsumer
from meshai.config import EnvironmentalConfig
from meshai.notifications.pipeline.bus import EventBus
rec = []
bus = EventBus(); bus.subscribe(rec.append)
c = CentralConsumer(EnvironmentalConfig(), bus)
env = {"id": "e1", "data": {"id": "e1", "adapter": adapter, "category": "wx.alert.x",
"time": "2026-05-28T00:00:00Z", "severity": 1,
"geo": {"centroid": [-114.0, 42.0], "primary_region": "US-ID", "regions": ["US-ID"]},
"data": {}}}
ev = c._handle("central.wx.alert.x", json.dumps(env).encode())
assert ev is not None and ev.source == expected

View file

@ -1,289 +0,0 @@
"""v0.5.7-regression: end-to-end Central envelope -> mesh wire string.
Closes the seam between consumer/composer/renderer that the v0.5.7 staged
flip exposed. Pre-v0.5.7-regression two pre-existing bugs were dormant:
1. consumer._normalize() fell back to `cat_raw` (the raw Central
hierarchical category like "incident.tomtom_incidents") when the
upstream payload lacked `title`/`headline`. That string ended up as
event.title and the composer's primary identifier.
2. MeshRenderer._format_one_line() prepended "[<Family>] " 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": "<p>The NWS in Boise has issued a Severe Thunderstorm Warning...</p>",
"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}"
)

View file

@ -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.<region>.<...>)."""
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.<tier>` 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.<state>). 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.<satellite>.<confidence>`
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.<state>`)
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.<state> 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.<param>.<agency>.<site>.<region>` where <region> is
either `us.<state>` (7 tokens) or `unknown` (6 tokens). The
pre-v0.5.7-water shape `central.hydro.>.<state>` 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"}

View file

@ -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"}

View file

@ -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)

View file

@ -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

View file

@ -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]}"
)

View file

@ -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.<satellite>.<confidence>` 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.<state>
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 `<IrwinID>:removed:<iso_now>` 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:<iso> 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 <satellite>.<confidence> only -- no us.<state>."""
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:<iso> 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:<iso> 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:<iso>
# 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 `<id>:removed` shape (no :<iso>
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"]

View file

@ -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()

View file

@ -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"

View file

@ -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()

View file

@ -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"

View file

@ -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("")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 … — <towns>' 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)

View file

@ -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 (firstdup<3h
dup>3hCancel) 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 (firstdup<3h
dup>3hCancel) 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:

View file

@ -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

View file

@ -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."""

View file

@ -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 `"<tier>": "<category>",` 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"

View file

@ -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

View file

@ -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"

View file

@ -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)

View file

@ -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"

View file

@ -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.<tier>` (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.<tier> 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.<tier>`,
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"]

View file

@ -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"]

View file

@ -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

View file

@ -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")

View file

@ -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"

View file

@ -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.>.<state>` (mid-subject `>`), invalid per
NATS rules. Now: `central.traffic.*.<state>` (Convention B, bare state)
for traffic; roads511 dual-subscribes both Convention B and
`central.traffic.*.us.<state>` (Convention A, itd_511 form).
2. roads511 dual subscription owns both shared bare-state and us.<state>
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.<state> (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"]

View file

@ -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.<param>.<agency>.<site>.<region>` where <region> is
`us.<state>` (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.<parameter_code>.<agency>.
<bare_site_no>` 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.<param>.<agency>.<site>.<region>
where <region> is us.<state> (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.<pcode>.<agency>.<site>`."""
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"