fix(gateway): resolve _get_dm_topic_info on adapter class, not instance

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.
This commit is contained in:
kshitijk4poor
2026-06-01 00:17:50 +05:30
committed by kshitij
parent 4259bab7d4
commit eb3cf9750e
2 changed files with 26 additions and 7 deletions

View File

@ -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

View File

@ -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()