mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
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:
parent
080bb83746
commit
6cf4bdd165
3 changed files with 72 additions and 29 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue