from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest from telegram import Chat, Message, MessageEntity, Update, User from yuxi.channels.adapters.telegram.adapter import TelegramAdapter from yuxi.channels.adapters.telegram.session import resolve_chat_type, resolve_thread_key from yuxi.channels.models import ( Attachment, ChannelIdentity, ChannelMessage, ChannelResponse, ChannelStatus, ChannelType, ChatType, DeliveryResult, EventType, HealthStatus, MessageType, ) def make_user(id=12345, username="testuser", first_name="Test", last_name="User"): return User(id=id, is_bot=False, first_name=first_name, username=username, last_name=last_name) def make_chat(id=-10012345, chat_type="private", username=None): return Chat(id=id, type=chat_type, username=username) def make_message( message_id=1, text="hello", from_user=None, chat=None, date=None, entities=(), is_topic_message=False, message_thread_id=None, ): from datetime import datetime return Message( message_id=message_id, date=date or datetime.now(), chat=chat or make_chat(), from_user=from_user or make_user(), text=text, entities=entities, is_topic_message=is_topic_message, message_thread_id=message_thread_id, ) def make_update(message=None): return Update(update_id=1, message=message) def make_command_message(text="/start"): from telegram import MessageEntity entities = (MessageEntity(type=MessageEntity.BOT_COMMAND, offset=0, length=len(text)),) return make_message(text=text, entities=entities) class TestTelegramAdapter: @pytest.fixture def adapter(self): config = {"bot_token": "123:abc", "dm_policy": "open", "reply_to_mode": "first"} return TelegramAdapter(config=config) def test_channel_id(self, adapter): assert adapter.channel_id == "telegram" def test_channel_type(self, adapter): assert adapter.channel_type == ChannelType.TELEGRAM def test_capabilities(self, adapter): assert adapter.supports_streaming is True assert adapter.text_chunk_limit == 4096 assert adapter.max_media_size_mb == 100 def test_normalize_text_message(self, adapter): user = make_user() chat = make_chat() msg = make_message(text="hello world", from_user=user, chat=chat) update = make_update(msg) result = adapter.normalize_inbound(update) assert isinstance(result, ChannelMessage) assert result.content == "hello world" assert result.identity.channel_user_id == "12345" assert result.identity.channel_chat_id == "-10012345" assert result.identity.channel_message_id == "1" assert result.message_type == MessageType.TEXT assert result.chat_type == ChatType.DIRECT def test_normalize_command(self, adapter): msg = make_command_message(text="/start") update = make_update(msg) result = adapter.normalize_inbound(update) assert result.message_type == MessageType.COMMAND def test_normalize_group_message(self, adapter): chat = make_chat(id=-100999, chat_type="supergroup") msg = make_message(text="hi group", chat=chat) update = make_update(msg) result = adapter.normalize_inbound(update) assert result.chat_type == ChatType.GROUP assert result.metadata["telegram_chat_type"] == "supergroup" def test_normalize_with_thread_id(self, adapter): chat = make_chat(id=-100999, chat_type="supergroup") msg = make_message(text="topic msg", chat=chat, is_topic_message=True, message_thread_id=42) update = make_update(msg) result = adapter.normalize_inbound(update) assert result.metadata["is_topic_message"] is True assert result.metadata["thread_id"] == "42" def test_normalize_without_message(self, adapter): update = Update(update_id=1) result = adapter.normalize_inbound(update) assert result.content == "(unknown update)" def test_normalize_edited_message(self, adapter): msg = make_message(text="edited") update = Update(update_id=1, edited_message=msg) result = adapter.normalize_inbound(update) assert result.event_type == EventType.MESSAGE_UPDATED assert result.content == "(passthrough)" def test_format_outbound_text(self, adapter): identity = ChannelIdentity( channel_id="telegram", channel_type=ChannelType.TELEGRAM, channel_user_id="12345", channel_chat_id="-10012345", ) response = ChannelResponse(identity=identity, content="hello world") payload = adapter.format_outbound(response) assert payload["chat_id"] == "-10012345" assert "text" in payload assert payload["parse_mode"] == "HTML" assert "link_preview_options" in payload def test_format_outbound_with_reply(self, adapter): identity = ChannelIdentity( channel_id="telegram", channel_type=ChannelType.TELEGRAM, channel_user_id="12345", channel_chat_id="-10012345", ) response = ChannelResponse(identity=identity, content="reply", reply_to_message_id="10") payload = adapter.format_outbound(response) assert payload["reply_to_message_id"] == 10 def test_format_outbound_with_thread(self, adapter): identity = ChannelIdentity( channel_id="telegram", channel_type=ChannelType.TELEGRAM, channel_user_id="12345", channel_chat_id="-10012345", ) response = ChannelResponse(identity=identity, content="thread msg", metadata={"thread_id": "42"}) payload = adapter.format_outbound(response) assert payload["message_thread_id"] == "42" @pytest.mark.asyncio async def test_health_check_no_app(self): config = {"bot_token": "123:abc"} adapter = TelegramAdapter(config=config) result = await adapter.health_check() assert result.status == "unhealthy" @pytest.mark.asyncio async def test_pre_connect_invalid_token(self): config = {"bot_token": "invalid:token"} adapter = TelegramAdapter(config=config) result = await adapter.pre_connect() assert result["status"] == "error" @pytest.mark.asyncio async def test_pre_connect_missing_token(self): adapter = TelegramAdapter(config={}) result = await adapter.pre_connect() assert result["status"] == "error" assert "Missing bot_token" in result["message"] class TestSessionResolution: def test_resolve_chat_type_private(self): assert resolve_chat_type("private") == ChatType.DIRECT def test_resolve_chat_type_group(self): assert resolve_chat_type("group") == ChatType.GROUP def test_resolve_chat_type_supergroup(self): assert resolve_chat_type("supergroup") == ChatType.GROUP def test_resolve_chat_type_channel(self): assert resolve_chat_type("channel") == ChatType.GUILD_CHANNEL def test_resolve_thread_key_without_thread(self): key = resolve_thread_key("123", None) assert key == "telegram:123" def test_resolve_thread_key_with_thread(self): key = resolve_thread_key("123", "42") assert key == "telegram:123:topic:42"