test: reset gateway.platforms.discord state before every test #30
@@ -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
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user