From 8e6acb9c52106344fe2c8e78431985c24a201e6b Mon Sep 17 00:00:00 2001 From: Matt Johnson Date: Fri, 3 Jul 2026 02:59:21 +0000 Subject: [PATCH] fix(routing): deliver meshcore_broadcast via meshcore_channel through CompositeTransport The hinted _broadcast path passed the channel NAME on the `channel` kwarg, which MeshCoreTransport ignores (it reads meshcore_channel), so meshcore_broadcast silently no-op'd on transport=both configs. Route the meshcore child via meshcore_channel and the meshtastic child via channel. Fix the test that asserted the broken kwarg layout. Add the new delivery types to the remaining enumeration/validation sites (channel-test endpoint, scheduler digest chunking, danger-zone valid set). Co-Authored-By: Claude Opus 4.8 (1M context) --- work/meshai/config.py | 5 +++-- work/meshai/dashboard/api/notification_routes.py | 10 +++++++++- work/meshai/notifications/pipeline/scheduler.py | 2 +- work/meshai/transport/composite_transport.py | 8 ++++++-- work/tests/test_notification_toggles.py | 7 +++++-- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/work/meshai/config.py b/work/meshai/config.py index 7df9bea..5866029 100644 --- a/work/meshai/config.py +++ b/work/meshai/config.py @@ -550,7 +550,7 @@ class NotificationRuleConfig: custom_message: str = "" # Delivery type - delivery_type: str = "" # mesh_broadcast, mesh_dm, email, webhook + delivery_type: str = "" # mesh_broadcast, mesh_dm, meshcore_broadcast, meshcore_dm, email, webhook # Mesh broadcast fields broadcast_channel: int = 0 @@ -697,7 +697,8 @@ _DZ_VALID_ROLES = frozenset({ }) _DZ_VALID_DELIVERY = frozenset({ - "mesh_broadcast", "mesh_dm", "email", "webhook", "none", + "mesh_broadcast", "mesh_dm", "meshcore_broadcast", "meshcore_dm", + "email", "webhook", "none", }) # Hazard families that map onto categories.VALID_TOGGLES. snow is a sub-gate of # weather and flood a sub-gate of seismic (resolved in the correlator), so they diff --git a/work/meshai/dashboard/api/notification_routes.py b/work/meshai/dashboard/api/notification_routes.py index a3f0e55..4dbbf47 100644 --- a/work/meshai/dashboard/api/notification_routes.py +++ b/work/meshai/dashboard/api/notification_routes.py @@ -15,11 +15,15 @@ class TestRequest(BaseModel): class ChannelTestRequest(BaseModel): """Request body for channel connectivity test.""" - type: str # mesh_broadcast, mesh_dm, email, webhook + type: str # mesh_broadcast, mesh_dm, meshcore_broadcast, meshcore_dm, email, webhook # Mesh broadcast channel_index: Optional[int] = 0 # Mesh DM node_ids: Optional[List[str]] = [] + # MeshCore broadcast + meshcore_channel: Optional[str] = None + # MeshCore DM + meshcore_dm_contacts: Optional[List[str]] = [] # Email smtp_host: Optional[str] = "" smtp_port: Optional[int] = 587 @@ -117,6 +121,10 @@ async def test_channel(request: Request, body: ChannelTestRequest): channel_config["channel_index"] = body.channel_index or 0 elif body.type == "mesh_dm": channel_config["node_ids"] = body.node_ids or [] + elif body.type == "meshcore_broadcast": + channel_config["meshcore_channel"] = body.meshcore_channel or "" + elif body.type == "meshcore_dm": + channel_config["meshcore_dm_contacts"] = body.meshcore_dm_contacts or [] elif body.type == "email": channel_config.update({ "smtp_host": body.smtp_host or "", diff --git a/work/meshai/notifications/pipeline/scheduler.py b/work/meshai/notifications/pipeline/scheduler.py index 66f2512..6835710 100644 --- a/work/meshai/notifications/pipeline/scheduler.py +++ b/work/meshai/notifications/pipeline/scheduler.py @@ -126,7 +126,7 @@ class DigestScheduler: channel = self._channel_factory(rule, self._connector) delivery_type = rule.delivery_type - if delivery_type in ("mesh_broadcast", "mesh_dm"): + if delivery_type in ("mesh_broadcast", "mesh_dm", "meshcore_broadcast", "meshcore_dm"): # One deliver call per chunk chunks = digest.mesh_chunks total = len(chunks) diff --git a/work/meshai/transport/composite_transport.py b/work/meshai/transport/composite_transport.py index d6a0c03..e2779b2 100644 --- a/work/meshai/transport/composite_transport.py +++ b/work/meshai/transport/composite_transport.py @@ -266,9 +266,13 @@ class CompositeTransport(MeshTransport): "CompositeTransport: hinted broadcast child %r not connected", name ) return False - child_channel = meshcore_channel if name == "meshcore" else channel try: - return child.send_message(text, destination=None, channel=child_channel) + if name == "meshcore": + return child.send_message( + text, destination=None, meshcore_channel=meshcore_channel + ) + else: + return child.send_message(text, destination=None, channel=channel) except Exception as exc: logger.error( "CompositeTransport: hinted broadcast via %r raised: %s", name, exc diff --git a/work/tests/test_notification_toggles.py b/work/tests/test_notification_toggles.py index 87d65a6..8554255 100644 --- a/work/tests/test_notification_toggles.py +++ b/work/tests/test_notification_toggles.py @@ -229,11 +229,14 @@ def test_meshcore_broadcast_routes_to_meshcore_child_only(): assert rules[0]["delivery_type"] == "meshcore_broadcast" assert rules[0]["meshcore_channel"] == "AIDA" - # MeshCore child received the call with correct transport+channel. + # MeshCore child received the call with the channel NAME on the correct kwarg. assert meshcore_child.send_message.called mc_kwargs = meshcore_child.send_message.call_args.kwargs assert mc_kwargs.get("destination") is None - assert mc_kwargs.get("channel") == "AIDA" # child receives channel=meshcore_channel + # Regression guard for DEFECT 1: channel NAME must be routed via meshcore_channel=. + assert mc_kwargs.get("meshcore_channel") == "AIDA" + # The old broken code passed AIDA via channel=; that must NOT be the routing mechanism. + assert mc_kwargs.get("channel") != "AIDA" # Meshtastic child must NOT have been called. meshtastic_child.send_message.assert_not_called()