diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index ec50822673..0d0ac3866f 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -369,10 +369,14 @@ class TelegramAdapter(BasePlatformAdapter): @classmethod def _message_thread_id_for_typing(cls, thread_id: Optional[str]) -> Optional[int]: - # Mirrors _message_thread_id_for_send: the General forum topic (thread id - # "1") is represented as "no thread id" on the wire. User-created topics - # keep their real id so typing stays scoped to that topic. - if not thread_id or str(thread_id) == cls._GENERAL_TOPIC_THREAD_ID: + # Asymmetric with _message_thread_id_for_send on purpose. Telegram's + # sendMessage and sendChatAction treat thread id "1" (the forum General + # topic) differently: sends reject message_thread_id=1 and must omit it, + # but sendChatAction needs message_thread_id=1 to place the typing + # bubble in the General topic (omitting it hides the bubble entirely + # from the client's view of that topic). Preserve the real id here — + # sends still map "1" → None via _message_thread_id_for_send. + if not thread_id: return None return int(thread_id) diff --git a/tests/gateway/test_telegram_thread_fallback.py b/tests/gateway/test_telegram_thread_fallback.py index b8330822b3..7b982e9588 100644 --- a/tests/gateway/test_telegram_thread_fallback.py +++ b/tests/gateway/test_telegram_thread_fallback.py @@ -159,12 +159,17 @@ async def test_send_omits_general_topic_thread_id(): @pytest.mark.asyncio -async def test_send_typing_general_topic_uses_none_thread_id(): - """Typing for forum General should hit the API with message_thread_id=None directly. +async def test_send_typing_preserves_general_topic_thread_id(): + """Typing for forum General must send message_thread_id=1, not None. - _message_thread_id_for_typing() maps the General topic (thread id "1") to None - the same way _message_thread_id_for_send() does, so there's no retry path — the - first call is already correct. + Asymmetric with _message_thread_id_for_send: sendMessage rejects + message_thread_id=1, but sendChatAction needs it to scope the typing + bubble to the General topic. Omitting it (message_thread_id=None) hides + the bubble from the General-topic view entirely. + + Regression guard for the d5357f816 refactor that mapped "1" → None in + the typing resolver and silently killed typing indicators in every + forum-group General topic. """ adapter = _make_adapter() call_log = [] @@ -177,7 +182,7 @@ async def test_send_typing_general_topic_uses_none_thread_id(): await adapter.send_typing("-100123", metadata={"thread_id": "1"}) assert call_log == [ - {"chat_id": -100123, "action": "typing", "message_thread_id": None}, + {"chat_id": -100123, "action": "typing", "message_thread_id": 1}, ]