mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Group A (3 tests): 'No LLM provider configured' RuntimeError - test_user_message_surrogates_sanitized, test_counters_initialized_in_init, test_openai_prompt_tokens_unchanged - Root cause: AIAgent.__init__ now requires base_url alongside api_key to skip resolve_provider_client() (which returns None when API keys are blanked in CI). Added base_url='http://localhost:1234/v1' to test agent construction. Group B (5 tests): Discord slash command auto-registration - test_auto_registers_missing_gateway_commands, test_auto_registered_command_*, test_register_skill_group_* - Root cause: xdist workers that loaded a discord mock WITHOUT app_commands.Command/Group caused _register_slash_commands() to fail silently. Added comprehensive shared discord mock in tests/gateway/conftest.py (same pattern as existing telegram mock). Group C (5 errors): Discord reply mode 'NoneType has no DMChannel' - All TestReplyToText tests - Root cause: FakeDMChannel was not a subclass of real discord.DMChannel, so isinstance() checks in _handle_message failed when running in full suite (real discord installed). Made FakeDMChannel inherit from discord.DMChannel when available. Removed fragile monkeypatch approach. Group D (2 tests): detect_provider_for_model wrong provider - test_openrouter_slug_match (got 'ai-gateway'), test_bare_name_gets_ openrouter_slug (got 'copilot') - Root cause: ai-gateway, copilot, and kilocode are multi-vendor aggregators that list other providers' models (OpenRouter-style slugs). They were being matched in Step 1 before OpenRouter. Added all three to _AGGREGATORS set so they're skipped like nous/openrouter. Group E (1 test): model_flow_custom StopIteration - test_model_flow_custom_saves_verified_v1_base_url - Root cause: 'Display name' prompt was added after the test was written. The input iterator had 5 answers but the flow now asks 6 questions. Added 6th empty string answer. Group F (1 test): Telegram proxy env assertion - test_uses_proxy_env_for_primary_and_fallback_transports - Root cause: _resolve_proxy_url() now checks TELEGRAM_PROXY first (via resolve_proxy_url('TELEGRAM_PROXY')). Test didn't clear this env var, allowing potential leakage from other tests in xdist workers. Added TELEGRAM_PROXY to the cleanup list.
147 lines
5.4 KiB
Python
147 lines
5.4 KiB
Python
"""Shared fixtures for gateway tests.
|
|
|
|
The ``_ensure_telegram_mock`` helper guarantees that a minimal mock of
|
|
the ``telegram`` package is registered in :data:`sys.modules` **before**
|
|
any test file triggers ``from gateway.platforms.telegram import ...``.
|
|
|
|
Without this, ``pytest-xdist`` workers that happen to collect
|
|
``test_telegram_caption_merge.py`` (bare top-level import, no per-file
|
|
mock) first will cache ``ChatType = None`` from the production
|
|
ImportError fallback, causing 30+ downstream test failures wherever
|
|
``ChatType.GROUP`` / ``ChatType.SUPERGROUP`` is accessed.
|
|
|
|
Individual test files may still call their own ``_ensure_telegram_mock``
|
|
— it short-circuits when the mock is already present.
|
|
"""
|
|
|
|
import sys
|
|
from unittest.mock import MagicMock
|
|
|
|
|
|
def _ensure_telegram_mock() -> None:
|
|
"""Install a comprehensive telegram mock in sys.modules.
|
|
|
|
Idempotent — skips when the real library is already imported.
|
|
Uses ``sys.modules[name] = mod`` (overwrite) instead of
|
|
``setdefault`` so it wins even if a partial/broken import
|
|
already cached a module with ``ChatType = None``.
|
|
"""
|
|
if "telegram" in sys.modules and hasattr(sys.modules["telegram"], "__file__"):
|
|
return # Real library is installed — nothing to mock
|
|
|
|
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"
|
|
|
|
# Real exception classes so ``except (NetworkError, ...)`` clauses
|
|
# in production code don't blow up with TypeError.
|
|
mod.error.NetworkError = type("NetworkError", (OSError,), {})
|
|
mod.error.TimedOut = type("TimedOut", (OSError,), {})
|
|
mod.error.BadRequest = type("BadRequest", (Exception,), {})
|
|
mod.error.Forbidden = type("Forbidden", (Exception,), {})
|
|
mod.error.InvalidToken = type("InvalidToken", (Exception,), {})
|
|
mod.error.RetryAfter = type("RetryAfter", (Exception,), {"retry_after": 1})
|
|
mod.error.Conflict = type("Conflict", (Exception,), {})
|
|
|
|
# Update.ALL_TYPES used in start_polling()
|
|
mod.Update.ALL_TYPES = []
|
|
|
|
for name in (
|
|
"telegram",
|
|
"telegram.ext",
|
|
"telegram.constants",
|
|
"telegram.request",
|
|
):
|
|
sys.modules[name] = mod
|
|
sys.modules["telegram.error"] = mod.error
|
|
|
|
|
|
def _ensure_discord_mock() -> None:
|
|
"""Install a comprehensive discord mock in sys.modules.
|
|
|
|
Idempotent — skips when the real library is already imported.
|
|
Uses ``sys.modules[name] = mod`` (overwrite) instead of
|
|
``setdefault`` so it wins even if a partial/broken import already
|
|
cached the module.
|
|
|
|
This mock is comprehensive — it includes **all** attributes needed by
|
|
every gateway discord test file. Individual test files should call
|
|
this function (it short-circuits when already present) rather than
|
|
maintaining their own mock setup.
|
|
"""
|
|
if "discord" in sys.modules and hasattr(sys.modules["discord"], "__file__"):
|
|
return # Real library is installed — nothing to mock
|
|
|
|
from types import SimpleNamespace
|
|
|
|
discord_mod = MagicMock()
|
|
discord_mod.Intents.default.return_value = MagicMock()
|
|
discord_mod.Client = MagicMock
|
|
discord_mod.File = MagicMock
|
|
discord_mod.DMChannel = type("DMChannel", (), {})
|
|
discord_mod.Thread = type("Thread", (), {})
|
|
discord_mod.ForumChannel = type("ForumChannel", (), {})
|
|
discord_mod.Interaction = object
|
|
discord_mod.Embed = MagicMock
|
|
discord_mod.ui = SimpleNamespace(
|
|
View=object,
|
|
button=lambda *a, **k: (lambda fn: fn),
|
|
Button=object,
|
|
)
|
|
discord_mod.ButtonStyle = SimpleNamespace(
|
|
success=1, primary=2, secondary=2, danger=3,
|
|
green=1, grey=2, blurple=2, red=3,
|
|
)
|
|
discord_mod.Color = SimpleNamespace(
|
|
orange=lambda: 1, green=lambda: 2, blue=lambda: 3,
|
|
red=lambda: 4, purple=lambda: 5,
|
|
)
|
|
|
|
# app_commands — needed by _register_slash_commands auto-registration
|
|
class _FakeGroup:
|
|
def __init__(self, *, name, description, parent=None):
|
|
self.name = name
|
|
self.description = description
|
|
self.parent = parent
|
|
self._children: dict = {}
|
|
if parent is not None:
|
|
parent.add_command(self)
|
|
|
|
def add_command(self, cmd):
|
|
self._children[cmd.name] = cmd
|
|
|
|
class _FakeCommand:
|
|
def __init__(self, *, name, description, callback, parent=None):
|
|
self.name = name
|
|
self.description = description
|
|
self.callback = callback
|
|
self.parent = parent
|
|
|
|
discord_mod.app_commands = SimpleNamespace(
|
|
describe=lambda **kwargs: (lambda fn: fn),
|
|
choices=lambda **kwargs: (lambda fn: fn),
|
|
Choice=lambda **kwargs: SimpleNamespace(**kwargs),
|
|
Group=_FakeGroup,
|
|
Command=_FakeCommand,
|
|
)
|
|
|
|
ext_mod = MagicMock()
|
|
commands_mod = MagicMock()
|
|
commands_mod.Bot = MagicMock
|
|
ext_mod.commands = commands_mod
|
|
|
|
for name in ("discord", "discord.ext", "discord.ext.commands"):
|
|
sys.modules[name] = discord_mod
|
|
sys.modules["discord.ext"] = ext_mod
|
|
sys.modules["discord.ext.commands"] = commands_mod
|
|
|
|
|
|
# Run at collection time — before any test file's module-level imports.
|
|
_ensure_telegram_mock()
|
|
_ensure_discord_mock()
|