"""Tests for text message batching across all gateway adapters. When a user sends a long message, the messaging client splits it at the platform's character limit. Each adapter should buffer rapid successive text messages from the same session and aggregate them before dispatching. Covers: Discord, Matrix, WeCom, and the adaptive delay logic for Telegram and Feishu. """ import asyncio from unittest.mock import AsyncMock import pytest from gateway.config import Platform, PlatformConfig from gateway.platforms.base import SessionSource from gateway.platforms.event import MessageEvent, MessageType # ===================================================================== # Helpers # ===================================================================== def _make_event( text: str, platform: Platform, chat_id: str = "12345", msg_type: MessageType = MessageType.TEXT, ) -> MessageEvent: return MessageEvent( text=text, message_type=msg_type, source=SessionSource(platform=platform, chat_id=chat_id, chat_type="dm"), ) # ===================================================================== # Discord text batching # ===================================================================== def _make_discord_adapter(): """Create a minimal DiscordAdapter for testing text batching.""" from plugins.platforms.discord.adapter import DiscordAdapter config = PlatformConfig(enabled=True, token="test-token") adapter = object.__new__(DiscordAdapter) adapter._platform = adapter.platform = Platform.DISCORD adapter.config = config adapter._pending_text_batches = {} adapter._pending_text_batch_tasks = {} adapter._text_batch_delay_seconds = 0.1 # fast for tests adapter._text_batch_split_delay_seconds = 0.3 # fast for tests adapter._active_sessions = {} adapter._pending_messages = {} adapter._message_handler = AsyncMock() adapter.handle_message = AsyncMock() return adapter class TestDiscordTextBatching: @pytest.mark.asyncio async def test_single_message_dispatched_after_delay(self): adapter = _make_discord_adapter() event = _make_event("hello world", Platform.DISCORD) adapter._enqueue_text_event(event) # Not dispatched yet adapter.handle_message.assert_not_called() # Wait for flush await asyncio.sleep(0.2) adapter.handle_message.assert_called_once() dispatched = adapter.handle_message.call_args[0][0] assert dispatched.text == "hello world" @pytest.mark.asyncio async def test_split_messages_aggregated(self): """Two rapid messages from the same chat should be merged.""" adapter = _make_discord_adapter() adapter._enqueue_text_event(_make_event("Part one of a long", Platform.DISCORD)) await asyncio.sleep(0.02) adapter._enqueue_text_event(_make_event("message that was split.", Platform.DISCORD)) adapter.handle_message.assert_not_called() await asyncio.sleep(0.2) adapter.handle_message.assert_called_once() text = adapter.handle_message.call_args[0][0].text assert "Part one" in text assert "split" in text # ===================================================================== # Matrix text batching # ===================================================================== def _make_matrix_adapter(): """Create a minimal MatrixAdapter for testing text batching.""" from plugins.platforms.matrix.adapter import MatrixAdapter config = PlatformConfig(enabled=True, token="test-token") adapter = object.__new__(MatrixAdapter) adapter._platform = adapter.platform = Platform.MATRIX adapter.config = config adapter._pending_text_batches = {} adapter._pending_text_batch_tasks = {} adapter._text_batch_delay_seconds = 0.1 adapter._text_batch_split_delay_seconds = 0.3 adapter._active_sessions = {} adapter._pending_messages = {} adapter._message_handler = AsyncMock() adapter.handle_message = AsyncMock() return adapter class TestMatrixTextBatching: @pytest.mark.asyncio async def test_single_message_dispatched_after_delay(self): adapter = _make_matrix_adapter() event = _make_event("hello world", Platform.MATRIX) adapter._enqueue_text_event(event) adapter.handle_message.assert_not_called() await asyncio.sleep(0.2) adapter.handle_message.assert_called_once() assert adapter.handle_message.call_args[0][0].text == "hello world" @pytest.mark.asyncio async def test_split_messages_aggregated(self): adapter = _make_matrix_adapter() adapter._enqueue_text_event(_make_event("first part", Platform.MATRIX)) await asyncio.sleep(0.02) adapter._enqueue_text_event(_make_event("second part", Platform.MATRIX)) adapter.handle_message.assert_not_called() await asyncio.sleep(0.2) adapter.handle_message.assert_called_once() text = adapter.handle_message.call_args[0][0].text assert "first part" in text assert "second part" in text # ===================================================================== # WeCom text batching # ===================================================================== def _make_wecom_adapter(): """Create a minimal WeComAdapter for testing text batching.""" from plugins.platforms.wecom.adapter import WeComAdapter config = PlatformConfig(enabled=True, token="test-token") adapter = object.__new__(WeComAdapter) adapter._platform = adapter.platform = Platform.WECOM adapter.config = config adapter._pending_text_batches = {} adapter._pending_text_batch_tasks = {} adapter._text_batch_delay_seconds = 0.1 adapter._text_batch_split_delay_seconds = 0.3 adapter._active_sessions = {} adapter._pending_messages = {} adapter._message_handler = AsyncMock() adapter.handle_message = AsyncMock() return adapter class TestWeComTextBatching: @pytest.mark.asyncio async def test_single_message_dispatched_after_delay(self): adapter = _make_wecom_adapter() event = _make_event("hello world", Platform.WECOM) adapter._enqueue_text_event(event) adapter.handle_message.assert_not_called() await asyncio.sleep(0.2) adapter.handle_message.assert_called_once() assert adapter.handle_message.call_args[0][0].text == "hello world" @pytest.mark.asyncio async def test_split_messages_aggregated(self): adapter = _make_wecom_adapter() adapter._enqueue_text_event(_make_event("first part", Platform.WECOM)) await asyncio.sleep(0.02) adapter._enqueue_text_event(_make_event("second part", Platform.WECOM)) adapter.handle_message.assert_not_called() await asyncio.sleep(0.2) adapter.handle_message.assert_called_once() text = adapter.handle_message.call_args[0][0].text assert "first part" in text assert "second part" in text