fix: harden WhatsApp target alias salvage

Add a parser-only routing regression that proves raw WhatsApp group JIDs bypass channel-directory resolution and home-channel fallback, include channel_aliases.json in quick state snapshots, harden malformed alias handling, and map Keiron McCammon for release attribution.
This commit is contained in:
Teknium 2026-06-15 05:13:34 -07:00
parent ea49a79633
commit 0d82060c74
6 changed files with 98 additions and 6 deletions

View file

@ -49,12 +49,16 @@ def _apply_channel_aliases(platforms: Dict[str, Any]) -> None:
if not isinstance(id_map, dict):
continue
entries = platforms.setdefault(plat_name, [])
if not isinstance(entries, list):
continue
for chat_id, friendly in id_map.items():
if not friendly:
if not isinstance(friendly, str) or not friendly.strip():
continue
chat_id = str(chat_id)
friendly = friendly.strip()
matched = False
for e in entries:
if e.get("id") == chat_id:
if isinstance(e, dict) and e.get("id") == chat_id:
e["name"] = friendly
matched = True
if not matched:

View file

@ -510,6 +510,7 @@ _QUICK_STATE_FILES = (
"cron/jobs.json",
"gateway_state.json",
"channel_directory.json",
"channel_aliases.json",
"processes.json",
# Pairing stores (generic + per-platform JSONs outside state.db)
"pairing", # legacy location (gateway/pairing.py)

View file

@ -87,6 +87,7 @@ AUTHOR_MAP = {
"dirtyren@users.noreply.github.com": "dirtyren",
"tharushkadinujaya05@gmail.com": "0xneobyte",
"138671361+Veritas-7@users.noreply.github.com": "Veritas-7",
"keiron@onehanded.com": "kmccammon",
"895252509@qq.com": "895252509",
"35259607+zxcasongs@users.noreply.github.com": "zxcasongs",
"alfred@my-cloud.me": "alfred-smith-0",

View file

@ -566,9 +566,13 @@ class TestChannelAliases:
assert names == ["general"]
def test_apply_aliases_handles_malformed_map(self):
"""Non-dict alias entries must not raise."""
"""Non-dict alias maps and non-string aliases must not raise."""
platforms = {"whatsapp": [{"id": "1@g.us", "name": "1", "type": "group"}]}
with patch("gateway.channel_directory._load_channel_aliases",
return_value={"whatsapp": "not-a-dict", "telegram": None}):
return_value={
"whatsapp": "not-a-dict",
"telegram": None,
"signal": {"+15551234567": 123},
}):
_apply_channel_aliases(platforms) # should not raise
assert platforms["whatsapp"][0]["name"] == "1"

View file

@ -1199,6 +1199,9 @@ class TestQuickSnapshot:
(home / "config.yaml").write_text("model:\n provider: openrouter\n")
(home / ".env").write_text("OPENROUTER_API_KEY=test-key-123\n")
(home / "auth.json").write_text('{"providers": {}}\n')
(home / "channel_aliases.json").write_text(
'{"whatsapp": {"120363408391911677@g.us": "general"}}\n'
)
(home / "cron").mkdir()
(home / "cron" / "jobs.json").write_text('{"jobs": []}\n')
@ -1241,6 +1244,13 @@ class TestQuickSnapshot:
snap_id = create_quick_snapshot(hermes_home=hermes_home)
assert (hermes_home / "state-snapshots" / snap_id / "cron" / "jobs.json").exists()
def test_copies_channel_aliases(self, hermes_home):
from hermes_cli.backup import create_quick_snapshot
snap_id = create_quick_snapshot(hermes_home=hermes_home)
copied = hermes_home / "state-snapshots" / snap_id / "channel_aliases.json"
assert copied.exists()
assert "120363408391911677@g.us" in copied.read_text()
def test_missing_files_skipped(self, hermes_home):
from hermes_cli.backup import create_quick_snapshot
snap_id = create_quick_snapshot(hermes_home=hermes_home)

View file

@ -1,10 +1,20 @@
"""Parser-only tests for send_message targets.
"""Parser-only and lightweight routing tests for send_message targets.
These stay separate from ``test_send_message_tool.py`` because that module
skips wholesale when optional Telegram dependencies are not installed.
"""
from tools.send_message_tool import _parse_target_ref
import asyncio
import json
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from gateway.config import Platform
from tools.send_message_tool import _parse_target_ref, send_message_tool
def _run_async_immediately(coro):
return asyncio.run(coro)
def test_photon_e164_target_is_explicit() -> None:
@ -18,3 +28,65 @@ def test_photon_e164_target_is_explicit() -> None:
def test_e164_target_still_requires_phone_platform() -> None:
assert _parse_target_ref("matrix", "+15551234567")[2] is False
def test_whatsapp_group_jid_target_is_explicit() -> None:
chat_id, thread_id, is_explicit = _parse_target_ref(
"whatsapp", "120363408391911677@g.us"
)
assert chat_id == "120363408391911677@g.us"
assert thread_id is None
assert is_explicit is True
def test_whatsapp_native_jids_are_explicit() -> None:
assert _parse_target_ref("whatsapp", "19255551234@s.whatsapp.net")[2] is True
assert _parse_target_ref("whatsapp", "149606612619433@lid")[2] is True
assert _parse_target_ref("whatsapp", "status@broadcast")[2] is True
assert _parse_target_ref("whatsapp", "120363000000000000@newsletter")[2] is True
def test_whatsapp_jid_suffix_only_matches_whatsapp() -> None:
assert _parse_target_ref("telegram", "120363408391911677@g.us")[2] is False
assert _parse_target_ref("signal", "149606612619433@lid")[2] is False
def test_whatsapp_friendly_name_still_uses_directory_resolution() -> None:
assert _parse_target_ref("whatsapp", "general")[2] is False
def test_send_message_routes_whatsapp_group_jid_without_home_fallback() -> None:
whatsapp_cfg = SimpleNamespace(enabled=True, token=None, extra={"api_url": "http://bridge"})
config = SimpleNamespace(
platforms={Platform.WHATSAPP: whatsapp_cfg},
get_home_channel=lambda _platform: SimpleNamespace(chat_id="15551234567@s.whatsapp.net"),
)
with patch("gateway.config.load_gateway_config", return_value=config), \
patch("tools.interrupt.is_interrupted", return_value=False), \
patch("gateway.channel_directory.resolve_channel_name", side_effect=AssertionError("raw JID should not resolve via directory")), \
patch("model_tools._run_async", side_effect=_run_async_immediately), \
patch("tools.send_message_tool._send_to_platform", new=AsyncMock(return_value={"success": True})) as send_mock, \
patch("gateway.mirror.mirror_to_session", return_value=True):
result = json.loads(
send_message_tool(
{
"action": "send",
"target": "whatsapp:120363408391911677@g.us",
"message": "hello group",
}
)
)
assert result["success"] is True
assert "note" not in result
send_mock.assert_awaited_once_with(
Platform.WHATSAPP,
whatsapp_cfg,
"120363408391911677@g.us",
"hello group",
thread_id=None,
media_files=[],
force_document=False,
)