test: reset gateway.platforms.discord state before every test #30

Merged
agent-dev-a merged 7 commits from fix/e2e-discord-xdist-isolation into main 2026-05-25 14:52:04 +00:00
4 changed files with 79 additions and 3 deletions
+13
View File
@@ -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
+46 -3
View File
@@ -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(),
)
+7
View File
@@ -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.
+13
View File
@@ -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