From e133ac49d8bedb192d0513c79c924fb39f5948f1 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Mon, 25 May 2026 07:23:17 +0000 Subject: [PATCH 1/7] test: reset gateway.platforms.discord state before every test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``test_discord_imports.py`` deletes and re-imports gateway.platforms.discord with ``DISCORD_AVAILABLE=False``, corrupting state for every subsequent discord test in the same xdist worker—including e2e tests that import DiscordAdapter. Add a reset block to the root ``_reset_module_state`` autouse fixture (which already clears ~10 other module-level caches) so the flag and module reference are restored before every test. Co-Authored-By: Claude Opus 4.7 --- tests/conftest.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index f9ad9d9b..65f40024 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -440,6 +440,19 @@ def _reset_module_state(): except Exception: pass + # --- gateway.platforms.discord — module reload anti-pattern guard --- + # ``test_discord_imports.py`` deletes and re-imports this module with + # ``DISCORD_AVAILABLE=False``, corrupting state for every subsequent + # discord test in the same xdist worker (including e2e tests). Reset + # the flag and module reference before every test. + try: + import gateway.platforms.discord as _dp_mod + _dp_mod.DISCORD_AVAILABLE = True + if getattr(_dp_mod, "discord", None) is None and "discord" in sys.modules: + _dp_mod.discord = sys.modules["discord"] + except Exception: + pass + yield -- 2.52.0 From e59edd7162dc8f8c9187c672672589afe30c2127 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Mon, 25 May 2026 07:46:39 +0000 Subject: [PATCH 2/7] test: add ChatType attrs to e2e telegram mock The e2e conftest installed a minimal telegram mock without ChatType.GROUP / SUPERGROUP / CHANNEL / PRIVATE attributes. When e2e tests collected before dm_topics tests in the same xdist worker, the cached mock lacked these attrs, so ``chat.type in (ChatType.GROUP, ChatType.SUPERGROUP)`` evaluated to False and group topic skill bindings were silently dropped. Add the four ChatType constants to the e2e mock so it matches the comprehensive mock in tests/gateway/conftest.py. Co-Authored-By: Claude Opus 4.7 --- tests/e2e/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 76b14e31..3f4824f1 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -37,6 +37,10 @@ def _ensure_telegram_mock(): telegram_mod.Update.ALL_TYPES = [] telegram_mod.Bot = MagicMock telegram_mod.constants.ParseMode.MARKDOWN_V2 = "MarkdownV2" + telegram_mod.constants.ChatType.PRIVATE = "private" + telegram_mod.constants.ChatType.GROUP = "group" + telegram_mod.constants.ChatType.SUPERGROUP = "supergroup" + telegram_mod.constants.ChatType.CHANNEL = "channel" telegram_mod.ext.Application = MagicMock() telegram_mod.ext.Application.builder = MagicMock telegram_mod.ext.ContextTypes.DEFAULT_TYPE = type(None) -- 2.52.0 From f26b181f7002e7a5b8ff7aa0ad891b6d30b9e130 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Mon, 25 May 2026 09:44:40 +0000 Subject: [PATCH 3/7] test: reload gateway.platforms.telegram after installing comprehensive mock When tests/e2e/conftest.py imports gateway.platforms.telegram while a minimal mock is in sys.modules, the cached production module holds stale references to the old mock's ChatType MagicMock. Later, when tests/gateway/conftest.py overwrites sys.modules['telegram'] with the comprehensive mock, the cached module still points to the OLD MagicMock object. This causes ``chat.type in (ChatType.GROUP, ChatType.SUPERGROUP)`` to evaluate False because the test's _ChatType is a DIFFERENT MagicMock instance. Reload gateway.platforms.telegram after installing the comprehensive mock so any previously-imported production module picks up the correct mock. Co-Authored-By: Claude Opus 4.7 --- tests/gateway/conftest.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/gateway/conftest.py b/tests/gateway/conftest.py index b159c1b2..d4bc8be1 100644 --- a/tests/gateway/conftest.py +++ b/tests/gateway/conftest.py @@ -82,6 +82,13 @@ def _ensure_telegram_mock() -> None: sys.modules[name] = mod sys.modules["telegram.error"] = mod.error + # If another test file (e.g. e2e/conftest) imported gateway.platforms.telegram + # while a different mock was in sys.modules, the cached production module still + # holds stale references. Reload so it picks up the comprehensive mock above. + if "gateway.platforms.telegram" in sys.modules: + import importlib + importlib.reload(sys.modules["gateway.platforms.telegram"]) + def _ensure_openai_mock() -> None: """Install a minimal openai mock in sys.modules. -- 2.52.0 From 7fc1a8b399c70e932a41dd80f447f2b3b22b3a62 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Mon, 25 May 2026 10:19:54 +0000 Subject: [PATCH 4/7] test(e2e): synchronise gateway.platforms.discord.discord with e2e mock Other test files replace sys.modules['discord'] or reload gateway.platforms.discord, causing the cached DiscordAdapter class (imported at module level in e2e/conftest) to reference a stale discord module. If discord.MessageType or discord.DMChannel become different MagicMock objects, isinstance checks in DiscordAdapter._handle_message silently drop messages. Add an autouse fixture that forces gateway.platforms.discord.discord back to the same mock object the e2e conftest uses before every e2e test, so message-type comparisons match. Co-Authored-By: Claude Opus 4.7 --- tests/e2e/conftest.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 3f4824f1..15a8319f 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -127,6 +127,29 @@ _slack_mod.SLACK_AVAILABLE = True from gateway.platforms.slack import SlackAdapter # noqa: E402 +@pytest.fixture(autouse=True) +def _ensure_e2e_discord_state(): + """Synchronise gateway.platforms.discord.discord with the e2e mock. + + Other test files may replace ``sys.modules['discord']`` or reload + ``gateway.platforms.discord``, causing the cached ``DiscordAdapter`` + class (imported at module level above) to reference a stale ``discord`` + module. If ``discord.MessageType`` or ``discord.DMChannel`` become + different MagicMock objects, ``isinstance`` checks in + ``DiscordAdapter._handle_message`` silently drop messages. + + This fixture forces ``gateway.platforms.discord.discord`` back to the + same mock object the e2e conftest uses, so message-type comparisons + match. + """ + import gateway.platforms.discord as _dp_mod + + _dp_mod.DISCORD_AVAILABLE = True + if "discord" in sys.modules: + _dp_mod.discord = sys.modules["discord"] + yield + + # Platform-generic factories def make_source(platform: Platform, chat_id: str = "e2e-chat-1", user_id: str = "e2e-user-1", chat_type: str = "dm") -> SessionSource: -- 2.52.0 From d5d2234786e2890bdfee1bd3600ee75d3b9a2c79 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Mon, 25 May 2026 11:01:05 +0000 Subject: [PATCH 5/7] test(e2e): patch DiscordAdapter module globals to match e2e mock The _ensure_e2e_discord_state fixture synced gateway.platforms.discord.discord with sys.modules['discord'], but the cached DiscordAdapter class imported at module level may belong to a *different* module object (e.g. after test_discord_imports.py deletes and re-imports the module). In that case _handle_message's discord.DMChannel diverges from make_fake_dm_channel's, causing isinstance checks to silently fail in the full xdist suite. Patch DiscordAdapter.__init__.__globals__['discord'] to the same mock the test helpers use, ensuring runtime lookups always match. Co-Authored-By: Claude Opus 4.7 --- tests/e2e/conftest.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 15a8319f..bdd9c2ac 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -139,14 +139,22 @@ def _ensure_e2e_discord_state(): ``DiscordAdapter._handle_message`` silently drop messages. This fixture forces ``gateway.platforms.discord.discord`` back to the - same mock object the e2e conftest uses, so message-type comparisons - match. + same mock object the e2e conftest uses, and also patches the + ``discord`` global in the module where ``DiscordAdapter`` was defined + so that runtime ``isinstance`` checks match the helpers. """ import gateway.platforms.discord as _dp_mod _dp_mod.DISCORD_AVAILABLE = True if "discord" in sys.modules: _dp_mod.discord = sys.modules["discord"] + + # The DiscordAdapter class imported at module level may belong to a + # *different* module object than the one currently in sys.modules (e.g. + # after test_discord_imports.py deletes and re-imports the module). + # Patching the class's module globals ensures _handle_message sees the + # same discord.DMChannel / discord.MessageType as make_fake_dm_channel. + DiscordAdapter.__init__.__globals__["discord"] = discord yield -- 2.52.0 From 607824f5c9ff456a256ed81ee6f171a014f171f1 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Mon, 25 May 2026 11:21:04 +0000 Subject: [PATCH 6/7] test(e2e): use DiscordAdapter's discord module in helpers instead of global patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix (patching DiscordAdapter.__init__.__globals__) caused 50 test failures because mutating the module dict leaked across tests on the same xdist worker — gateway tests that ran after e2e tests saw the e2e mock instead of the comprehensive mock. Replace the global patch with a helper `_discord_for_helpers()` that reads `discord` from DiscordAdapter's own globals. make_fake_dm_channel, make_fake_thread, and make_discord_message now use that helper, ensuring `isinstance` checks always match without side effects. Co-Authored-By: Claude Opus 4.7 --- tests/e2e/conftest.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index bdd9c2ac..ce87e643 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -139,22 +139,14 @@ def _ensure_e2e_discord_state(): ``DiscordAdapter._handle_message`` silently drop messages. This fixture forces ``gateway.platforms.discord.discord`` back to the - same mock object the e2e conftest uses, and also patches the - ``discord`` global in the module where ``DiscordAdapter`` was defined - so that runtime ``isinstance`` checks match the helpers. + same mock object the e2e conftest uses, so message-type comparisons + match. """ import gateway.platforms.discord as _dp_mod _dp_mod.DISCORD_AVAILABLE = True if "discord" in sys.modules: _dp_mod.discord = sys.modules["discord"] - - # The DiscordAdapter class imported at module level may belong to a - # *different* module object than the one currently in sys.modules (e.g. - # after test_discord_imports.py deletes and re-imports the module). - # Patching the class's module globals ensures _handle_message sees the - # same discord.DMChannel / discord.MessageType as make_fake_dm_channel. - DiscordAdapter.__init__.__globals__["discord"] = discord yield @@ -364,12 +356,28 @@ def make_fake_text_channel(channel_id: int = CHANNEL_ID, name: str = "general", ) +def _discord_for_helpers(): + """Return the ``discord`` module that DiscordAdapter uses at runtime. + + The module-level ``discord`` import in this conftest may diverge from + the one cached in DiscordAdapter's globals after other tests reload or + replace ``gateway.platforms.discord``. Using the same object for + ``__class__`` assignment ensures ``isinstance`` checks in + ``_handle_message`` match. + """ + _d = DiscordAdapter.__init__.__globals__.get("discord") + if _d is not None: + return _d + # Fallback to the module-level import (should never be needed). + return discord + + def make_fake_dm_channel(channel_id: int = 55555): ch = MagicMock(spec=[]) ch.id = channel_id ch.name = "DM" ch.topic = None - ch.__class__ = discord.DMChannel + ch.__class__ = _discord_for_helpers().DMChannel return ch @@ -382,7 +390,7 @@ def make_fake_thread(thread_id: int = THREAD_ID, name: str = "test-thread", pare th.guild = th.parent.guild th.topic = None th.type = 11 - th.__class__ = discord.Thread + th.__class__ = _discord_for_helpers().Thread return th @@ -407,7 +415,7 @@ def make_discord_message( id=message_id, content=content, author=author, channel=channel, guild=getattr(channel, "guild", None), mentions=mentions, attachments=attachments, - type=getattr(discord, "MessageType", SimpleNamespace()).default, + type=getattr(_discord_for_helpers(), "MessageType", SimpleNamespace()).default, reference=None, created_at=datetime.now(timezone.utc), create_thread=AsyncMock(), ) -- 2.52.0 From cfe1abf376bcfd1cf4ba2ebccec2143920085d8e Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Mon, 25 May 2026 13:33:56 +0000 Subject: [PATCH 7/7] test(integration): restore original terminal registry entry after modal test import `tests/integration/test_modal_terminal.py` executes `tools/terminal_tool.py` in a new module via `importlib.util.module_from_spec`. The module-level `registry.register()` in `terminal_tool.py` updates the global registry with the new module's `check_terminal_requirements`, breaking subsequent tests that monkeypatch `tools.terminal_tool._get_env_config` (the registry uses the new module's globals, not the original). Save and restore the original registry entry after `exec_module` to prevent this pollution. Co-Authored-By: Claude Opus 4.7 --- tests/integration/test_modal_terminal.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/integration/test_modal_terminal.py b/tests/integration/test_modal_terminal.py index a4fc2699..170323f7 100644 --- a/tests/integration/test_modal_terminal.py +++ b/tests/integration/test_modal_terminal.py @@ -44,11 +44,24 @@ sys.path.insert(0, str(parent_dir)) # Import terminal_tool module directly using importlib to avoid tools/__init__.py import importlib.util +from tools.registry import registry + terminal_tool_path = parent_dir / "tools" / "terminal_tool.py" spec = importlib.util.spec_from_file_location("terminal_tool", terminal_tool_path) terminal_module = importlib.util.module_from_spec(spec) + +# Save the original registry entry before exec_module overwrites it with the +# re-executed module's check_terminal_requirements. +_original_terminal_entry = registry.get_entry("terminal") + spec.loader.exec_module(terminal_module) +# Restore the original registry entry so that subsequent tests which +# monkeypatch tools.terminal_tool._get_env_config still affect the +# check_fn that the registry actually uses. +if _original_terminal_entry is not None: + registry._tools["terminal"] = _original_terminal_entry + terminal_tool = terminal_module.terminal_tool check_terminal_requirements = terminal_module.check_terminal_requirements _get_env_config = terminal_module._get_env_config -- 2.52.0