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 diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 76b14e31..ce87e643 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) @@ -123,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: @@ -329,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 @@ -347,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 @@ -372,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(), ) 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. 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