From 6cf4bdd165a8f690b869760eb47d3e2dafa5fda2 Mon Sep 17 00:00:00 2001 From: mehmetkr-31 Date: Wed, 29 Jul 2026 20:20:53 -0700 Subject: [PATCH] test(gateway): fix order-dependent telegram-mock flake cluster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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()==, 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. --- scripts/release.py | 1 + tests/gateway/conftest.py | 62 +++++++++++++++++++++++++++++---- tests/gateway/test_dm_topics.py | 38 +++++++++----------- 3 files changed, 72 insertions(+), 29 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 0a6c65c08f0..56a7f6cd069 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -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", diff --git a/tests/gateway/conftest.py b/tests/gateway/conftest.py index 9cbc99fdd53..7a21465dd3d 100644 --- a/tests/gateway/conftest.py +++ b/tests/gateway/conftest.py @@ -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* + (````). 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. diff --git a/tests/gateway/test_dm_topics.py b/tests/gateway/test_dm_topics.py index 08076862533..f889f95c66b 100644 --- a/tests/gateway/test_dm_topics.py +++ b/tests/gateway/test_dm_topics.py @@ -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