hermes-agent/tests/gateway/test_telegram_group_gating.py
Teknium c1fe6339b7 test(telegram): update /cmd@botname assertion for entity-only detection
Current main's _message_mentions_bot() uses MessageEntity-only detection
(commit e330112a), so the test for '/status@hermes_bot' needs to include
a MENTION entity. Real Telegram always emits one for /cmd@botname — the
bot menu and CommandHandler rely on this mechanism.
2026-04-21 03:06:56 -07:00

160 lines
6.3 KiB
Python

import json
from types import SimpleNamespace
from unittest.mock import AsyncMock
from gateway.config import Platform, PlatformConfig, load_gateway_config
def _make_adapter(require_mention=None, free_response_chats=None, mention_patterns=None, ignored_threads=None):
from gateway.platforms.telegram import TelegramAdapter
extra = {}
if require_mention is not None:
extra["require_mention"] = require_mention
if free_response_chats is not None:
extra["free_response_chats"] = free_response_chats
if mention_patterns is not None:
extra["mention_patterns"] = mention_patterns
if ignored_threads is not None:
extra["ignored_threads"] = ignored_threads
adapter = object.__new__(TelegramAdapter)
adapter.platform = Platform.TELEGRAM
adapter.config = PlatformConfig(enabled=True, token="***", extra=extra)
adapter._bot = SimpleNamespace(id=999, username="hermes_bot")
adapter._message_handler = AsyncMock()
adapter._pending_text_batches = {}
adapter._pending_text_batch_tasks = {}
adapter._text_batch_delay_seconds = 0.01
adapter._mention_patterns = adapter._compile_mention_patterns()
return adapter
def _group_message(
text="hello",
*,
chat_id=-100,
thread_id=None,
reply_to_bot=False,
entities=None,
caption=None,
caption_entities=None,
):
reply_to_message = None
if reply_to_bot:
reply_to_message = SimpleNamespace(from_user=SimpleNamespace(id=999))
return SimpleNamespace(
text=text,
caption=caption,
entities=entities or [],
caption_entities=caption_entities or [],
message_thread_id=thread_id,
chat=SimpleNamespace(id=chat_id, type="group"),
reply_to_message=reply_to_message,
)
def _mention_entity(text, mention="@hermes_bot"):
offset = text.index(mention)
return SimpleNamespace(type="mention", offset=offset, length=len(mention))
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 @hermes_bot", entities=[_mention_entity("hi @hermes_bot")])) is True
assert adapter._should_process_message(_group_message("replying", reply_to_bot=True)) is True
# Commands must also respect require_mention when it is enabled
assert adapter._should_process_message(_group_message("/status"), is_command=True) is False
# But commands with @mention still pass (Telegram emits a MENTION entity
# for /cmd@botname — the bot menu and python-telegram-bot's CommandHandler
# rely on this same mechanism)
assert adapter._should_process_message(
_group_message("/status@hermes_bot", entities=[_mention_entity("/status@hermes_bot")])
) is True
# And commands still pass unconditionally when require_mention is disabled
adapter_no_mention = _make_adapter(require_mention=False)
assert adapter_no_mention._should_process_message(_group_message("/status"), is_command=True) is True
def test_free_response_chats_bypass_mention_requirement():
adapter = _make_adapter(require_mention=True, free_response_chats=["-200"])
assert adapter._should_process_message(_group_message("hello everyone", chat_id=-200)) is True
assert adapter._should_process_message(_group_message("hello everyone", chat_id=-201)) is False
def test_ignored_threads_drop_group_messages_before_other_gates():
adapter = _make_adapter(require_mention=False, free_response_chats=["-200"], ignored_threads=[31, "42"])
assert adapter._should_process_message(_group_message("hello everyone", chat_id=-200, thread_id=31)) is False
assert adapter._should_process_message(_group_message("hello everyone", chat_id=-200, thread_id=42)) is False
assert adapter._should_process_message(_group_message("hello everyone", chat_id=-200, thread_id=99)) 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_telegram_group_settings(monkeypatch, tmp_path):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"telegram:\n"
" require_mention: true\n"
" mention_patterns:\n"
" - \"^\\\\s*chompy\\\\b\"\n"
" free_response_chats:\n"
" - \"-123\"\n",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("TELEGRAM_REQUIRE_MENTION", raising=False)
monkeypatch.delenv("TELEGRAM_MENTION_PATTERNS", raising=False)
monkeypatch.delenv("TELEGRAM_FREE_RESPONSE_CHATS", raising=False)
config = load_gateway_config()
assert config is not None
assert __import__("os").environ["TELEGRAM_REQUIRE_MENTION"] == "true"
assert json.loads(__import__("os").environ["TELEGRAM_MENTION_PATTERNS"]) == [r"^\s*chompy\b"]
assert __import__("os").environ["TELEGRAM_FREE_RESPONSE_CHATS"] == "-123"
def test_config_bridges_telegram_ignored_threads(monkeypatch, tmp_path):
hermes_home = tmp_path / ".hermes"
hermes_home.mkdir()
(hermes_home / "config.yaml").write_text(
"telegram:\n"
" ignored_threads:\n"
" - 31\n"
" - \"42\"\n",
encoding="utf-8",
)
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
monkeypatch.delenv("TELEGRAM_IGNORED_THREADS", raising=False)
config = load_gateway_config()
assert config is not None
assert __import__("os").environ["TELEGRAM_IGNORED_THREADS"] == "31,42"