meshai/work/tests/test_region_routing_dispatcher.py
malice 0feb8adaca
fix(region-routing): independent mt/mc region-routing enable switches (#102)
Split the single region_routes.enabled master switch into per-transport
mt_enabled (Meshtastic) and mc_enabled (MeshCore) flags so the two
transports can be region-routed independently. Previously the shared
switch forced MeshCore into the region matrix; with all mc cells null it
routed MeshCore nowhere instead of falling through to the toggle-level
meshcore_channel. The dispatcher is now authoritative per-transport: a
disabled transport falls through to its toggle path, and matched-but-
inactive cells still suppress the toggle for enabled transports. The
destinations delivery branch also honors matrix-handled suppression to
prevent double-broadcast. Loader maps legacy enabled:true to
mt_enabled:true, mc_enabled:false. Each GUI routing page gains its own
master enable toggle.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 23:58:09 -06:00

902 lines
38 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Region-routes matrix dispatcher tests (P3 — authoritative-on-match).
Covers the matrix branch inserted between Section 1 (staleness) and Section 2
(region-scope/toggle-min_severity) in _dispatch_toggles.
NOTE: the test harness does NOT build a CoverageFilter — event.region /
event.regions are set manually on each event object. That is intentional: the
matrix branch resolves against event.region/regions independently of how those
fields were populated.
Test inventory
--------------
1. cell_match_routes_mt_and_mc — a cell with both mt+mc sends two deliveries
2. routine_event_bypasses_toggle_floor — matrix cell floor=routine; toggle.min_severity=priority
3. no_cell_for_region_falls_through — event region not in cells → normal toggle path
4. matrix_disabled_falls_through — enabled=False → byte-identical toggle path
5. per_region_cooldown_independence — cooldown on region A does not block region B
6. per_channel_dedup_independence — failed channel leaves no dedup; retries on next call
7. authoritative_no_double_send — matrix match does NOT also fire toggle default
8. below_floor_cell_suppresses — matched below-floor cell still owns its ENABLED transport (muted); a DISABLED transport falls through
9. dedup_suppresses_repeat_same_channel — same event id + same channel → dedup on second call
10. disabled_cell_suppresses — matched disabled cell still owns its ENABLED transport (muted); a DISABLED transport falls through
"""
import asyncio
import time
import pytest
from meshai.config import Config, RegionRouteMatrix, NotificationDestination
from meshai.notifications.pipeline.dispatcher import Dispatcher
from meshai.notifications.events import make_event
# ------------------------------------------------------------------ helpers
class RecChannel:
"""Channel recorder capturing delivery type, channel values, and success."""
def __init__(self, rec: list, succeed: bool = True):
self.rec = rec
self.succeed = succeed
async def deliver(self, payload, rule):
self.rec.append({
"delivery_type": rule.delivery_type,
"broadcast_channel": getattr(rule, "broadcast_channel", None),
"meshcore_channel": getattr(rule, "meshcore_channel", None),
"name": rule.name,
"message": payload.message if payload else None,
})
return self.succeed
def _make_dispatcher(cfg, succeed=True):
rec: list = []
d = Dispatcher(cfg, lambda rule, conn: RecChannel(rec, succeed), connector=None)
return d, rec
def _dispatch(cfg, event, succeed=True):
d, rec = _make_dispatcher(cfg, succeed=succeed)
asyncio.run(d.dispatch(event))
return d, rec
def _base_cfg(fam="fire", cooldown_s=0, min_severity="priority"):
"""Minimal config: one toggle enabled, no cold-start grace."""
cfg = Config()
cfg.notifications.rules = []
cfg.notifications.cold_start_grace_seconds = 0
t = cfg.notifications.toggles[fam]
t.enabled = True
t.min_severity = min_severity
t.regions = [] # no toggle-level region filter
t.severity_channels = {
"routine": ["mesh_broadcast"],
"priority": ["mesh_broadcast"],
"immediate": ["mesh_broadcast"],
}
t.freshness_seconds = 0 # never stale in tests
t.cooldown_seconds = cooldown_s
t.broadcast_channel = 0 # toggle default channel (different from matrix cells)
return cfg
def _rr(cells: dict, enabled: bool = True,
mt_enabled: bool = None, mc_enabled: bool = None) -> RegionRouteMatrix:
# v0.16.x per-transport split: legacy `enabled=` maps to BOTH transports so
# existing tests keep their "matrix authoritative for mt AND mc" semantics.
# New tests pass mt_enabled=/mc_enabled= explicitly to exercise per-transport
# authority (a DISABLED transport falls through to the toggle path).
if mt_enabled is None:
mt_enabled = enabled
if mc_enabled is None:
mc_enabled = enabled
return RegionRouteMatrix(mt_enabled=mt_enabled, mc_enabled=mc_enabled, cells=cells)
def _ev(fam="fire", region=None, regions=None, severity="immediate",
eid=None, suffix=None):
ev = make_event(
source="test", category="wildfire_incident" if fam == "fire" else "weather_warning",
severity=severity, title="Test event",
)
if eid:
ev.id = eid
if region:
ev.region = region
if regions:
ev.regions = regions
if suffix:
ev.data["_dedup_suffix"] = suffix
return ev
# ============================================================ Test 1
# Cell match → both mt and mc channels deliver
def test_cell_match_routes_mt_and_mc():
"""A cell with mt=3 and mc='sci-fires' must produce two deliveries —
mesh_broadcast on channel 3 and meshcore_broadcast on channel 'sci-fires'."""
cfg = _base_cfg(fam="fire")
cfg.notifications.region_routes = _rr(cells={
"fire": {
"SCI": {"mt": 3, "mc": "sci-fires", "min_severity": "routine", "enabled": True},
}
})
ev = _ev(fam="fire", region="SCI")
_, rec = _dispatch(cfg, ev)
types = {r["delivery_type"] for r in rec}
assert "mesh_broadcast" in types, "must deliver via mesh_broadcast"
assert "meshcore_broadcast" in types, "must deliver via meshcore_broadcast"
assert len(rec) == 2, "exactly two deliveries (mt + mc)"
mb = next(r for r in rec if r["delivery_type"] == "mesh_broadcast")
mcb = next(r for r in rec if r["delivery_type"] == "meshcore_broadcast")
assert mb["broadcast_channel"] == 3
assert mcb["meshcore_channel"] == "sci-fires"
# ============================================================ Test 2
# Routine severity event routes via matrix even when toggle.min_severity=priority
def test_routine_event_bypasses_toggle_floor():
"""Matrix cell min_severity=routine must route a routine-severity event even
when the toggle's own min_severity=priority (Section 2 bypass)."""
cfg = _base_cfg(fam="fire", min_severity="priority")
cfg.notifications.region_routes = _rr(cells={
"fire": {
"SWI": {"mt": 1, "mc": None, "min_severity": "routine", "enabled": True},
}
})
ev = _ev(fam="fire", region="SWI", severity="routine")
_, rec = _dispatch(cfg, ev)
# Matrix delivered at mt=1
assert len(rec) == 1
assert rec[0]["delivery_type"] == "mesh_broadcast"
assert rec[0]["broadcast_channel"] == 1
# ============================================================ Test 3
# Event region not in cells → fall through to normal toggle path
def test_no_cell_for_region_falls_through():
"""When the event's region is not present in fam_cells, _matrix_matched is
None and the dispatcher falls through to the toggle default path."""
cfg = _base_cfg(fam="fire", min_severity="routine")
cfg.notifications.region_routes = _rr(cells={
"fire": {
"SCI": {"mt": 3, "mc": None, "min_severity": "routine", "enabled": True},
}
})
# Region 'SWI' has no cell
ev = _ev(fam="fire", region="SWI")
_, rec = _dispatch(cfg, ev)
# Toggle default path delivers via broadcast_channel=0 (toggle's default)
assert len(rec) == 1
assert rec[0]["delivery_type"] == "mesh_broadcast"
assert rec[0]["broadcast_channel"] == 0 # toggle default, not matrix cell
# ============================================================ Test 4
# matrix disabled → byte-identical to no-matrix behaviour
def test_matrix_disabled_falls_through():
"""region_routes.enabled=False must leave the toggle path completely unchanged."""
cfg = _base_cfg(fam="fire", min_severity="routine")
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": 7, "mc": "x", "min_severity": "routine", "enabled": True}}},
enabled=False,
)
ev = _ev(fam="fire", region="SCI")
_, rec = _dispatch(cfg, ev)
# Toggle default still fires with broadcast_channel=0
assert len(rec) == 1
assert rec[0]["broadcast_channel"] == 0
# ============================================================ Test 5
# Per-region cooldown independence
def test_per_region_cooldown_independence():
"""Cooldown armed for region A must not block region B."""
cfg = _base_cfg(fam="fire", cooldown_s=300, min_severity="routine")
cfg.notifications.region_routes = _rr(cells={
"fire": {
"SCI": {"mt": 3, "mc": None, "min_severity": "routine", "enabled": True},
"SWI": {"mt": 1, "mc": None, "min_severity": "routine", "enabled": True},
}
})
d, rec = _make_dispatcher(cfg)
ev_sci = _ev(fam="fire", region="SCI", eid="ev-sci")
asyncio.run(d.dispatch(ev_sci))
# SCI ch=3 now has cooldown armed
assert len(rec) == 1
assert rec[0]["broadcast_channel"] == 3
ev_swi = _ev(fam="fire", region="SWI", eid="ev-swi")
asyncio.run(d.dispatch(ev_swi))
# SWI ch=1 must still deliver despite SCI cooldown
assert len(rec) == 2
assert rec[1]["broadcast_channel"] == 1
assert d.dispatch_stats()["cooldown_dropped"] == 0
# ============================================================ Test 6
# Per-channel dedup independence: failed channel leaves no dedup trace
def test_per_channel_dedup_failed_channel_retries():
"""A channel that fails delivery must leave no dedup entry, so it retries
next sweep. A distinct (different ch_type/chan) channel that succeeded is
still deduped on the second dispatch."""
cfg = _base_cfg(fam="fire", min_severity="routine")
cfg.notifications.region_routes = _rr(cells={
"fire": {
"SCI": {
"mt": 3, "mc": "sci-fires",
"min_severity": "routine", "enabled": True,
},
}
})
d = Dispatcher.__new__(Dispatcher)
# Track which channels have succeeded to control per-channel return value.
_succeeded: set = set()
_rec: list = []
def _factory(rule, conn):
ch = rule.delivery_type
chan = rule.broadcast_channel if ch == "mesh_broadcast" else rule.meshcore_channel
class _Ch:
async def deliver(self_, payload, rule_):
_rec.append({
"delivery_type": ch,
"chan": chan,
})
# mesh_broadcast ch=3 fails first time only
if ch == "mesh_broadcast" and ("mb", chan) not in _succeeded:
return False
_succeeded.add((ch, chan))
return True
return _Ch()
from meshai.persistence import init_db
import os, tempfile
# Use a fresh DB (conftest already set MESHAI_DB_PATH via monkeypatch)
Dispatcher.__init__(d, cfg, _factory, connector=None)
ev = _ev(fam="fire", region="SCI", eid="dedup-ch-test")
# First dispatch: mesh_broadcast fails, meshcore_broadcast succeeds.
asyncio.run(d.dispatch(ev))
assert len(_rec) == 2 # both attempted
# Dedup keys are 2-tuples (source, "id#suffix|ch_type|chan") — the channel
# is baked into the second element (this shape survives the boot restore).
# meshcore_broadcast succeeded → recorded; mesh_broadcast failed → NOT.
assert any("meshcore_broadcast" in k[1] for k in d._dedup_lru), \
"meshcore_broadcast (success) must be in dedup_lru"
assert not any("|mesh_broadcast|" in k[1] for k in d._dedup_lru), \
"mesh_broadcast (failed) must NOT be in dedup_lru"
# Second dispatch same event: meshcore_broadcast deduped; mesh_broadcast retries.
before = len(_rec)
asyncio.run(d.dispatch(ev))
new_deliveries = _rec[before:]
# Only mesh_broadcast attempted again (meshcore was deduped)
assert any(r["delivery_type"] == "mesh_broadcast" for r in new_deliveries), \
"mesh_broadcast must retry on second dispatch"
assert not any(r["delivery_type"] == "meshcore_broadcast" for r in new_deliveries), \
"meshcore_broadcast must be deduped on second dispatch"
assert d.dispatch_stats()["dedup_dropped"] == 1
# ============================================================ Test 7
# Authoritative-on-match: matrix match must NOT also fire toggle default
def test_authoritative_no_double_send():
"""When the matrix matches, the toggle default (Sections 26) must NOT run.
We verify by checking the rule name: matrix branch uses toggle:<fam> but
the channel is the matrix cell's channel, not the toggle's broadcast_channel."""
cfg = _base_cfg(fam="fire", min_severity="routine")
cfg.notifications.toggles["fire"].broadcast_channel = 0 # toggle default
cfg.notifications.region_routes = _rr(cells={
"fire": {
"SCI": {"mt": 5, "mc": None, "min_severity": "routine", "enabled": True},
}
})
ev = _ev(fam="fire", region="SCI")
_, rec = _dispatch(cfg, ev)
# Exactly one delivery, via matrix cell channel 5 (not toggle default 0)
assert len(rec) == 1
assert rec[0]["broadcast_channel"] == 5, \
"matrix channel (5) must be used, not toggle default (0)"
# ============================================================ Test 8
# Empty chans (every cell below floor) → authoritative no-send, not toggle fallback
def test_below_floor_cell_suppresses_enabled_transport():
"""Authoritative-suppress (restored): a matched cell BELOW its min_severity
floor appends no channel, but the ENABLED transport is still matrix-owned
for this matched event -> its toggle send is suppressed (authoritative
no-send), identical to the null-column case (row5). A DISABLED transport is
not owned and still emits via the toggle."""
cfg = _dual_cfg(fam="fire") # toggle: MT ch0 + MC #aida
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": 3, "mc": None,
"min_severity": "immediate", # cell floor
"enabled": True}}},
mt_enabled=True, mc_enabled=False,
)
# routine event: below the cell floor but at/above the toggle floor (routine)
ev = _ev(fam="fire", region="SCI", severity="routine")
_, rec = _dispatch(cfg, ev)
mesh = [r for r in rec if r["delivery_type"] == "mesh_broadcast"]
mc = [r for r in rec if r["delivery_type"] == "meshcore_broadcast"]
# MT owned by matrix + below floor -> NO mesh_broadcast anywhere (toggle muted)
assert mesh == [], "below-floor MT must be suppressed everywhere, got %r" % (mesh,)
# MC not owned (mc_enabled False) -> emits via toggle #aida
assert len(mc) == 1 and mc[0]["meshcore_channel"] == "#aida"
assert len(rec) == 1
# ============================================================ Test 9
# Dedup suppresses repeat on same channel
def test_dedup_suppresses_repeat_same_channel():
"""Same event id + same channel → first send succeeds, second is deduped."""
cfg = _base_cfg(fam="fire", min_severity="routine")
cfg.notifications.region_routes = _rr(cells={
"fire": {
"SCI": {"mt": 2, "mc": None, "min_severity": "routine", "enabled": True},
}
})
d, rec = _make_dispatcher(cfg)
ev = _ev(fam="fire", region="SCI", eid="dedup-repeat")
asyncio.run(d.dispatch(ev))
assert len(rec) == 1
asyncio.run(d.dispatch(ev))
assert len(rec) == 1 # second suppressed
assert d.dispatch_stats()["dedup_dropped"] == 1
# ============================================================ Test 10
# cell enabled=False is skipped
def test_disabled_cell_suppresses_enabled_transport():
"""Authoritative-suppress (restored): a matched cell with enabled=False
appends no channel, but the ENABLED transport stays matrix-owned -> its
toggle send is suppressed (authoritative no-send). A DISABLED transport is
not owned and still emits via the toggle."""
cfg = _dual_cfg(fam="fire") # toggle: MT ch0 + MC #aida
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": 3, "mc": None,
"min_severity": "routine",
"enabled": False}}},
mt_enabled=True, mc_enabled=False,
)
ev = _ev(fam="fire", region="SCI")
_, rec = _dispatch(cfg, ev)
mesh = [r for r in rec if r["delivery_type"] == "mesh_broadcast"]
mc = [r for r in rec if r["delivery_type"] == "meshcore_broadcast"]
# MT owned by matrix + cell disabled -> NO mesh_broadcast anywhere
assert mesh == [], "disabled-cell MT must be suppressed everywhere, got %r" % (mesh,)
# MC not owned (mc_enabled False) -> emits via toggle #aida
assert len(mc) == 1 and mc[0]["meshcore_channel"] == "#aida"
assert len(rec) == 1
def test_both_enabled_inactive_cell_full_no_send():
"""Both transports enabled + matched cell inactive (below floor) -> both are
matrix-owned, so BOTH toggle sends are suppressed: full authoritative
no-send preserved (nothing emitted on either transport)."""
cfg = _dual_cfg(fam="fire")
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": 3, "mc": "#swi",
"min_severity": "immediate", # cell floor
"enabled": True}}},
mt_enabled=True, mc_enabled=True,
)
# routine event: below the cell floor, at/above the toggle floor (routine)
ev = _ev(fam="fire", region="SCI", severity="routine")
_, rec = _dispatch(cfg, ev)
assert rec == [], \
"both transports matrix-owned + inactive -> nothing sends, got %r" % (rec,)
# ============================================================ Bonus: existing tests
# Smoke-check that non-matrix toggle path is unaffected when region_routes.enabled=False
def test_existing_toggle_path_unaffected_by_inert_matrix():
"""Sanity: a toggle with no region_routes config behaves exactly as before
(sends via the toggle's default broadcast_channel=1)."""
cfg = _base_cfg(fam="weather", min_severity="routine")
cfg.notifications.toggles["weather"].broadcast_channel = 1
# No region_routes set → default RegionRouteMatrix(enabled=False)
ev = make_event(
source="nws", category="weather_warning", severity="priority",
title="Red Flag",
)
_, rec = _dispatch(cfg, ev)
assert len(rec) == 1
assert rec[0]["broadcast_channel"] == 1
# ============================================================ Bonus: regions list
def test_matrix_resolves_from_event_regions_list():
"""event.regions=[...] (multi-region) must also resolve cells, not just
event.region. Both matching regions send to their respective channels."""
cfg = _base_cfg(fam="fire", min_severity="routine")
cfg.notifications.region_routes = _rr(cells={
"fire": {
"SCI": {"mt": 3, "mc": None, "min_severity": "routine", "enabled": True},
"SWI": {"mt": 1, "mc": None, "min_severity": "routine", "enabled": True},
}
})
# event.region=None, event.regions=["SCI","SWI"]
ev = _ev(fam="fire", regions=["SCI", "SWI"])
_, rec = _dispatch(cfg, ev)
# Each region maps to a distinct mt index → two sends
channels = {r["broadcast_channel"] for r in rec}
assert channels == {1, 3}
def test_restart_dedup_key_matches_boot_restore_form():
"""Regression (restart-flood): the matrix dedup key MUST equal the boot-
restore shape — a 2-tuple (source, "id|ch_type|chan"). Dispatcher
construction rehydrates _dedup_lru as 2-tuples; a 4-tuple check key would
miss after every restart and re-broadcast the whole matrix backlog."""
cfg = _base_cfg(fam="fire", cooldown_s=0)
cfg.notifications.region_routes = _rr(
{"fire": {"SCI": {"mt": 3, "mc": None,
"min_severity": "routine", "enabled": True}}}
)
d, rec = _make_dispatcher(cfg)
ev = _ev(fam="fire", region="SCI", eid="restart-test")
# Exactly what dispatcher.__init__ restores from dispatcher_dedup:
d._dedup_lru[("test", "restart-test|mesh_broadcast|3")] = True
asyncio.run(d.dispatch(ev))
mesh_sends = [r for r in rec if r["delivery_type"] == "mesh_broadcast"]
assert mesh_sends == [], (
"matrix must dedup against the restored 2-tuple key; a re-broadcast "
"here is the restart-flood bug"
)
# ============================================================ Defect A tests
# Every matrix broadcast attempt writes a mesh_broadcasts_out row even when
# the event has no _broadcast_audit stamp (native traffic/weather events).
def test_matrix_broadcast_writes_audit_row_without_broadcast_audit():
"""Defect A fix: a native event without _broadcast_audit must still create
a mesh_broadcasts_out row recording the channel, transport, and success."""
from meshai.persistence import get_db
cfg = _base_cfg(fam="roads", cooldown_s=0, min_severity="routine")
cfg.notifications.region_routes = _rr(cells={
"roads": {
"SW Idaho": {"mt": 3, "mc": None, "min_severity": "routine", "enabled": True},
}
})
# Build a native-style event with NO _broadcast_audit on its data.
from meshai.notifications.events import make_event
ev = make_event(
source="wzdx", category="work_zone",
severity="immediate", title="Lane closure on I-84",
)
ev.region = "SW Idaho"
# No ev.data["_broadcast_audit"] set — this is the native adapter case.
assert "_broadcast_audit" not in (ev.data or {}), \
"precondition: native event must not have _broadcast_audit"
d, rec = _make_dispatcher(cfg)
asyncio.run(d.dispatch(ev))
assert len(rec) == 1, "delivery should have succeeded"
# Defect A fix: the audit row must now exist even without _broadcast_audit.
conn = get_db()
rows = conn.execute(
"SELECT channel, transport, success FROM mesh_broadcasts_out"
).fetchall()
assert len(rows) == 1, (
f"expected 1 mesh_broadcasts_out row (got {len(rows)}); "
"Defect A: native events without _broadcast_audit were never audited"
)
row = rows[0]
assert row["channel"] == 3, \
f"audit row must record the matrix cell's channel (3), got {row['channel']}"
assert row["transport"] == "meshtastic", \
f"audit row transport must be 'meshtastic', got {row['transport']}"
assert row["success"] == 1, \
f"audit row success must be 1 (delivery succeeded), got {row['success']}"
def test_matrix_broadcast_audit_row_on_failure():
"""Defect A fix: a failed matrix delivery must still write a success=0 row."""
from meshai.persistence import get_db
cfg = _base_cfg(fam="roads", cooldown_s=0, min_severity="routine")
cfg.notifications.region_routes = _rr(cells={
"roads": {
"SW Idaho": {"mt": 5, "mc": None, "min_severity": "routine", "enabled": True},
}
})
from meshai.notifications.events import make_event
ev = make_event(
source="itd_511", category="road_closure",
severity="immediate", title="Road closed",
)
ev.region = "SW Idaho"
# Dispatcher that always returns False from deliver().
d, rec = _make_dispatcher(cfg, succeed=False)
asyncio.run(d.dispatch(ev))
conn = get_db()
rows = conn.execute(
"SELECT channel, transport, success FROM mesh_broadcasts_out"
).fetchall()
assert len(rows) == 1, "failed delivery must still create an audit row"
assert rows[0]["channel"] == 5
assert rows[0]["success"] == 0, "failed delivery must record success=0"
# ============================================================ Defect B tests
# The matrix cell's channel index must survive the full dispatcher→channel→
# connector→send_queue→blocking_send chain unmodified.
def test_matrix_cell_channel_reaches_connector_send():
"""Defect B verification: the channel index from the matrix cell (3) must
be the exact value passed to connector.send_message_async, not the toggle
default (0) or any other value.
Uses a real MeshtasticTransport (queue path) with a patched
_blocking_mt_send so no radio is required. Asserts that the channel arg
arriving at _blocking_mt_send equals the matrix cell's mt value.
"""
import asyncio
from unittest.mock import patch
from meshai.config import Config, RegionRouteMatrix, ConnectionConfig
from meshai.connector import MeshtasticTransport
from meshai.notifications.channels import create_channel
from meshai.notifications.pipeline.dispatcher import Dispatcher
from meshai.transport.send_queue import RadioSendQueue
# Build a MeshtasticTransport with a real send queue so the full
# enqueue → drain → _blocking_mt_send path is exercised.
conn_cfg = ConnectionConfig(
meshtastic_send_pacing_min_seconds=0.05,
meshtastic_send_pacing_max_seconds=0.09,
)
mt = MeshtasticTransport(conn_cfg)
mt._connected = True # satisfy connected check inside deliver
loop = asyncio.new_event_loop()
pace_min_fn = lambda: getattr(conn_cfg, "meshtastic_send_pacing_min_seconds", 2.2)
pace_max_fn = lambda: getattr(conn_cfg, "meshtastic_send_pacing_max_seconds", 2.6)
mt._mt_queue = RadioSendQueue(pace_min_fn=pace_min_fn, pace_max_fn=pace_max_fn)
mt._mt_queue.start(loop)
mt._loop = loop
sent_channels: list = []
def _fake_blocking_send(text, destination, channel):
sent_channels.append(channel)
return True
cfg = Config()
cfg.notifications.rules = []
cfg.notifications.cold_start_grace_seconds = 0
t = cfg.notifications.toggles["roads"]
t.enabled = True
t.min_severity = "routine"
t.severity_channels = {"routine": ["mesh_broadcast"], "immediate": ["mesh_broadcast"]}
t.freshness_seconds = 0
t.cooldown_seconds = 0
t.broadcast_channel = 0 # toggle default — must NOT reach the radio
cfg.notifications.region_routes = RegionRouteMatrix(
mt_enabled=True, mc_enabled=True,
cells={"roads": {"SW Idaho": {"mt": 3, "mc": None,
"min_severity": "routine", "enabled": True}}},
)
from meshai.notifications.events import make_event
ev = make_event(source="wzdx", category="work_zone",
severity="immediate", title="Test")
ev.region = "SW Idaho"
with patch.object(mt, "_blocking_mt_send", side_effect=_fake_blocking_send):
d = Dispatcher(cfg, create_channel, connector=mt)
loop.run_until_complete(d.dispatch(ev))
loop.run_until_complete(mt._mt_queue.stop())
loop.close()
assert sent_channels, "send must have been called at least once"
assert all(ch == 3 for ch in sent_channels), (
f"ALL sends must use channel 3 (the matrix cell's mt value), "
f"got channels: {sent_channels}. "
f"If any value is 0 it means the toggle default leaked through instead "
f"of the matrix cell's channel."
)
# ============================================================ /api/channels test
# GET /api/channels must work on a CompositeTransport (not just bare MT).
def test_get_channels_composite_transport():
"""Defect: /api/channels read connector._interface which does not exist on
CompositeTransport → always returned []. Fix: resolve the meshtastic child
first via connector.meshtastic_child()."""
from unittest.mock import MagicMock
from fastapi import FastAPI
from fastapi.testclient import TestClient
from meshai.dashboard.api.mesh_routes import router
from meshai.transport.composite_transport import CompositeTransport
from meshai.connector import MeshtasticTransport
from meshai.config import ConnectionConfig
# Build a real CompositeTransport wrapping a fake MeshtasticTransport.
mt = MagicMock(spec=MeshtasticTransport)
mt.transport_name = "meshtastic"
mt.connected = True
# Fake localNode with three channels.
def _make_ch(idx, name, role):
ch = MagicMock()
ch.index = idx
ch.role = role
s = MagicMock()
s.name = name
ch.settings = s
return ch
fake_node = MagicMock()
fake_node.channels = [
_make_ch(0, "LongFast", 1), # PRIMARY
_make_ch(1, "", 0), # DISABLED
_make_ch(3, "SWI Alerts", 2), # SECONDARY
]
fake_interface = MagicMock()
fake_interface.localNode = fake_node
mt._interface = fake_interface
composite = CompositeTransport([mt])
app = FastAPI()
app.include_router(router, prefix="/api")
app.state.connector = composite
client = TestClient(app)
r = client.get("/api/channels")
assert r.status_code == 200
data = r.json()
assert len(data) == 3, f"expected 3 channel entries, got {data}"
by_idx = {c["index"]: c for c in data}
assert by_idx[0]["name"] == "LongFast"
assert by_idx[0]["role"] == "PRIMARY"
assert by_idx[0]["enabled"] is True
assert by_idx[1]["role"] == "DISABLED"
assert by_idx[1]["enabled"] is False
assert by_idx[3]["name"] == "SWI Alerts"
assert by_idx[3]["role"] == "SECONDARY"
assert by_idx[3]["enabled"] is True
# ==================================================================
# v0.16.x — per-transport region-route authority (mt_enabled / mc_enabled)
# ------------------------------------------------------------------
# The matrix is authoritative ONLY for the transport(s) whose master flag is on.
# A DISABLED transport falls through to the toggle path so its alert still goes
# out (this is how MeshCore reaches #aida while mc_enabled is False). These tests
# cover all five rows of the design truth table plus a no-region-match case.
def _dual_cfg(fam="fire", min_severity="routine"):
"""Toggle configured to emit BOTH transports via the toggle path:
mesh_broadcast on channel 0 and meshcore_broadcast on '#aida'. Lets us prove
per-transport matrix authority: a matrix-owned transport is suppressed here
while a matrix-DISABLED transport still emits via this toggle."""
cfg = _base_cfg(fam=fam, min_severity=min_severity)
t = cfg.notifications.toggles[fam]
t.severity_channels = {
"routine": ["mesh_broadcast", "meshcore_broadcast"],
"priority": ["mesh_broadcast", "meshcore_broadcast"],
"immediate": ["mesh_broadcast", "meshcore_broadcast"],
}
t.broadcast_channel = 0 # toggle default MT channel
t.meshcore_channel = "#aida" # toggle default MC channel
return cfg
def test_ptx_row1_mt_only_matrix_mt_toggle_mc():
"""Row 1 (PRODUCTION): mt_enabled=True, mc_enabled=False, cell mt=3, mc=null.
-> MT ch3 via matrix; MC via toggle #aida; NO toggle MT (suppressed)."""
cfg = _dual_cfg(fam="fire")
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": 3, "mc": None,
"min_severity": "routine", "enabled": True}}},
mt_enabled=True, mc_enabled=False,
)
ev = _ev(fam="fire", region="SCI")
_, rec = _dispatch(cfg, ev)
mesh = [r for r in rec if r["delivery_type"] == "mesh_broadcast"]
mc = [r for r in rec if r["delivery_type"] == "meshcore_broadcast"]
# exactly one MT (matrix ch3) — NO double mesh_broadcast
assert len(mesh) == 1, "exactly one mesh_broadcast, got %r" % (mesh,)
assert mesh[0]["broadcast_channel"] == 3, "MT via matrix cell ch3, not toggle 0"
# MC via toggle #aida (mc_enabled False -> matrix did not own it)
assert len(mc) == 1, "meshcore must emit via toggle, got %r" % (mc,)
assert mc[0]["meshcore_channel"] == "#aida"
assert len(rec) == 2
def test_ptx_row2_mc_only_matrix_mc_toggle_mt():
"""Row 2: mt_enabled=False, mc_enabled=True, cell mc='#swi' (mt=9 ignored).
-> MC '#swi' via matrix; MT via toggle ch0; NO toggle MC (suppressed)."""
cfg = _dual_cfg(fam="fire")
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": 9, "mc": "#swi",
"min_severity": "routine", "enabled": True}}},
mt_enabled=False, mc_enabled=True,
)
ev = _ev(fam="fire", region="SCI")
_, rec = _dispatch(cfg, ev)
mesh = [r for r in rec if r["delivery_type"] == "mesh_broadcast"]
mc = [r for r in rec if r["delivery_type"] == "meshcore_broadcast"]
# MC owned by matrix -> '#swi', exactly one, NO double meshcore_broadcast
assert len(mc) == 1, "exactly one meshcore_broadcast, got %r" % (mc,)
assert mc[0]["meshcore_channel"] == "#swi", "MC via matrix cell, not toggle #aida"
# MT falls through to the toggle default ch0 (mt_enabled False; cell mt=9 ignored)
assert len(mesh) == 1, "MT must emit via toggle, got %r" % (mesh,)
assert mesh[0]["broadcast_channel"] == 0
assert len(rec) == 2
def test_ptx_row3_both_enabled_matrix_owns_both():
"""Row 3: both flags True, cell mt=3 mc='#swi'. -> both via matrix; toggle
emits neither (both channel-types suppressed)."""
cfg = _dual_cfg(fam="fire")
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": 3, "mc": "#swi",
"min_severity": "routine", "enabled": True}}},
mt_enabled=True, mc_enabled=True,
)
ev = _ev(fam="fire", region="SCI")
_, rec = _dispatch(cfg, ev)
mesh = [r for r in rec if r["delivery_type"] == "mesh_broadcast"]
mc = [r for r in rec if r["delivery_type"] == "meshcore_broadcast"]
assert len(mesh) == 1 and mesh[0]["broadcast_channel"] == 3
assert len(mc) == 1 and mc[0]["meshcore_channel"] == "#swi"
assert len(rec) == 2, "no toggle duplicates — both transports matrix-owned"
def test_ptx_row4_both_disabled_toggle_owns_both():
"""Row 4: both flags False -> matrix gate skipped; toggle emits BOTH
transports (ch0 + #aida) unchanged."""
cfg = _dual_cfg(fam="fire")
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": 3, "mc": "#swi",
"min_severity": "routine", "enabled": True}}},
mt_enabled=False, mc_enabled=False,
)
ev = _ev(fam="fire", region="SCI")
_, rec = _dispatch(cfg, ev)
mesh = [r for r in rec if r["delivery_type"] == "mesh_broadcast"]
mc = [r for r in rec if r["delivery_type"] == "meshcore_broadcast"]
assert len(mesh) == 1 and mesh[0]["broadcast_channel"] == 0, "toggle MT default"
assert len(mc) == 1 and mc[0]["meshcore_channel"] == "#aida", "toggle MC default"
assert len(rec) == 2
def test_ptx_row5_mt_enabled_null_column_suppresses_mt_entirely():
"""Row 5: mt_enabled=True but cell mt=null -> matrix marks mesh_broadcast
handled (suppressing the toggle MT) yet sends nothing on MT => NO MT at all.
MC (mc_enabled=False) still emits via the toggle #aida."""
cfg = _dual_cfg(fam="fire")
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": None, "mc": None,
"min_severity": "routine", "enabled": True}}},
mt_enabled=True, mc_enabled=False,
)
ev = _ev(fam="fire", region="SCI")
_, rec = _dispatch(cfg, ev)
mesh = [r for r in rec if r["delivery_type"] == "mesh_broadcast"]
mc = [r for r in rec if r["delivery_type"] == "meshcore_broadcast"]
assert mesh == [], "null mt under mt_enabled -> NO MT anywhere (toggle suppressed)"
assert len(mc) == 1 and mc[0]["meshcore_channel"] == "#aida", "MC via toggle"
assert len(rec) == 1
def test_ptx_no_region_match_runs_full_toggle_path():
"""No matching cell for the event region -> _matrix_handled stays empty ->
BOTH transports emit via the toggle path (no suppression)."""
cfg = _dual_cfg(fam="fire")
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": 3, "mc": "#swi",
"min_severity": "routine", "enabled": True}}},
mt_enabled=True, mc_enabled=True,
)
# region SWI has no cell
ev = _ev(fam="fire", region="SWI")
_, rec = _dispatch(cfg, ev)
mesh = [r for r in rec if r["delivery_type"] == "mesh_broadcast"]
mc = [r for r in rec if r["delivery_type"] == "meshcore_broadcast"]
assert len(mesh) == 1 and mesh[0]["broadcast_channel"] == 0
assert len(mc) == 1 and mc[0]["meshcore_channel"] == "#aida"
assert len(rec) == 2
def test_ptx_destination_mesh_broadcast_not_double_when_matrix_owns_mt():
"""Regression (double-MT-broadcast): a family that is matrix-routed for MT
(mt_enabled=True, matched cell with `mt` set) AND whose toggle delivers via a
mesh_broadcast DESTINATION must NOT broadcast on MT twice.
The matrix owns mesh_broadcast for the matched event (it delivers in
Section 1.5 and marks the transport in `_matrix_handled`). The Section 2
destinations branch previously ignored `_matrix_handled`, so the same
mesh_broadcast transport was delivered a SECOND time via the destination.
A destination whose type is a matrix-owned transport must be filtered,
leaving EXACTLY ONE MT send on the matrix cell channel.
"""
cfg = _base_cfg(fam="fire", min_severity="routine")
# Toggle delivers via a shared mesh_broadcast DESTINATION (ch7) rather than
# the inline severity_channels path.
cfg.notifications.destinations = {
"mesh_dest": NotificationDestination(
name="mesh_dest", type="mesh_broadcast", broadcast_channel=7),
}
cfg.notifications.toggles["fire"].destinations = ["mesh_dest"]
# Matrix owns MT for region SCI at channel 3 (mc not owned / not present).
cfg.notifications.region_routes = _rr(
cells={"fire": {"SCI": {"mt": 3, "mc": None,
"min_severity": "routine", "enabled": True}}},
mt_enabled=True, mc_enabled=False,
)
ev = _ev(fam="fire", region="SCI") # immediate severity -> above floor
_, rec = _dispatch(cfg, ev)
mesh = [r for r in rec if r["delivery_type"] == "mesh_broadcast"]
# EXACTLY ONE mesh_broadcast: the matrix cell (ch3), NOT also the
# destination (ch7). Two here is the double-MT-broadcast bug.
assert len(mesh) == 1, "exactly one mesh_broadcast (no double MT), got %r" % (mesh,)
assert mesh[0]["broadcast_channel"] == 3, \
"the single MT send must be the matrix cell (ch3), not the destination (ch7)"
assert all(r["broadcast_channel"] != 7 for r in rec), \
"the mesh_broadcast destination (ch7) must be filtered by _matrix_handled"
assert len(rec) == 1, "only the matrix MT send; no extra destination delivery"