fix(tests): resolve 12 CI failures + 10 errors across 6 root causes (#11040)
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.
This commit is contained in:
@@ -1044,7 +1044,7 @@ def detect_provider_for_model(
|
||||
return (resolved_provider, default_models[0])
|
||||
|
||||
# Aggregators list other providers' models — never auto-switch TO them
|
||||
_AGGREGATORS = {"nous", "openrouter"}
|
||||
_AGGREGATORS = {"nous", "openrouter", "ai-gateway", "copilot", "kilocode"}
|
||||
|
||||
# If the model belongs to the current provider's catalog, don't suggest switching
|
||||
current_models = _PROVIDER_MODELS.get(current_provider, [])
|
||||
|
||||
@@ -578,7 +578,7 @@ def test_model_flow_custom_saves_verified_v1_base_url(monkeypatch, capsys):
|
||||
# After the probe detects a single model ("llm"), the flow asks
|
||||
# "Use this model? [Y/n]:" — confirm with Enter, then context length,
|
||||
# then display name.
|
||||
answers = iter(["http://localhost:8000", "local-key", "", "", ""])
|
||||
answers = iter(["http://localhost:8000", "local-key", "", "", "", ""])
|
||||
monkeypatch.setattr("builtins.input", lambda _prompt="": next(answers))
|
||||
monkeypatch.setattr("getpass.getpass", lambda _prompt="": next(answers))
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ class TestRunConversationSurrogateSanitization:
|
||||
mock_stream.return_value = mock_response
|
||||
mock_api.return_value = mock_response
|
||||
|
||||
agent = AIAgent(model="test/model", quiet_mode=True, skip_memory=True, skip_context_files=True)
|
||||
agent = AIAgent(model="test/model", api_key="test-key", base_url="http://localhost:1234/v1", quiet_mode=True, skip_memory=True, skip_context_files=True)
|
||||
agent.client = MagicMock()
|
||||
|
||||
# Pass a message with surrogates
|
||||
|
||||
@@ -62,5 +62,86 @@ def _ensure_telegram_mock() -> None:
|
||||
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()
|
||||
|
||||
@@ -284,9 +284,20 @@ class TestEnvVarOverride:
|
||||
# Tests for reply_to_text extraction in _handle_message
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class FakeDMChannel:
|
||||
# Build FakeDMChannel as a subclass of the real discord.DMChannel when the
|
||||
# library is installed — this guarantees isinstance() checks pass in
|
||||
# production code regardless of test ordering or monkeypatch state.
|
||||
try:
|
||||
import discord as _discord_lib
|
||||
_DMChannelBase = _discord_lib.DMChannel
|
||||
except (ImportError, AttributeError):
|
||||
_DMChannelBase = object
|
||||
|
||||
|
||||
class FakeDMChannel(_DMChannelBase):
|
||||
"""Minimal DM channel stub (skips mention / channel-allow checks)."""
|
||||
def __init__(self, channel_id: int = 100, name: str = "dm"):
|
||||
# Do NOT call super().__init__() — real DMChannel requires State
|
||||
self.id = channel_id
|
||||
self.name = name
|
||||
|
||||
@@ -309,10 +320,6 @@ def _make_message(*, content: str = "hi", reference=None):
|
||||
@pytest.fixture
|
||||
def reply_text_adapter(monkeypatch):
|
||||
"""DiscordAdapter wired for _handle_message → handle_message capture."""
|
||||
import gateway.platforms.discord as discord_platform
|
||||
|
||||
monkeypatch.setattr(discord_platform.discord, "DMChannel", FakeDMChannel, raising=False)
|
||||
|
||||
config = PlatformConfig(enabled=True, token="fake-token")
|
||||
adapter = DiscordAdapter(config)
|
||||
adapter._client = SimpleNamespace(user=SimpleNamespace(id=999))
|
||||
|
||||
@@ -322,7 +322,7 @@ class TestFallbackTransportInit:
|
||||
seen_kwargs.append(kwargs.copy())
|
||||
return FakeTransport([], {})
|
||||
|
||||
for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "https_proxy", "http_proxy", "all_proxy"):
|
||||
for key in ("HTTPS_PROXY", "HTTP_PROXY", "ALL_PROXY", "https_proxy", "http_proxy", "all_proxy", "TELEGRAM_PROXY"):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
monkeypatch.setenv("HTTPS_PROXY", "http://proxy.example:8080")
|
||||
monkeypatch.setattr(tnet.httpx, "AsyncHTTPTransport", factory)
|
||||
|
||||
@@ -59,7 +59,7 @@ def _make_agent(monkeypatch, api_mode, provider, response_fn):
|
||||
self._disable_streaming = True
|
||||
return super().run_conversation(msg, conversation_history=conversation_history, task_id=task_id)
|
||||
|
||||
return _A(model="test-model", api_key="test-key", provider=provider, api_mode=api_mode)
|
||||
return _A(model="test-model", api_key="test-key", base_url="http://localhost:1234/v1", provider=provider, api_mode=api_mode)
|
||||
|
||||
|
||||
def _anthropic_resp(input_tok, output_tok, cache_read=0, cache_creation=0):
|
||||
|
||||
@@ -4115,8 +4115,8 @@ class TestMemoryNudgeCounterPersistence:
|
||||
"""Counters must exist on the agent after __init__."""
|
||||
with patch("run_agent.get_tool_definitions", return_value=[]):
|
||||
a = AIAgent(
|
||||
model="test", api_key="test-key", provider="openrouter",
|
||||
skip_context_files=True, skip_memory=True,
|
||||
model="test", api_key="test-key", base_url="http://localhost:1234/v1",
|
||||
provider="openrouter", skip_context_files=True, skip_memory=True,
|
||||
)
|
||||
assert hasattr(a, "_turns_since_memory")
|
||||
assert hasattr(a, "_iters_since_skill")
|
||||
|
||||
Reference in New Issue
Block a user