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) <noreply@anthropic.com>
This commit is contained in:
Matt Johnson 2026-07-03 02:59:21 +00:00
commit 8e6acb9c52
5 changed files with 24 additions and 8 deletions

View file

@ -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

View file

@ -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 "",

View file

@ -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)

View file

@ -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

View file

@ -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()