Address feature gaps vs Telegram/Discord/Mattermost adapters: - free_response_chats whitelist to bypass mention gating per-group - strip bot @phone mentions from body before forwarding to agent - unwrap templateMessage/buttonsMessage/listMessage in bridge - info-level log on successful mention pattern compilation - use module-level json import instead of inline import in config - eliminate double _normalize_whatsapp_id call via walrus operator - hoist botIds computation outside per-message loop in bridge
143 lines
4.9 KiB
Python
143 lines
4.9 KiB
Python
import json
|
|
from unittest.mock import AsyncMock
|
|
|
|
from gateway.config import Platform, PlatformConfig, load_gateway_config
|
|
|
|
|
|
def _make_adapter(require_mention=None, mention_patterns=None, free_response_chats=None):
|
|
from gateway.platforms.whatsapp import WhatsAppAdapter
|
|
|
|
extra = {}
|
|
if require_mention is not None:
|
|
extra["require_mention"] = require_mention
|
|
if mention_patterns is not None:
|
|
extra["mention_patterns"] = mention_patterns
|
|
if free_response_chats is not None:
|
|
extra["free_response_chats"] = free_response_chats
|
|
|
|
adapter = object.__new__(WhatsAppAdapter)
|
|
adapter.platform = Platform.WHATSAPP
|
|
adapter.config = PlatformConfig(enabled=True, extra=extra)
|
|
adapter._message_handler = AsyncMock()
|
|
adapter._mention_patterns = adapter._compile_mention_patterns()
|
|
return adapter
|
|
|
|
|
|
def _group_message(body="hello", **overrides):
|
|
data = {
|
|
"isGroup": True,
|
|
"body": body,
|
|
"chatId": "120363001234567890@g.us",
|
|
"mentionedIds": [],
|
|
"botIds": ["15551230000@s.whatsapp.net", "15551230000@lid"],
|
|
"quotedParticipant": "",
|
|
}
|
|
data.update(overrides)
|
|
return data
|
|
|
|
|
|
def test_group_messages_can_be_opened_via_config():
|
|
adapter = _make_adapter(require_mention=False)
|
|
|
|
assert adapter._should_process_message(_group_message("hello everyone")) is True
|
|
|
|
|
|
def test_group_messages_can_require_direct_trigger_via_config():
|
|
adapter = _make_adapter(require_mention=True)
|
|
|
|
assert adapter._should_process_message(_group_message("hello everyone")) is False
|
|
assert adapter._should_process_message(
|
|
_group_message(
|
|
"hi there",
|
|
mentionedIds=["15551230000@s.whatsapp.net"],
|
|
)
|
|
) is True
|
|
assert adapter._should_process_message(
|
|
_group_message(
|
|
"replying",
|
|
quotedParticipant="15551230000@lid",
|
|
)
|
|
) is True
|
|
assert adapter._should_process_message(_group_message("/status")) is True
|
|
|
|
|
|
def test_regex_mention_patterns_allow_custom_wake_words():
|
|
adapter = _make_adapter(require_mention=True, mention_patterns=[r"^\s*chompy\b"])
|
|
|
|
assert adapter._should_process_message(_group_message("chompy status")) is True
|
|
assert adapter._should_process_message(_group_message(" chompy help")) is True
|
|
assert adapter._should_process_message(_group_message("hey chompy")) is False
|
|
|
|
|
|
def test_invalid_regex_patterns_are_ignored():
|
|
adapter = _make_adapter(require_mention=True, mention_patterns=[r"(", r"^\s*chompy\b"])
|
|
|
|
assert adapter._should_process_message(_group_message("chompy status")) is True
|
|
assert adapter._should_process_message(_group_message("hello everyone")) is False
|
|
|
|
|
|
def test_config_bridges_whatsapp_group_settings(monkeypatch, tmp_path):
|
|
hermes_home = tmp_path / ".hermes"
|
|
hermes_home.mkdir()
|
|
(hermes_home / "config.yaml").write_text(
|
|
"whatsapp:\n"
|
|
" require_mention: true\n"
|
|
" mention_patterns:\n"
|
|
" - \"^\\\\s*chompy\\\\b\"\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
|
monkeypatch.delenv("WHATSAPP_REQUIRE_MENTION", raising=False)
|
|
monkeypatch.delenv("WHATSAPP_MENTION_PATTERNS", raising=False)
|
|
|
|
config = load_gateway_config()
|
|
|
|
assert config is not None
|
|
assert config.platforms[Platform.WHATSAPP].extra["require_mention"] is True
|
|
assert config.platforms[Platform.WHATSAPP].extra["mention_patterns"] == [r"^\s*chompy\b"]
|
|
assert __import__("os").environ["WHATSAPP_REQUIRE_MENTION"] == "true"
|
|
assert json.loads(__import__("os").environ["WHATSAPP_MENTION_PATTERNS"]) == [r"^\s*chompy\b"]
|
|
|
|
|
|
def test_free_response_chats_bypass_mention_gating():
|
|
adapter = _make_adapter(
|
|
require_mention=True,
|
|
free_response_chats=["120363001234567890@g.us"],
|
|
)
|
|
|
|
assert adapter._should_process_message(_group_message("hello everyone")) is True
|
|
|
|
|
|
def test_free_response_chats_does_not_bypass_other_groups():
|
|
adapter = _make_adapter(
|
|
require_mention=True,
|
|
free_response_chats=["999999999999@g.us"],
|
|
)
|
|
|
|
assert adapter._should_process_message(_group_message("hello everyone")) is False
|
|
|
|
|
|
def test_dm_always_passes_even_with_require_mention():
|
|
adapter = _make_adapter(require_mention=True)
|
|
|
|
dm = {"isGroup": False, "body": "hello", "botIds": [], "mentionedIds": []}
|
|
assert adapter._should_process_message(dm) is True
|
|
|
|
|
|
def test_mention_stripping_removes_bot_phone_from_body():
|
|
adapter = _make_adapter(require_mention=True)
|
|
|
|
data = _group_message("@15551230000 what is the weather?")
|
|
cleaned = adapter._clean_bot_mention_text(data["body"], data)
|
|
assert "15551230000" not in cleaned
|
|
assert "weather" in cleaned
|
|
|
|
|
|
def test_mention_stripping_preserves_body_when_no_mention():
|
|
adapter = _make_adapter(require_mention=True)
|
|
|
|
data = _group_message("just a normal message")
|
|
cleaned = adapter._clean_bot_mention_text(data["body"], data)
|
|
assert cleaned == "just a normal message"
|