mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 01:14:45 +02:00
fix(rf): v0.5.7-rf -- SWPC subject validation + protons severity=0 documentation + categories audit
Sixth family of the v0.5.7 NATS-and-categories campaign. RF family = native ducting calculator + three Central SWPC adapters (swpc_alerts, swpc_kindex, swpc_protons), all umbrella-subscribed under `central.space.>`.
Per the family-by-family pattern: cross-checked every prompt assumption against the Central v0.10.0 guide before implementing. The big surprise this phase: FIX 1 was already correct (no NATS-syntax bug to fix), and FIX 2 was a non-bug too (severity=0 already routes safely). The real work was FIX 3 -- four missing registry entries that meshai emits but the rule editor couldn't target.
FIX 1 -- SWPC subject pattern (already correct; pinned). Per Central v0.10.0 guide §swpc_alerts / §swpc_kindex / §swpc_protons, all three adapters publish under the `central.space.>` umbrella with no region in subject (space weather is planetary):
swpc_alerts: central.space.alert.<product_id> (4 tokens, product_id tail)
swpc_kindex: central.space.kindex (3 tokens, fixed)
swpc_protons: central.space.proton_flux (3 tokens, fixed)
`_subjects_for("swpc", region)` already returned `["central.space.>"]` ignoring region (v0.5.4 work got this right). Added an explanatory inline comment near the table entry calling out each adapter's concrete subject + the universal severity=0 contract (next fix), plus a test pinning the umbrella + region-ignored behavior + coverage of each per-adapter subject form. Future "let me add a region tail here" refactors will fail loudly.
FIX 2 -- swpc_protons severity=0 routing (non-bug; regression-guard pin). The prompt described a "severity=0 silently dropped" failure mode. Investigation: no such bug exists in current code.
- All three SWPC adapters publish severity=0 in the live guide samples.
- consumer.map_severity already maps 0 -> "routine" (the `if sev >= 3:`
immediate clamp doesn't hit; falls through to the default return).
- NotificationToggle.severity_channels is dict-keyed by severity STRING
(locked in by v0.5.7-seismic test_severity_channels_is_string_keyed_no_int_indexerror_risk);
"routine" is a valid key with no IndexError vector.
Three things tightened anyway: (a) inline comment near the swpc subject entry documenting "all three publish severity=0 -> routine per guide examples"; (b) end-to-end synthetic envelope test for swpc_protons injection (severity=0 in, ev.severity="routine" / ev.category="solar_radiation_storm" / ev.source="swpc" out, no exception); (c) parallel test for swpc_kindex confirming a second SWPC adapter wires identically.
FIX 3 -- ALERT_CATEGORIES rf_propagation audit. Pre-v0.5.7-rf registry had three entries under toggle="rf_propagation": hf_blackout, geomagnetic_storm, tropospheric_ducting. Audit:
Native ducting.py emits via _TIER_CATEGORY:
super_refraction -> rf_anomalous_propagation
duct -> rf_ducting_enhancement
surface_duct -> rf_ducting_enhancement
Central path via map_category:
space.alert.* -> rf_propagation_alert (swpc_alerts)
space.kindex -> geomagnetic_storm (swpc_kindex; already in registry)
space.proton_flux -> solar_radiation_storm (swpc_protons)
space.* catchall -> geomagnetic_storm
Four categories emitted but missing from the registry -- rule editor couldn't target them. Added all four under toggle="rf_propagation" with name + description + default_severity + example_message matching the guide-documented behavior:
rf_anomalous_propagation (routine, ducting super_refraction tier)
rf_ducting_enhancement (priority, ducting duct + surface_duct tiers)
rf_propagation_alert (priority, NOAA SWPC space-weather product)
solar_radiation_storm (priority, GOES proton flux S-scale)
composer.py emoji + label tables gained matching entries so live LoRa rendering shows the right glyphs (📡 for ducting forms, ⚠ for SWPC alerts, 🌐 for solar radiation, all labelled "RF").
Legacy entries kept (forward-compat / no current emitter): hf_blackout and tropospheric_ducting remain in the registry as selectable rule targets even though no current code path emits them. Reasoning:
- hf_blackout: HF-specific R-scale parsing of swpc_alerts.message could
re-introduce this emission in a future phase; removing the registry
entry would break any user rule currently configured to target it.
- tropospheric_ducting: legacy name superseded by rf_ducting_enhancement
in native ducting.py; same forward-compat concern -- a future phase
may emit a "tropospheric" specialization separate from generic ducts.
If either remains un-emitted by v0.6, file a follow-up cleanup phase to remove. Test_alert_categories_rf_complete uses a SUBSET assertion (emit set ⊆ registry) rather than equality so legacy entries are allowed.
Audit table after v0.5.7-rf:
Registry rf_propagation (7):
hf_blackout (legacy, no current emitter)
geomagnetic_storm (central swpc_kindex + catchall)
tropospheric_ducting (legacy, no current emitter)
rf_anomalous_propagation [v0.5.7-rf NEW] (native ducting super_refraction)
rf_ducting_enhancement [v0.5.7-rf NEW] (native ducting duct + surface_duct)
rf_propagation_alert [v0.5.7-rf NEW] (central swpc_alerts)
solar_radiation_storm [v0.5.7-rf NEW] (central swpc_protons)
Emit set ⊆ Registry: TRUE (no orphan emissions).
Tests
-----
PYTHONPATH=. pytest -q: 431 passed (was 413; +18 net).
- tests/test_rf_v057.py (new): umbrella subject is `central.space.>` for all regions; per-adapter published subjects all match; map_severity(0) -> "routine"; NotificationToggle.severity_channels dict-keyed (no IndexError); synthetic swpc_protons + swpc_kindex envelopes route cleanly with severity=0; four new rf_propagation entries all registry-present with required fields; geomagnetic_storm still mapped from space.kindex; map_category routing pinned for each SWPC adapter; native ducting + central SWPC emit sets are subsets of registry rf entries.
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
730b03099a
commit
6c84baf12c
4 changed files with 307 additions and 1 deletions
|
|
@ -149,6 +149,16 @@ def _subjects_for(adapter: str, region: Optional[str]) -> list[str]:
|
||||||
# publishes `central.hydro.<param>.<agency>.<site>.<region>`.
|
# publishes `central.hydro.<param>.<agency>.<site>.<region>`.
|
||||||
"usgs": [f"central.hydro.*.*.*.{region}",
|
"usgs": [f"central.hydro.*.*.*.{region}",
|
||||||
"central.hydro.*.*.*.unknown"],
|
"central.hydro.*.*.*.unknown"],
|
||||||
|
# SWPC space weather: planetary (no region). The umbrella subject
|
||||||
|
# central.space.> catches all three SWPC adapters per Central v0.10.0
|
||||||
|
# guide §swpc_alerts/§swpc_kindex/§swpc_protons:
|
||||||
|
# - swpc_alerts: central.space.alert.<product_id>
|
||||||
|
# - swpc_kindex: central.space.kindex (fixed)
|
||||||
|
# - swpc_protons: central.space.proton_flux (fixed)
|
||||||
|
# All three publish severity=0 by default (verified against the
|
||||||
|
# live samples in the guide); map_severity(0) -> "routine", which
|
||||||
|
# routes through the NotificationToggle's "routine" severity_channels
|
||||||
|
# entry (dict is string-keyed, no IndexError risk).
|
||||||
"swpc": ["central.space.>"],
|
"swpc": ["central.space.>"],
|
||||||
# Convention B (bare state) — shared by traffic family (wzdx,
|
# Convention B (bare state) — shared by traffic family (wzdx,
|
||||||
# tomtom_incidents, state_511_atis). Single-token `*` matches the
|
# tomtom_incidents, state_511_atis). Single-token `*` matches the
|
||||||
|
|
|
||||||
|
|
@ -226,7 +226,50 @@ ALERT_CATEGORIES = {
|
||||||
"toggle": "weather",
|
"toggle": "weather",
|
||||||
},
|
},
|
||||||
|
|
||||||
# Environmental - Space Weather
|
# Environmental - Space Weather / RF Propagation
|
||||||
|
# v0.5.7-rf audit (test_alert_categories_rf_complete enforces parity):
|
||||||
|
# Native: ducting.py -> rf_anomalous_propagation (super_refraction tier);
|
||||||
|
# ducting.py -> rf_ducting_enhancement (duct + surface_duct tiers).
|
||||||
|
# Central path (via map_category): space.alert -> rf_propagation_alert
|
||||||
|
# (swpc_alerts); space.kindex -> geomagnetic_storm (swpc_kindex);
|
||||||
|
# space.proton_flux -> solar_radiation_storm (swpc_protons);
|
||||||
|
# catchall space.* -> geomagnetic_storm.
|
||||||
|
# All three SWPC adapters publish severity=0 -> "routine" per guide
|
||||||
|
# §swpc_alerts/§swpc_kindex/§swpc_protons live samples.
|
||||||
|
#
|
||||||
|
# Legacy entries kept: hf_blackout and tropospheric_ducting have no
|
||||||
|
# current emitter (ducting.py was renamed to rf_ducting_enhancement,
|
||||||
|
# and HF-blackout-specific parsing of swpc_alerts.message is deferred
|
||||||
|
# to a future phase). They remain UI-selectable as forward-compatible
|
||||||
|
# rule targets; queued for cleanup if no emitter materializes.
|
||||||
|
"rf_anomalous_propagation": {
|
||||||
|
"name": "RF Anomalous Propagation",
|
||||||
|
"description": "Super-refractive atmospheric layer affecting VHF/UHF propagation — sub-standard refractive conditions, mostly affects line-of-sight links",
|
||||||
|
"default_severity": "routine",
|
||||||
|
"example_message": "📡 Anomalous Propagation: Super-refraction detected, dM/dz -45 M-units/km, ~80m thick layer. VHF/UHF links may show enhanced range.",
|
||||||
|
"toggle": "rf_propagation",
|
||||||
|
},
|
||||||
|
"rf_ducting_enhancement": {
|
||||||
|
"name": "RF Ducting Enhancement",
|
||||||
|
"description": "Tropospheric duct trapping VHF/UHF signals — extended range, signals propagate well beyond the normal radio horizon",
|
||||||
|
"default_severity": "priority",
|
||||||
|
"example_message": "📡 Ducting Enhancement: Surface duct detected, base 0 m, ~120 m thick. VHF/UHF extended range, expect signals well beyond horizon.",
|
||||||
|
"toggle": "rf_propagation",
|
||||||
|
},
|
||||||
|
"rf_propagation_alert": {
|
||||||
|
"name": "Space Weather Alert",
|
||||||
|
"description": "NOAA SWPC space weather alert/watch/warning — geomagnetic storm scales (G1-G5), radiation storms (S), radio blackouts (R), or summaries. Full operational text in event body.",
|
||||||
|
"default_severity": "priority",
|
||||||
|
"example_message": "⚠ Space Weather Alert (A20F): WATCH — Geomagnetic Storm Category G1 Predicted. Apr 25: G1 (Minor). Aurora possible at high latitudes.",
|
||||||
|
"toggle": "rf_propagation",
|
||||||
|
},
|
||||||
|
"solar_radiation_storm": {
|
||||||
|
"name": "Solar Radiation Storm",
|
||||||
|
"description": "GOES proton flux above S-scale threshold — S1 ≥10 pfu at ≥10 MeV; S2 ≥100; S3 ≥1000; impacts HF over polar regions and satellite operations",
|
||||||
|
"default_severity": "priority",
|
||||||
|
"example_message": "🌐 Solar Radiation Storm: GOES-19 proton flux 12.5 pfu at ≥10 MeV (S1 threshold). HF over polar regions degraded.",
|
||||||
|
"toggle": "rf_propagation",
|
||||||
|
},
|
||||||
"hf_blackout": {
|
"hf_blackout": {
|
||||||
"name": "HF Radio Blackout",
|
"name": "HF Radio Blackout",
|
||||||
"description": "R3+ solar flare degrading HF propagation on sunlit side",
|
"description": "R3+ solar flare degrading HF propagation on sunlit side",
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,11 @@ _CATEGORY_EMOJI: dict[str, str] = {
|
||||||
"hf_blackout": "⚠",
|
"hf_blackout": "⚠",
|
||||||
"geomagnetic_storm": "🌐",
|
"geomagnetic_storm": "🌐",
|
||||||
"tropospheric_ducting": "📡",
|
"tropospheric_ducting": "📡",
|
||||||
|
# v0.5.7-rf additions:
|
||||||
|
"rf_anomalous_propagation": "📡",
|
||||||
|
"rf_ducting_enhancement": "📡",
|
||||||
|
"rf_propagation_alert": "⚠",
|
||||||
|
"solar_radiation_storm": "🌐",
|
||||||
# Fire (v0.5.7-fire: fire_proximity/wildfire_proximity removed; aligned
|
# Fire (v0.5.7-fire: fire_proximity/wildfire_proximity removed; aligned
|
||||||
# to the new registry entries wildfire_hotspot + wildfire_incident).
|
# to the new registry entries wildfire_hotspot + wildfire_incident).
|
||||||
"wildfire_hotspot": "🔥",
|
"wildfire_hotspot": "🔥",
|
||||||
|
|
@ -105,6 +110,11 @@ _CATEGORY_LABEL: dict[str, str] = {
|
||||||
"hf_blackout": "RF",
|
"hf_blackout": "RF",
|
||||||
"geomagnetic_storm": "RF",
|
"geomagnetic_storm": "RF",
|
||||||
"tropospheric_ducting": "RF",
|
"tropospheric_ducting": "RF",
|
||||||
|
# v0.5.7-rf additions:
|
||||||
|
"rf_anomalous_propagation": "RF",
|
||||||
|
"rf_ducting_enhancement": "RF",
|
||||||
|
"rf_propagation_alert": "RF",
|
||||||
|
"solar_radiation_storm": "RF",
|
||||||
"road_closure": "ROADS",
|
"road_closure": "ROADS",
|
||||||
"traffic_congestion": "ROADS",
|
"traffic_congestion": "ROADS",
|
||||||
"avalanche_warning": "AVY",
|
"avalanche_warning": "AVY",
|
||||||
|
|
|
||||||
243
tests/test_rf_v057.py
Normal file
243
tests/test_rf_v057.py
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
"""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"]
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue