"""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 2–6) must NOT run. We verify by checking the rule name: matrix branch uses toggle: 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"