Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions tests/tools/test_send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,82 @@ def test_whatsapp_routes_via_local_bridge_sender(self):
async_mock.assert_awaited_once_with({"bridge_port": 3000}, chat_id, "hello from hermes")


class TestSendWhatsappJidNormalization:
"""The Baileys bridge expects WhatsApp JIDs (digits@s.whatsapp.net), but
_parse_target_ref returns E.164 strings unchanged. _send_whatsapp must
bridge that format gap before POSTing to the bridge."""

def _post_capture(self):
"""Return (post_mock, session_ctx_mock) that records the JSON body."""
post_resp = MagicMock()
post_resp.status = 200
post_resp.json = AsyncMock(return_value={"messageId": "wamid.ABC"})
post_resp.__aenter__ = AsyncMock(return_value=post_resp)
post_resp.__aexit__ = AsyncMock(return_value=False)

post_mock = MagicMock(return_value=post_resp)
session_obj = MagicMock()
session_obj.post = post_mock
session_obj.__aenter__ = AsyncMock(return_value=session_obj)
session_obj.__aexit__ = AsyncMock(return_value=False)
return post_mock, session_obj

def test_e164_target_is_converted_to_jid(self):
"""+15551234567 -> 15551234567@s.whatsapp.net before HTTP POST."""
from tools.send_message_tool import _send_whatsapp

post_mock, session_obj = self._post_capture()
with patch("aiohttp.ClientSession", return_value=session_obj):
result = asyncio.run(_send_whatsapp({}, "+15551234567", "hi"))

assert result["success"] is True
_, kwargs = post_mock.call_args
assert kwargs["json"]["chatId"] == "15551234567@s.whatsapp.net"
# chat_id in the return value is normalized too, so callers can
# correlate with the eventual bridge state
assert result["chat_id"] == "15551234567@s.whatsapp.net"

def test_already_formed_jid_passes_through(self):
"""Targets already in JID form must not be double-normalized."""
from tools.send_message_tool import _send_whatsapp

post_mock, session_obj = self._post_capture()
with patch("aiohttp.ClientSession", return_value=session_obj):
result = asyncio.run(
_send_whatsapp({}, "15551234567@s.whatsapp.net", "hi")
)

assert result["success"] is True
_, kwargs = post_mock.call_args
assert kwargs["json"]["chatId"] == "15551234567@s.whatsapp.net"

def test_lid_jid_passes_through(self):
"""The newer @lid identity format must not be rewritten."""
from tools.send_message_tool import _send_whatsapp

post_mock, session_obj = self._post_capture()
with patch("aiohttp.ClientSession", return_value=session_obj):
result = asyncio.run(_send_whatsapp({}, "123456789012345@lid", "hi"))

assert result["success"] is True
_, kwargs = post_mock.call_args
assert kwargs["json"]["chatId"] == "123456789012345@lid"

def test_group_jid_passes_through(self):
"""Group JIDs (digits@g.us) must reach the bridge unchanged."""
from tools.send_message_tool import _send_whatsapp

post_mock, session_obj = self._post_capture()
with patch("aiohttp.ClientSession", return_value=session_obj):
result = asyncio.run(
_send_whatsapp({}, "120363045123456789@g.us", "hi")
)

assert result["success"] is True
_, kwargs = post_mock.call_args
assert kwargs["json"]["chatId"] == "120363045123456789@g.us"


class TestSendTelegramHtmlDetection:
"""Verify that messages containing HTML tags are sent with parse_mode=HTML
and that plain / markdown messages use MarkdownV2."""
Expand Down
12 changes: 12 additions & 0 deletions tools/send_message_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,18 @@ async def _send_whatsapp(extra, chat_id, message):
import aiohttp
except ImportError:
return {"error": "aiohttp not installed. Run: pip install aiohttp"}
# Normalize E.164 phone numbers to WhatsApp JID format.
# _parse_target_ref accepts "whatsapp:+15551234567" and returns the
# E.164 string unchanged (correct for signal-cli, which the parser
# comment also covers). The Baileys bridge, however, calls
# sock.sendMessage(chat_id, ...) which expects a JID
# ("15551234567@s.whatsapp.net" or similar) and throws
# "JID decode failed" on a raw E.164 string. Convert here so both
# raw E.164 and already-formed JIDs (passed through as-is) work.
if isinstance(chat_id, str) and chat_id.startswith("+") and "@" not in chat_id:
digits = chat_id[1:].strip()
if digits.isdigit():
chat_id = f"{digits}@s.whatsapp.net"
try:
bridge_port = extra.get("bridge_port", 3000)
async with aiohttp.ClientSession() as session:
Expand Down