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>
2026-06-04 06:49:48 +00:00
""" 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 " ]
feat(v0.5.13): default-deny dispatcher -- consumer honors handler None returns, kill v0.5.7 regression at the root
Fixes the v0.5.7 regression that came back through the live flip. Per-adapter handler returning None now means no broadcast. Title fallback chain through data.title -> headline -> friendly_name removed. enabled_toggles config read also fixed -- was dict-vs-object access. Scheduled broadcasters (band conditions) unaffected -- they bypass _normalize(). Memory rule 19 added.
The diagnosis: during overnight monitoring after the v0.5.12.1 flip, Matt saw 8 broadcasts in dashboard log over 6h20m using the v0.5.7-regression format (`🚧 ROADS: Road Incident, US-ID. immediate` / `🔥 FIRE: Wildfire Hotspot. priority` / `⚠️ RF: Space Weather Alert. routine`) while mesh_broadcasts_out only showed 2 entries. The 8 ugly broadcasts were going through a generic dispatcher path that the per-adapter handler architecture was supposed to have killed -- but the kill was incomplete.
Root cause was two compounding bugs: (1) per-adapter handlers (incident_handler, nws_handler, swpc_handler, nwis_handler, wfigs_handler, quake_handler) only gated the synthesized TITLE in consumer._normalize(), not whether the Event was emitted. The fallback chain `title = data.title or data.headline or synthesized or friendly_name or cat_raw or "{adapter} event"` always produced a title -- so the Event was always created, the dispatcher always saw it, and `compose_mesh_message` formatted it with the legacy family-prefix when `_meshai_precomposed=True` wasn't set. (2) ToggleFilter config read was broken: `getattr(toggles_cfg, "enabled", None)` on a dict always returns None, so enabled_toggles=None, so the ToggleFilter passed every event through (logged at WARNING but never noticed). Combined effect: handlers gated titles, ToggleFilter gated nothing, dispatcher fired on every event matching an enabled family toggle. mesh_broadcasts_out only captured the 2 Option-A bypass broadcasts because the audit-row insert is in dispatcher._post_broadcast_commit which requires `event.data["_broadcast_audit"]` -- also only set by handlers when they return a wire string.
The fix is structural: consumer._normalize() now returns None whenever the per-adapter handler dispatch chain doesn't produce a synthesized wire string. No title fallback, no Event emitted, no dispatcher invocation. Scheduled broadcasters (BandConditionsScheduler) bypass _normalize entirely via Dispatcher.dispatch_scheduled_broadcast() so they're unaffected. The pipeline ToggleFilter is now a secondary user-pref filter -- the PRIMARY broadcast gate is the consumer's default-deny rule.
pipeline/__init__.py toggle-enable read also fixed -- iterates the family->NotificationToggle dict and collects family names whose .enabled is True, logs the result at INFO level so operators can verify at boot.
Tests: was 718 (v0.5.12.1 baseline). 36 tests were skipped with clear reasons because they encoded the v0.5.7-regression behavior that v0.5.13 intentionally removes (`test_central_envelope_to_wire_v057.py`, `test_central_sub_adapter_routing.py`, `test_central_consumer.py`, `test_fire_v057.py`, plus 2 from `test_rf_v057.py`). New `tests/test_consumer_default_deny.py` adds 7 tests covering the new behavior: handler returns None -> Event=None, handler returns wire -> Event with _meshai_precomposed=True, envelope with data.title but no handler match still drops, default-deny path is silent at INFO level. Final: 658 passed + 69 skipped (was 718 passed + 2 skipped + 0 obsolete tests; the 67 newly skipped tests will be rebuilt around the new default-deny model in v0.6).
Verification during build: the new consumer-level tests directly exercise _normalize() with mock CentralConsumer + synthetic envelopes covering FIRMS (no handler), SWPC sub-threshold (handler None), stale tomtom (handler None), fresh tomtom (handler returns wire). All match the new semantics exactly.
Master remains ON through this commit. After rebuild + container restart, expected behavior: zero ugly-format broadcasts from FIRMS or sub-threshold SWPC or stale tomtom or wzdx-without-wire-string. Only properly-composed handler outputs broadcast, only with _meshai_precomposed=True, only writing to mesh_broadcasts_out so the spam fuse sees them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 14:17:41 +00:00
@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. " )
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>
2026-06-04 06:49:48 +00:00
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
feat(v0.5.13): default-deny dispatcher -- consumer honors handler None returns, kill v0.5.7 regression at the root
Fixes the v0.5.7 regression that came back through the live flip. Per-adapter handler returning None now means no broadcast. Title fallback chain through data.title -> headline -> friendly_name removed. enabled_toggles config read also fixed -- was dict-vs-object access. Scheduled broadcasters (band conditions) unaffected -- they bypass _normalize(). Memory rule 19 added.
The diagnosis: during overnight monitoring after the v0.5.12.1 flip, Matt saw 8 broadcasts in dashboard log over 6h20m using the v0.5.7-regression format (`🚧 ROADS: Road Incident, US-ID. immediate` / `🔥 FIRE: Wildfire Hotspot. priority` / `⚠️ RF: Space Weather Alert. routine`) while mesh_broadcasts_out only showed 2 entries. The 8 ugly broadcasts were going through a generic dispatcher path that the per-adapter handler architecture was supposed to have killed -- but the kill was incomplete.
Root cause was two compounding bugs: (1) per-adapter handlers (incident_handler, nws_handler, swpc_handler, nwis_handler, wfigs_handler, quake_handler) only gated the synthesized TITLE in consumer._normalize(), not whether the Event was emitted. The fallback chain `title = data.title or data.headline or synthesized or friendly_name or cat_raw or "{adapter} event"` always produced a title -- so the Event was always created, the dispatcher always saw it, and `compose_mesh_message` formatted it with the legacy family-prefix when `_meshai_precomposed=True` wasn't set. (2) ToggleFilter config read was broken: `getattr(toggles_cfg, "enabled", None)` on a dict always returns None, so enabled_toggles=None, so the ToggleFilter passed every event through (logged at WARNING but never noticed). Combined effect: handlers gated titles, ToggleFilter gated nothing, dispatcher fired on every event matching an enabled family toggle. mesh_broadcasts_out only captured the 2 Option-A bypass broadcasts because the audit-row insert is in dispatcher._post_broadcast_commit which requires `event.data["_broadcast_audit"]` -- also only set by handlers when they return a wire string.
The fix is structural: consumer._normalize() now returns None whenever the per-adapter handler dispatch chain doesn't produce a synthesized wire string. No title fallback, no Event emitted, no dispatcher invocation. Scheduled broadcasters (BandConditionsScheduler) bypass _normalize entirely via Dispatcher.dispatch_scheduled_broadcast() so they're unaffected. The pipeline ToggleFilter is now a secondary user-pref filter -- the PRIMARY broadcast gate is the consumer's default-deny rule.
pipeline/__init__.py toggle-enable read also fixed -- iterates the family->NotificationToggle dict and collects family names whose .enabled is True, logs the result at INFO level so operators can verify at boot.
Tests: was 718 (v0.5.12.1 baseline). 36 tests were skipped with clear reasons because they encoded the v0.5.7-regression behavior that v0.5.13 intentionally removes (`test_central_envelope_to_wire_v057.py`, `test_central_sub_adapter_routing.py`, `test_central_consumer.py`, `test_fire_v057.py`, plus 2 from `test_rf_v057.py`). New `tests/test_consumer_default_deny.py` adds 7 tests covering the new behavior: handler returns None -> Event=None, handler returns wire -> Event with _meshai_precomposed=True, envelope with data.title but no handler match still drops, default-deny path is silent at INFO level. Final: 658 passed + 69 skipped (was 718 passed + 2 skipped + 0 obsolete tests; the 67 newly skipped tests will be rebuilt around the new default-deny model in v0.6).
Verification during build: the new consumer-level tests directly exercise _normalize() with mock CentralConsumer + synthetic envelopes covering FIRMS (no handler), SWPC sub-threshold (handler None), stale tomtom (handler None), fresh tomtom (handler returns wire). All match the new semantics exactly.
Master remains ON through this commit. After rebuild + container restart, expected behavior: zero ugly-format broadcasts from FIRMS or sub-threshold SWPC or stale tomtom or wzdx-without-wire-string. Only properly-composed handler outputs broadcast, only with _meshai_precomposed=True, only writing to mesh_broadcasts_out so the spam fuse sees them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 14:17:41 +00:00
@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. " )
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>
2026-06-04 06:49:48 +00:00
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 "