mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 01:14:45 +02:00
fix(avalanche): v0.5.7-avalanche -- Central avalanche check + categories audit
Seventh family of the v0.5.7 NATS-and-categories campaign. Smaller scope than prior families (consumer.py unchanged): the Central side is verifiably empty, and the registry-audit gap is a single-entry add.
FIX 1 -- Central avalanche adapter check: VERIFIED ABSENT in Central v0.10.0. Searched the consumer integration guide (docs/CONSUMER-INTEGRATION.md at v0.10.0-itd-511) -- zero `avalanche` / `NWAC` / `CAIC` references. Searched the producer source tree (src/central/adapters/) -- no avalanche-named adapter files. meshai already accounts for this:
- meshai/central/consumer.py _SUBJECTS_BARE has no `avalanche` key, so
_subjects_for("avalanche", *) returns [] regardless of region.
- CENTRAL_ADAPTER_TO_SOURCE has no avalanche entry on either side.
- _subject_owned() (consumer.py line 334-) explicitly logs a warning
if someone flips avalanche.feed_source=central, then skips
subscribing.
No code change needed for FIX 1. Tests now pin these invariants so a future refactor that introduces an unexpected avalanche Central wire breaks loudly here.
FIX 2 -- ALERT_CATEGORIES avalanche-family audit. Native meshai/env/avalanche.py emits two categories from the NWAC/CAIC danger-level tier:
danger_level >= 4 (High, Extreme) -> avalanche_warning
danger_level == 3 (Considerable) -> avalanche_watch
danger_level <= 2 (Low, Moderate) -> silently dropped (not actionable)
Pre-v0.5.7-avalanche registry had avalanche_warning + avalanche_considerable. avalanche_warning matched the native emit. avalanche_considerable was a LEGACY name for the Considerable-danger tier -- the native code already emits avalanche_watch for that same semantic (verified at meshai/env/avalanche.py:266; tests/test_adapter_avalanche.py:90 asserts the mapping).
So avalanche_watch was MISSING from the registry, leaving the rule editor unable to target danger-level=3 emissions even though they were correctly routed to toggle="avalanche" via the `("avalanche", "avalanche")` prefix fallback.
Added avalanche_watch under toggle="avalanche", default_severity="routine", with a description that points at the Considerable-tier semantics and an example_message matching the live NWAC product phrasing. composer._CATEGORY_EMOJI and _CATEGORY_LABEL gained matching entries so live LoRa rendering shows the right glyph (⛷, label "AVY").
Legacy entry kept: avalanche_considerable remains in the registry as a forward-compat target even though no current code path emits it. Reasoning matches the v0.5.7-rf precedent:
- router.py source-attribution tables (lines 317, 429) reference it
- composer.py emoji + label tables reference it
- A future phase might re-emit avalanche_considerable as a finer-grained
distinction from the generic Watch label; removing the registry entry
would break any user rule currently targeting it.
If avalanche_considerable remains un-emitted by v0.6, file a follow-up cleanup phase to remove it together with the rf-family hf_blackout / tropospheric_ducting legacy entries. test_alert_categories_avalanche_complete uses a SUBSET assertion (native emit ⊆ registry) so the legacy entry is allowed.
Audit table after v0.5.7-avalanche:
Registry avalanche (3):
avalanche_warning (native danger_level >= 4)
avalanche_watch [v0.5.7-avalanche NEW] (native danger_level == 3)
avalanche_considerable (legacy, no current emitter)
Native emit: {avalanche_warning, avalanche_watch} ⊆ Registry -- parity for everything emitted.
Tests
-----
PYTHONPATH=. pytest -q: 442 passed (was 431; +11 net).
- tests/test_avalanche_v057.py (new): _subjects_for("avalanche", *) returns [] for every region input; avalanche absent from _SUBJECTS_BARE and CENTRAL_ADAPTER_TO_SOURCE; flipping avalanche.feed_source=central produces zero subscriptions; avalanche_watch present under toggle="avalanche" with required fields; avalanche_warning + avalanche_considerable still registry-present; native emit set equals {avalanche_warning, avalanche_watch} and is a subset of the registry.
Safe-mode preserved (master off, all family toggles off, all adapters native, central disabled). No live toggle flipped. Not tagging yet -- v0.5.7 tag waits until all families ship.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6c84baf12c
commit
7f8633aed5
3 changed files with 165 additions and 2 deletions
|
|
@ -423,6 +423,21 @@ ALERT_CATEGORIES = {
|
|||
},
|
||||
|
||||
# Environmental - Avalanche
|
||||
# v0.5.7-avalanche audit (test_alert_categories_avalanche_complete enforces parity):
|
||||
# Central v0.10.0 does NOT ship an avalanche adapter (verified against the
|
||||
# guide TOC + producer src tree); avalanche is native-only in meshai. So
|
||||
# the audit is single-source: meshai/env/avalanche.py emits exactly two
|
||||
# categories based on the NWAC/CAIC danger level:
|
||||
# danger_level >= 4 (High, Extreme) -> avalanche_warning
|
||||
# danger_level == 3 (Considerable) -> avalanche_watch
|
||||
# danger_level <= 2 (Low, Moderate) -> silently dropped (not actionable)
|
||||
#
|
||||
# Legacy entry kept: avalanche_considerable has no current emitter -- the
|
||||
# Considerable-danger semantic now ships as avalanche_watch instead. The
|
||||
# legacy registry entry remains UI-selectable as a forward-compat target
|
||||
# (router.py source-attribution tables and composer.py emoji/label tables
|
||||
# still reference it). Queued for cleanup if no emitter materializes by
|
||||
# v0.6.
|
||||
"avalanche_warning": {
|
||||
"name": "Avalanche Danger High",
|
||||
"description": "Avalanche danger level 4 (High) or 5 (Extreme) in your area",
|
||||
|
|
@ -430,6 +445,13 @@ ALERT_CATEGORIES = {
|
|||
"example_message": "⛷ Avalanche Danger HIGH: Sawtooth Zone — avoid avalanche terrain. Natural avalanches likely.",
|
||||
"toggle": "avalanche",
|
||||
},
|
||||
"avalanche_watch": {
|
||||
"name": "Avalanche Danger Considerable",
|
||||
"description": "Avalanche danger level 3 (Considerable) — dangerous conditions on steep slopes; most avalanche fatalities occur at this level. Travel with caution and conservative decision-making.",
|
||||
"default_severity": "routine",
|
||||
"example_message": "⛷ Avalanche Danger CONSIDERABLE: Sawtooth Zone — dangerous conditions on steep slopes. Cautious route-finding required.",
|
||||
"toggle": "avalanche",
|
||||
},
|
||||
"avalanche_considerable": {
|
||||
"name": "Avalanche Danger Considerable",
|
||||
"description": "Avalanche danger level 3 (Considerable) — most fatalities occur at this level",
|
||||
|
|
|
|||
|
|
@ -64,7 +64,8 @@ _CATEGORY_EMOJI: dict[str, str] = {
|
|||
"road_incident": "🚨",
|
||||
# Avalanche
|
||||
"avalanche_warning": "⛷",
|
||||
"avalanche_considerable": "⛷",
|
||||
"avalanche_watch": "⛷", # v0.5.7-avalanche
|
||||
"avalanche_considerable": "⛷", # legacy / forward-compat
|
||||
# Mesh health
|
||||
"infra_offline": "⚠",
|
||||
"critical_node_down": "🚨",
|
||||
|
|
@ -118,7 +119,8 @@ _CATEGORY_LABEL: dict[str, str] = {
|
|||
"road_closure": "ROADS",
|
||||
"traffic_congestion": "ROADS",
|
||||
"avalanche_warning": "AVY",
|
||||
"avalanche_considerable": "AVY",
|
||||
"avalanche_watch": "AVY", # v0.5.7-avalanche
|
||||
"avalanche_considerable": "AVY", # legacy / forward-compat
|
||||
"earthquake_event": "QUAKE",
|
||||
"earthquake": "QUAKE",
|
||||
"critical_node_down": "MESH",
|
||||
|
|
|
|||
139
tests/test_avalanche_v057.py
Normal file
139
tests/test_avalanche_v057.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
"""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):
|
||||
assert _subjects_for("avalanche", region) == [], \
|
||||
f"unexpected subjects for region={region!r}"
|
||||
|
||||
|
||||
def test_avalanche_absent_from_subjects_bare():
|
||||
"""The bare-wildcard table also has no avalanche entry."""
|
||||
assert "avalanche" not in _SUBJECTS_BARE
|
||||
|
||||
|
||||
def test_avalanche_absent_from_central_adapter_remap():
|
||||
"""No Central adapter name remaps to meshai's 'avalanche' source."""
|
||||
assert "avalanche" not in CENTRAL_ADAPTER_TO_SOURCE.values(), \
|
||||
f"unexpected avalanche 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"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue