From 82352e54c46acc9f332fd5e111440e3706269022 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sun, 10 May 2026 21:59:14 -0700 Subject: [PATCH] test(telegram): regression coverage for edit overflow split-and-deliver Two new tests: - tests/gateway/test_telegram_format.py test_message_too_long_splits_into_continuations_not_silent_truncation: asserts edit_message returns success=True with continuation_message_ids populated and message_id pointing at the last continuation when content exceeds MAX_MESSAGE_LENGTH (#19537). Replaces the original fail-on-overflow assertion with the split-and-deliver contract. - tests/gateway/test_stream_consumer.py TestEditOverflowSplitAndDeliver.test_consumer_advances_message_id_on_split_and_deliver: asserts the consumer side updates _message_id to the latest continuation, clears _last_sent_text, and fires on_new_message when the adapter reports a split-and-deliver result. --- tests/gateway/test_stream_consumer.py | 50 +++++++++++++++++++++++++++ tests/gateway/test_telegram_format.py | 37 ++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/tests/gateway/test_stream_consumer.py b/tests/gateway/test_stream_consumer.py index 12671d806d8..41d8f40e84d 100644 --- a/tests/gateway/test_stream_consumer.py +++ b/tests/gateway/test_stream_consumer.py @@ -939,6 +939,56 @@ class TestFinalResponseDeliveryGuard: assert consumer._final_response_sent is True +class TestEditOverflowSplitAndDeliver: + """When edit_message split-and-delivers an oversized payload across the + original message + N continuations (Telegram >4096 UTF-16), the consumer + must update _message_id to the latest continuation, reset _last_sent_text, + and fire on_new_message so subsequent tool-progress bubbles linearize + below the new visible message.""" + + @pytest.mark.asyncio + async def test_consumer_advances_message_id_on_split_and_deliver(self): + adapter = MagicMock() + # Simulate edit_message split-and-deliver: success=True with the + # final continuation's id and a populated continuation_message_ids + # tuple (the new SendResult contract). + adapter.edit_message = AsyncMock(return_value=SimpleNamespace( + success=True, + message_id="msg_continuation_2", + continuation_message_ids=("msg_continuation_1", "msg_continuation_2"), + )) + adapter.send = AsyncMock( + return_value=SimpleNamespace(success=True, message_id="msg_initial"), + ) + adapter.MAX_MESSAGE_LENGTH = 4096 + + config = StreamConsumerConfig( + edit_interval=0.01, buffer_threshold=5, cursor="", + ) + consumer = GatewayStreamConsumer(adapter, "chat_999", config) + + # Track on_new_message firings. + new_msg_count = [0] + consumer._on_new_message = lambda: new_msg_count.__setitem__(0, new_msg_count[0] + 1) + + # Seed the consumer as if a first send succeeded already. + consumer._message_id = "msg_initial" + consumer._last_sent_text = "old" + consumer._already_sent = True + + # Drive an edit that the adapter "split and delivers". + ok = await consumer._send_or_edit("new full text after overflow") + + assert ok is True + # Consumer advanced to the latest continuation id. + assert consumer._message_id == "msg_continuation_2" + # Skip-if-same cache reset so the next edit doesn't false-positive. + assert consumer._last_sent_text == "" + # on_new_message fired so the tool-progress bubble breaks below + # the new continuation (per the openclaw #32535 lesson). + assert new_msg_count[0] == 1 + + class TestInterimCommentaryMessages: @pytest.mark.asyncio async def test_commentary_message_stays_separate_from_final_stream(self): diff --git a/tests/gateway/test_telegram_format.py b/tests/gateway/test_telegram_format.py index 1cd09f2e7db..55fb118d8f7 100644 --- a/tests/gateway/test_telegram_format.py +++ b/tests/gateway/test_telegram_format.py @@ -759,6 +759,43 @@ class TestEditMessageStreamingSafety: "text": "final **bold**", } + @pytest.mark.asyncio + async def test_message_too_long_splits_into_continuations_not_silent_truncation(self): + """When edit_message_text exceeds Telegram's 4096 UTF-16 limit, the + adapter must split the content across the existing message + new + continuation messages so the user gets the full reply. Previously + the adapter best-effort truncated the content with '…' and returned + success=True, dropping everything past the truncation boundary + (#19537).""" + adapter = TelegramAdapter(PlatformConfig(enabled=True, token="fake-token")) + adapter._bot = MagicMock() + adapter._bot.edit_message_text = AsyncMock() + # Continuation sends return monotonically increasing message ids. + _next_id = [1000] + async def _fake_send(**kwargs): + _next_id[0] += 1 + return SimpleNamespace(message_id=_next_id[0]) + adapter._bot.send_message = AsyncMock(side_effect=_fake_send) + + # 6000-char content well over the 4096 UTF-16 limit. + oversized = "x" * 6000 + result = await adapter.edit_message("123", "456", oversized, finalize=False) + + # Adapter reports success with continuations populated. + assert result.success is True + assert result.error is None + assert len(result.continuation_message_ids) >= 1, ( + "expected at least one continuation message" + ) + # The reported message_id is the LAST visible message (the final + # continuation), so subsequent edits target the most recent. + assert result.message_id == result.continuation_message_ids[-1] + # Original message_id (456) was edited with chunk 1. + first_edit = adapter._bot.edit_message_text.call_args + assert first_edit.kwargs["message_id"] == 456 + # Continuations were sent threaded as replies for visual grouping. + assert adapter._bot.send_message.await_count == len(result.continuation_message_ids) + # ========================================================================= # Telegram guest mention gating # =========================================================================