From eb3cf9750eb8395c158be5cb929604041d1b03b5 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 1 Jun 2026 00:17:50 +0530 Subject: [PATCH] fix(gateway): resolve _get_dm_topic_info on adapter class, not instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the synthetic-notification DM-topic routing fix. The new _is_telegram_dm_topic_target probed the adapter's _get_dm_topic_info via instance-level getattr, which a MagicMock auto-creates as a truthy callable — so any test double with a non-dm chat_type and a thread_id would be misclassified as a DM topic lane and have the fallback routing keys injected. Resolve the method on type(adapter) and treat only dict-shaped returns as an operator-declared topic, mirroring the existing guard in _rename_telegram_topic_for_session_title. Update the home-channel startup test to declare _get_dm_topic_info on a real adapter subclass instead of patching a MagicMock onto the instance. --- gateway/run.py | 22 ++++++++++++++++------ tests/gateway/test_restart_notification.py | 11 ++++++++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index aafb7ac63..dbe3742ca 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -14261,12 +14261,22 @@ class GatewayRunner: return False if chat_type == "dm": return True - get_dm_topic_info = getattr(adapter, "_get_dm_topic_info", None) - if callable(get_dm_topic_info) and chat_id: - try: - return bool(get_dm_topic_info(str(chat_id), str(thread_id))) - except Exception: - logger.debug("Failed to inspect Telegram DM topic metadata", exc_info=True) + # Inspect operator-declared DM topics via the adapter's lookup. Resolve + # the method on the CLASS, not the instance: getattr() on a MagicMock + # auto-creates a callable child for any attribute, so an instance-level + # lookup would report a DM topic for every test double. Only a + # dict-shaped return counts as an operator-declared topic — a bare + # MagicMock or other sentinel must not. Mirrors the guard in + # _rename_telegram_topic_for_session_title. + if adapter is not None and chat_id: + get_dm_topic_info = getattr(type(adapter), "_get_dm_topic_info", None) + if callable(get_dm_topic_info): + try: + topic_info = get_dm_topic_info(adapter, str(chat_id), str(thread_id)) + except Exception: + logger.debug("Failed to inspect Telegram DM topic metadata", exc_info=True) + else: + return isinstance(topic_info, dict) return False @staticmethod diff --git a/tests/gateway/test_restart_notification.py b/tests/gateway/test_restart_notification.py index 6abfaac35..56be23370 100644 --- a/tests/gateway/test_restart_notification.py +++ b/tests/gateway/test_restart_notification.py @@ -261,7 +261,16 @@ async def test_send_home_channel_startup_notification_preserves_thread_metadata( name="Ops Topic", thread_id="777", ) - adapter._get_dm_topic_info = MagicMock(return_value={"name": "Ops Topic"}) + # Declare the DM-topic lookup on the adapter CLASS, not the instance. + # _is_telegram_dm_topic_target resolves _get_dm_topic_info via type(adapter) + # so a MagicMock auto-attribute (instance-level) is intentionally ignored; + # a real adapter exposes the method on its class. Mirrors the fake-adapter + # pattern in test_telegram_topic_mode.py. + class _DmTopicAdapter(type(adapter)): + def _get_dm_topic_info(self, chat_id, thread_id): + return {"name": "Ops Topic"} + + adapter.__class__ = _DmTopicAdapter adapter.send = AsyncMock(return_value=SendResult(success=True, message_id="home")) delivered = await runner._send_home_channel_startup_notifications()