hermes-agent/tests/gateway/test_telegram_forum_commands.py
Teknium 5600105478 refactor(gateway): migrate slack/dingtalk/whatsapp/matrix/feishu/telegram/wecom/email/sms adapters to bundled plugins
Salvage of PR #41284 onto current main. Relocates the last 9 inline messaging
adapters (+ satellites: telegram_network, feishu_comment/_rules/meeting_invite,
wecom_crypto, wecom_callback) from gateway/platforms/ into self-contained
bundled plugins under plugins/platforms/<x>/, discovered via the platform
registry. Strips the per-platform core touchpoints from gateway/run.py,
gateway/config.py, hermes_cli/gateway.py, hermes_cli/setup.py, and
tools/send_message_tool.py.

Carries forward the migration fixes (explicit enabled:false honored,
get_connected_platforms forces discovery, plugin is_connected via
gateway.get_env_value, logs --component gateway matches plugins.platforms.*,
matrix hidden on Windows).

Additionally ports config keys main added since the PR base: the matrix
plugin's _apply_yaml_config now also covers allowed_users,
ignore_user_patterns, process_notices, and session_scope (the inline
gateway/config.py matrix block gained these in the 1340 commits the PR sat
open; they would otherwise have been silently dropped on deletion).
2026-06-20 10:26:45 -07:00

118 lines
4.4 KiB
Python

"""Tests for lazy forum command registration in TelegramAdapter."""
import asyncio
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from gateway.config import Platform, PlatformConfig
def _make_test_adapter():
"""Build a TelegramAdapter without running __init__."""
from plugins.platforms.telegram.adapter import TelegramAdapter
adapter = object.__new__(TelegramAdapter)
adapter.platform = Platform.TELEGRAM
adapter.config = PlatformConfig(enabled=True, token="***", extra={})
# ``name`` is a property derived from platform.value.title()
adapter._bot = MagicMock()
adapter._bot.set_my_commands = AsyncMock()
adapter._forum_command_registered = set()
adapter._forum_lock = asyncio.Lock()
return adapter
def _forum_message(chat_id=-100, is_forum=True):
return SimpleNamespace(
chat=SimpleNamespace(id=chat_id, is_forum=is_forum),
)
@pytest.mark.asyncio
async def test_ensure_forum_commands_skips_non_forum():
adapter = _make_test_adapter()
msg = _forum_message(is_forum=False)
await adapter._ensure_forum_commands(msg)
adapter._bot.set_my_commands.assert_not_called()
@pytest.mark.asyncio
async def test_ensure_forum_commands_skips_already_registered():
adapter = _make_test_adapter()
adapter._forum_command_registered.add(-100)
msg = _forum_message(is_forum=True)
await adapter._ensure_forum_commands(msg)
adapter._bot.set_my_commands.assert_not_called()
@pytest.mark.asyncio
async def test_ensure_forum_commands_registers_once():
adapter = _make_test_adapter()
msg = _forum_message(chat_id=-123, is_forum=True)
with patch("hermes_cli.commands.telegram_menu_commands") as mock_menu:
mock_menu.return_value = ([("new", "Start new session"), ("help", "Show help")], 0)
with patch("telegram.BotCommand") as MockBotCommand:
instances = []
def _make_cmd(name, desc):
cmd = MagicMock()
cmd.name = name
cmd.description = desc
instances.append(cmd)
return cmd
MockBotCommand.side_effect = _make_cmd
with patch("telegram.BotCommandScopeChat") as MockScope:
# Track the chat_id passed to the BotCommandScopeChat constructor
# so the assertions below see an int instead of a bare MagicMock.
def _make_scope(chat_id):
s = MagicMock()
s.chat_id = chat_id
return s
MockScope.side_effect = _make_scope
await adapter._ensure_forum_commands(msg)
assert -123 in adapter._forum_command_registered
adapter._bot.set_my_commands.assert_awaited_once()
args, kwargs = adapter._bot.set_my_commands.call_args
assert len(args[0]) == 2 # two BotCommand instances
assert kwargs["scope"] is not None
assert isinstance(kwargs["scope"].chat_id, int)
assert kwargs["scope"].chat_id == -123
@pytest.mark.asyncio
async def test_ensure_forum_commands_handles_set_failure():
adapter = _make_test_adapter()
msg = _forum_message(chat_id=-456, is_forum=True)
adapter._bot.set_my_commands.side_effect = Exception("Telegram API error")
with patch("hermes_cli.commands.telegram_menu_commands") as mock_menu:
mock_menu.return_value = ([("new", "Start new session")], 0)
# Should NOT raise despite the API error
await adapter._ensure_forum_commands(msg)
# On failure we don't retry for this chat, so it's added to the set
# to avoid hammering a broken chat.
assert -456 not in adapter._forum_command_registered
@pytest.mark.asyncio
async def test_ensure_forum_commands_race_safety():
"""Two concurrent coroutines must not double-register the same chat."""
adapter = _make_test_adapter()
msg = _forum_message(chat_id=-789, is_forum=True)
with patch("hermes_cli.commands.telegram_menu_commands") as mock_menu:
mock_menu.return_value = ([("new", "Start new session")], 0)
with patch("telegram.BotCommand"):
with patch("telegram.BotCommandScopeChat"):
coro1 = adapter._ensure_forum_commands(msg)
coro2 = adapter._ensure_forum_commands(msg)
await asyncio.gather(coro1, coro2)
# The lock should make this exactly 1 call, not 2.
assert adapter._bot.set_my_commands.await_count == 1