meshai/work/tests/test_adapter_avalanche.py
malice 9f06930a0d
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>
2026-07-17 14:03:40 -06:00

196 lines
6.5 KiB
Python

"""Tests for avalanche adapter Phase 2.10 — to_event() method."""
import time
from unittest.mock import MagicMock
import pytest
from meshai.env.avalanche import AvalancheAdapter
from meshai.notifications.events import Event
# ============================================================
# FIXTURES
# ============================================================
@pytest.fixture
def mock_config():
"""Create a mock AvalancheConfig with real scalar fields."""
config = MagicMock()
config.center_ids = ["SNFAC"]
config.tick_seconds = 1800
config.season_months = [12, 1, 2, 3, 4]
return config
@pytest.fixture
def adapter(mock_config):
"""Create an AvalancheAdapter with mocked config."""
return AvalancheAdapter(mock_config)
def make_avy_event(
center_id="SNFAC",
zone_name="Banner Summit",
danger_level=4,
danger_name="High",
severity="priority",
travel_advice="Dangerous avalanche conditions.",
lat=44.3,
lon=-115.2,
headline=None,
):
"""Helper to create a stored avalanche event dict (mirrors _fetch)."""
now = time.time()
if headline is None:
headline = f"{zone_name}: {danger_name} avalanche danger"
if travel_advice:
headline += f" -- {travel_advice[:100]}"
return {
"source": "avalanche",
"event_id": f"avy_{center_id}_{zone_name.replace(' ', '_').lower()}",
"event_type": "Avalanche Advisory",
"severity": severity,
"headline": headline,
"zone_name": zone_name,
"center": "Sawtooth Avalanche Center",
"center_id": center_id,
"center_link": "https://www.sawtoothavalanche.com",
"forecast_link": "https://www.sawtoothavalanche.com/forecast",
"danger": danger_name.lower(),
"danger_level": danger_level,
"danger_name": danger_name,
"travel_advice": travel_advice,
"state": "ID",
"lat": lat,
"lon": lon,
"expires": now + 3600,
"fetched_at": now,
}
# ============================================================
# CATEGORY TESTS
# ============================================================
def test_high_and_extreme_are_warning(adapter):
"""Danger level 4 (High) and 5 (Extreme) map to avalanche_warning."""
for level, name in [(4, "High"), (5, "Extreme")]:
event = adapter.to_event(make_avy_event(danger_level=level, danger_name=name))
assert event is not None
assert event.category == "avalanche_warning"
def test_considerable_is_watch(adapter):
"""Danger level 3 (Considerable) maps to avalanche_watch."""
event = adapter.to_event(
make_avy_event(danger_level=3, danger_name="Considerable", severity="routine")
)
assert event is not None
assert event.category == "avalanche_watch"
# ============================================================
# SEVERITY PASS-THROUGH TESTS
# ============================================================
def test_severity_passes_through(adapter):
"""Severity from the stored event passes through unchanged."""
for sev in ["routine", "priority", "immediate"]:
event = adapter.to_event(make_avy_event(severity=sev, danger_level=4))
assert event is not None
assert event.severity == sev
# ============================================================
# GROUP KEY / INHIBIT KEY TESTS
# ============================================================
def test_group_key_is_event_id(adapter):
"""Group key is the stable avy_{center}_{zone} key."""
event = adapter.to_event(make_avy_event(center_id="SNFAC", zone_name="Banner Summit"))
assert event is not None
assert event.group_key == "avy_SNFAC_banner_summit"
def test_inhibit_keys_match_group_key(adapter):
"""The sole inhibit key equals the group key (Inhibitor does severity tiering)."""
event = adapter.to_event(make_avy_event())
assert event is not None
assert event.inhibit_keys == [event.group_key]
def test_distinct_zones_get_distinct_keys(adapter):
"""Two zones in the same center get distinct group keys."""
e1 = adapter.to_event(make_avy_event(zone_name="Banner Summit"))
e2 = adapter.to_event(make_avy_event(zone_name="Galena Summit"))
assert e1.group_key != e2.group_key
# ============================================================
# CONTENT / FIELD POPULATION TESTS
# ============================================================
def test_populates_core_fields(adapter):
"""Core Event fields are populated from the stored dict."""
evt = make_avy_event(lat=44.31, lon=-115.22)
event = adapter.to_event(evt)
assert event is not None
assert event.source == "avalanche"
assert event.lat == 44.31
assert event.lon == -115.22
assert event.expires == evt["expires"]
assert event.timestamp == evt["fetched_at"]
assert event.id # auto-computed
def test_summary_includes_danger_and_advice(adapter):
"""Summary includes the danger name and travel advice."""
event = adapter.to_event(
make_avy_event(danger_name="Extreme", danger_level=5, travel_advice="Avoid all terrain.")
)
assert event is not None
assert "Extreme" in event.summary
assert "Avoid all terrain." in event.summary
# ============================================================
# DEFENSIVE / NON-EMIT TESTS
# ============================================================
def test_low_danger_returns_none(adapter):
"""Low/Moderate (1-2) danger is not actionable, returns None."""
for level, name in [(1, "Low"), (2, "Moderate")]:
assert adapter.to_event(make_avy_event(danger_level=level, danger_name=name)) is None
def test_no_rating_returns_none(adapter):
"""A no-rating (-1/0) advisory returns None."""
for level in [-1, 0]:
assert adapter.to_event(make_avy_event(danger_level=level, danger_name="No Rating")) is None
def test_missing_danger_level_returns_none(adapter):
"""Missing danger_level returns None."""
evt = make_avy_event()
evt["danger_level"] = None
assert adapter.to_event(evt) is None
def test_missing_centroid_returns_none(adapter):
"""Missing centroid (lat/lon) returns None."""
evt = make_avy_event()
evt["lat"] = None
assert adapter.to_event(evt) is None
def test_missing_event_id_returns_none(adapter):
"""Missing event_id returns None (no stable group key)."""
evt = make_avy_event()
evt["event_id"] = None
assert adapter.to_event(evt) is None
def test_does_not_raise_on_corrupted_dict(adapter):
"""Corrupted dict returns None without raising."""
assert adapter.to_event({"garbage": True}) is None