"""Regression tests for long slash-command pastes split by Telegram clients. Telegram splits messages above 4096 chars into multiple updates. A long ``/queue `` paste arrives as a COMMAND chunk near the limit plus plain TEXT continuation chunk(s). Before the fix, `_handle_command` dispatched the command chunk immediately, orphaning the continuation — which then landed as a separate plain message and interrupted the running agent. Near-limit command chunks must route through the text-batching pipeline so continuations merge in before dispatch; short commands stay immediate. """ import asyncio from types import SimpleNamespace from unittest.mock import AsyncMock import pytest from gateway.config import Platform, PlatformConfig def _make_adapter(): from plugins.platforms.telegram.adapter import TelegramAdapter adapter = object.__new__(TelegramAdapter) adapter.platform = Platform.TELEGRAM adapter.config = PlatformConfig(enabled=True, token="fake-token", extra={}) adapter._bot = SimpleNamespace(id=999, username="test_bot") adapter._message_handler = AsyncMock() adapter._pending_text_batches = {} adapter._pending_text_batch_tasks = {} adapter._text_batch_delay_seconds = 0.01 adapter._text_batch_split_delay_seconds = 0.05 adapter._mention_patterns = adapter._compile_mention_patterns() adapter._forum_lock = asyncio.Lock() adapter._forum_command_registered = set() adapter._active_sessions = {} adapter._pending_messages = {} adapter.handle_message = AsyncMock() return adapter def _make_message(text, *, chat_id=12345): return SimpleNamespace( message_id=42, text=text, caption=None, entities=[], caption_entities=[], message_thread_id=None, is_topic_message=False, chat=SimpleNamespace(id=chat_id, type="private", title=None, is_forum=False), from_user=SimpleNamespace(id=111, full_name="Test User", first_name="Test"), reply_to_message=None, date=None, location=None, photo=None, video=None, audio=None, voice=None, document=None, sticker=None, media_group_id=None, ) def _make_update(text): return SimpleNamespace(update_id=1, message=_make_message(text), effective_message=None) @pytest.mark.asyncio async def test_short_command_dispatched_immediately(): adapter = _make_adapter() await adapter._handle_command(_make_update("/stop"), SimpleNamespace()) adapter.handle_message.assert_awaited_once() assert not adapter._pending_text_batches @pytest.mark.asyncio async def test_near_limit_command_is_batched_not_dispatched(): adapter = _make_adapter() long_cmd = "/queue " + "x" * 4090 # near Telegram's 4096 split point await adapter._handle_command(_make_update(long_cmd), SimpleNamespace()) # Must NOT dispatch immediately — a continuation chunk is almost certain. adapter.handle_message.assert_not_awaited() assert len(adapter._pending_text_batches) == 1 @pytest.mark.asyncio async def test_split_queue_command_merges_with_continuation(): adapter = _make_adapter() first = "/queue " + "x" * 4089 # 4096-char first chunk continuation = "y" * 500 await adapter._handle_command(_make_update(first), SimpleNamespace()) adapter.handle_message.assert_not_awaited() # Continuation arrives as a plain text update moments later. await adapter._handle_text_message(_make_update(continuation), SimpleNamespace()) # Wait past the split-delay flush window. await asyncio.sleep(0.3) adapter.handle_message.assert_awaited_once() dispatched = adapter.handle_message.call_args[0][0] assert dispatched.text.startswith("/queue ") assert "y" * 500 in dispatched.text # One merged event — the continuation never dispatched on its own. assert not adapter._pending_text_batches