test(gateway): fix order-dependent telegram-mock flake cluster

The file-local telegram mock in test_dm_topics.py installed unconditionally
(no __file__ guard), registered a separate string-valued telegram.constants
module, and force-popped the adapter — poisoning the session for any later
telegram test in the same process (assert 'MARKDOWN_V2' in "'MarkdownV2'").

Fix at the source:
- conftest: _FakeEnumMember(str) with PTB-faithful str()==value and
  repr()==<ChatType.X: 'x'>, satisfying both repr assertions and the
  adapter's str(chat.type) normalization; the same object is bound to
  mod.ParseMode and mod.constants.ParseMode.
- test_dm_topics.py: delete the divergent local mock installer; import the
  shared conftest one.
- release.py: mailmap entry for the author.

Verified: the 5-failure cluster repro (dm_topics + slash_confirm +
approval_buttons + model_picker + network_reconnect + telegram_format in one
process) goes 83/83 green (3x); full tests/gateway single-process run drops
10 -> 5 failed, the remainder being pre-existing discord order-dep failures
out of scope here.

Salvaged from #68873. Credit to @liuhao1024 for the earliest root-cause
diagnosis of this str-enum mock class in PR #33875, two months earlier.

Fixes the telegram-mock order-dependent flake cluster.
This commit is contained in:
mehmetkr-31 2026-07-29 20:20:53 -07:00 committed by Teknium
parent 080bb83746
commit 6cf4bdd165
3 changed files with 72 additions and 29 deletions

View file

@ -1025,6 +1025,7 @@ LEGACY_AUTHOR_MAP = {
"137614867+cutepawss@users.noreply.github.com": "cutepawss",
"96793918+memosr@users.noreply.github.com": "memosr",
"mehmet.sr35@gmail.com": "memosr",
"mehmet.kar@std.yildiz.edu.tr": "mehmetkr-31",
"milkoor@users.noreply.github.com": "milkoor",
"xuerui911@gmail.com": "Fatty911",
"131039422+SHL0MS@users.noreply.github.com": "SHL0MS",

View file

@ -48,6 +48,41 @@ def make_async_session_db(sync_mock=None):
return AsyncSessionDB(sync_mock), sync_mock
class _FakeEnumMember(str):
"""A python-telegram-bot-faithful stand-in for a ``StrEnum`` member.
PTB constants (``ParseMode``, ``ChatType``) are ``StrEnum`` members:
``str(x)`` and equality give the *value* (``"supergroup"``) while
``repr(x)`` shows the qualified *member name*
(``<ChatType.SUPERGROUP>``). Test stubs that pick only one of those
shapes break the other consumer: plain strings fail assertions like
``"MARKDOWN_V2" in repr(parse_mode)``, while auto-generated MagicMock
attributes fail the adapter's ``str(chat.type)`` normalization
(``adapter.py`` ``_build_message_event``). This class satisfies both,
so every telegram test sees the same semantics regardless of which
file's mock installed first.
"""
_qualname: str
def __new__(cls, enum_name: str, member_name: str, value: str):
obj = str.__new__(cls, value)
obj._qualname = f"{enum_name}.{member_name}"
return obj
def __repr__(self) -> str: # pragma: no cover - trivial
return f"<{self._qualname}: {str.__repr__(self)}>"
def _fake_str_enum(enum_name: str, **members: str):
"""Build a ``SimpleNamespace``-like enum of :class:`_FakeEnumMember`."""
from types import SimpleNamespace
return SimpleNamespace(
**{name: _FakeEnumMember(enum_name, name, value) for name, value in members.items()}
)
def _ensure_telegram_mock() -> None:
"""Install a comprehensive telegram mock in sys.modules.
@ -61,13 +96,26 @@ def _ensure_telegram_mock() -> None:
mod = MagicMock()
mod.ext.ContextTypes.DEFAULT_TYPE = type(None)
mod.constants.ParseMode.MARKDOWN = "Markdown"
mod.constants.ParseMode.MARKDOWN_V2 = "MarkdownV2"
mod.constants.ParseMode.HTML = "HTML"
mod.constants.ChatType.PRIVATE = "private"
mod.constants.ChatType.GROUP = "group"
mod.constants.ChatType.SUPERGROUP = "supergroup"
mod.constants.ChatType.CHANNEL = "channel"
# One shared PTB-faithful enum namespace per constant, attached to BOTH
# access paths: ``sys.modules["telegram.constants"]`` is registered as
# the root mock below, so ``from telegram.constants import ParseMode``
# resolves ``mod.ParseMode`` — while config/docs-style access reads
# ``telegram.constants.ParseMode``. Binding the same object to both
# keeps every consumer comparing against identical members.
_parse_mode = _fake_str_enum(
"ParseMode", MARKDOWN="Markdown", MARKDOWN_V2="MarkdownV2", HTML="HTML"
)
_chat_type = _fake_str_enum(
"ChatType",
PRIVATE="private",
GROUP="group",
SUPERGROUP="supergroup",
CHANNEL="channel",
)
mod.ParseMode = _parse_mode
mod.constants.ParseMode = _parse_mode
mod.ChatType = _chat_type
mod.constants.ChatType = _chat_type
# Mirror PTB's exception hierarchy: BadRequest is a semantic API error,
# but inherits from NetworkError in python-telegram-bot 22.x.

View file

@ -20,30 +20,24 @@ import pytest
from gateway.config import PlatformConfig
def _ensure_telegram_mock():
telegram_mod = MagicMock()
telegram_mod.ext.ContextTypes.DEFAULT_TYPE = type(None)
# Register telegram.constants as a separate module mock so that
# ``from telegram.constants import ChatType`` resolves to our mock
# with string-valued members (not auto-generated MagicMocks).
constants_mod = MagicMock()
constants_mod.ParseMode.MARKDOWN_V2 = "MarkdownV2"
constants_mod.ChatType.GROUP = "group"
constants_mod.ChatType.SUPERGROUP = "supergroup"
constants_mod.ChatType.CHANNEL = "channel"
constants_mod.ChatType.PRIVATE = "private"
sys.modules["telegram"] = telegram_mod
sys.modules["telegram.ext"] = telegram_mod.ext
sys.modules["telegram.constants"] = constants_mod
sys.modules["telegram.request"] = telegram_mod.request
# Force reimport so the adapter picks up the mock ChatType.
sys.modules.pop("plugins.platforms.telegram.adapter", None)
# Use the shared, comprehensive telegram mock from conftest instead of a
# file-local one. The previous local installer differed from every other
# telegram test's stub in two ways — it registered a SEPARATE string-valued
# ``telegram.constants`` module (others register the root mock, so ParseMode
# members stay auto-generated MagicMock attributes) and its ``telegram.error``
# was a bare MagicMock (conftest defines real exception subclasses with PTB's
# hierarchy) — and it installed UNCONDITIONALLY (no real-library guard).
# Because it also force-reimported the adapter, the divergent stub leaked into
# sys.modules for the rest of the session: every later telegram test that
# asserts ParseMode repr or isinstance against telegram.error classes failed
# order-dependently in full runs while passing in isolation.
from tests.gateway.conftest import _ensure_telegram_mock # noqa: E402
_ensure_telegram_mock()
# Force reimport so the adapter binds to whatever sys.modules now holds
# (the shared mock, or the real library when it is installed) rather than a
# stub an earlier test file may have bound it to.
sys.modules.pop("plugins.platforms.telegram.adapter", None)
from plugins.platforms.telegram.adapter import TelegramAdapter # noqa: E402