Files
webui/tests/test_issue572.py
nesquena-hermes a512f2020e feat: MCP toolsets in WebUI + onboarding fix for non-standard providers — v0.50.63
Squash-merges PR #578 (rebased from #574 by @renheqiang + #575 by @nesquena-hermes). MCP server toolsets now included in WebUI sessions; onboarding wizard no longer fires for non-standard providers. 1331 tests pass. Nathan override applied for self-built #575.
2026-04-15 23:39:07 -07:00

206 lines
9.7 KiB
Python

"""Tests for issue #572: onboarding must not fire or overwrite config for
providers not in the quick-setup list (minimax-cn, deepseek, xai, etc.).
Root cause: _provider_api_key_present() only knew about the four providers in
_SUPPORTED_PROVIDER_SETUPS. For any other provider it returned False, causing
chat_ready=False, which made the wizard fire even when the user was fully
configured. The second part of the fix ensures _saveOnboardingProviderSetup()
in the frontend also skips the POST when current_is_oauth is set.
Covers:
1. _provider_api_key_present returns True for minimax-cn when
MINIMAX_CN_API_KEY is in env (via hermes_cli.auth.get_auth_status)
2. _status_from_runtime gives chat_ready=True for minimax-cn with a key set
3. get_onboarding_status returns completed=True for a fully-configured
unsupported provider when config.yaml exists
4. The hermes_cli import failure path is safe (falls back gracefully)
"""
from __future__ import annotations
import os
import pathlib
import sys
import types
from unittest import mock
import pytest
def _inject_hermes_cli_auth(get_auth_status_return):
"""Inject a minimal hermes_cli.auth stub into sys.modules.
CI doesn't install hermes_cli (it's a separate package). Tests that
exercise the hermes_cli fallback path must inject the module themselves
rather than relying on mock.patch('hermes_cli.auth.get_auth_status')
which fails with ModuleNotFoundError when the module isn't installed.
"""
mock_auth = types.ModuleType("hermes_cli.auth")
mock_auth.get_auth_status = mock.MagicMock(return_value=get_auth_status_return)
mock_hermes_cli = types.ModuleType("hermes_cli")
return mock.patch.dict(sys.modules, {
"hermes_cli": mock_hermes_cli,
"hermes_cli.auth": mock_auth,
})
# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
def _call_provider_api_key_present(provider: str, cfg: dict = None, env_values: dict = None):
from api.onboarding import _provider_api_key_present
return _provider_api_key_present(provider, cfg or {}, env_values or {})
# ---------------------------------------------------------------------------
# 1. _provider_api_key_present via hermes_cli fallback
# ---------------------------------------------------------------------------
class TestProviderApiKeyPresentFallback:
def test_minimax_cn_logged_in_returns_true(self):
"""minimax-cn: if hermes_cli.auth.get_auth_status returns logged_in, must be True."""
with mock.patch("api.onboarding._SUPPORTED_PROVIDER_SETUPS", {
"openrouter": {}, "anthropic": {}, "openai": {}, "custom": {}
}):
with _inject_hermes_cli_auth({"logged_in": True}):
result = _call_provider_api_key_present("minimax-cn")
assert result is True
def test_unsupported_provider_logged_out_returns_false(self):
"""Unsupported provider with no key → False, no crash."""
with mock.patch("api.onboarding._SUPPORTED_PROVIDER_SETUPS", {
"openrouter": {}, "anthropic": {}, "openai": {}, "custom": {}
}):
with _inject_hermes_cli_auth({"logged_in": False}):
result = _call_provider_api_key_present("deepseek")
assert result is False
def test_hermes_cli_import_failure_is_safe(self):
"""If hermes_cli is unavailable, falls back silently to False."""
import builtins
real_import = builtins.__import__
def _block_hermes_cli(name, *args, **kwargs):
if name.startswith("hermes_cli"):
raise ImportError("hermes_cli not available")
return real_import(name, *args, **kwargs)
with mock.patch("api.onboarding._SUPPORTED_PROVIDER_SETUPS", {
"openrouter": {}, "anthropic": {}, "openai": {}, "custom": {}
}):
with mock.patch("builtins.__import__", side_effect=_block_hermes_cli):
result = _call_provider_api_key_present("minimax-cn")
assert result is False # safe fallback
def test_supported_provider_still_works_without_fallback(self):
"""openrouter with env key must still succeed via the original path."""
from api.onboarding import _provider_api_key_present, _SUPPORTED_PROVIDER_SETUPS
env_values = {"OPENROUTER_API_KEY": "sk-test"}
result = _provider_api_key_present("openrouter", {}, env_values)
assert result is True
def test_inline_api_key_in_cfg_still_works(self):
"""model.api_key in config.yaml must be recognized for any provider."""
cfg = {"model": {"provider": "minimax-cn", "default": "MiniMax-M2.7", "api_key": "key123"}}
result = _call_provider_api_key_present("minimax-cn", cfg)
assert result is True
# ---------------------------------------------------------------------------
# 2. _status_from_runtime: unsupported provider with key → chat_ready=True
# ---------------------------------------------------------------------------
class TestStatusFromRuntimeUnsupportedProvider:
def _run(self, provider: str, model: str, api_key_present: bool, oauth_present: bool = False):
from api.onboarding import _status_from_runtime
cfg = {"model": {"provider": provider, "default": model}}
with (
mock.patch("api.onboarding._HERMES_FOUND", True),
mock.patch("api.onboarding._load_env_file", return_value={}),
mock.patch("api.onboarding._get_active_hermes_home", return_value=pathlib.Path("/tmp")),
mock.patch("api.onboarding._provider_api_key_present", return_value=api_key_present),
mock.patch("api.onboarding._provider_oauth_authenticated", return_value=oauth_present),
):
return _status_from_runtime(cfg, True)
def test_minimax_cn_with_key_gives_chat_ready(self):
"""minimax-cn + api key present → chat_ready must be True."""
result = self._run("minimax-cn", "MiniMax-M2.7", api_key_present=True)
assert result["chat_ready"] is True, f"Expected chat_ready=True, got: {result}"
assert result["provider_ready"] is True
assert result["setup_state"] == "ready"
def test_deepseek_with_key_gives_chat_ready(self):
"""deepseek + api key → chat_ready."""
result = self._run("deepseek", "deepseek-chat", api_key_present=True)
assert result["chat_ready"] is True
def test_unsupported_provider_no_key_no_oauth_gives_not_ready(self):
"""No key, no oauth → provider_ready=False."""
result = self._run("minimax-cn", "MiniMax-M2.7", api_key_present=False, oauth_present=False)
assert result["chat_ready"] is False
assert result["provider_ready"] is False
def test_oauth_provider_still_works_via_oauth_path(self):
"""openai-codex (OAuth) with no api_key but oauth present → ready."""
result = self._run("openai-codex", "codex-model", api_key_present=False, oauth_present=True)
assert result["chat_ready"] is True
# ---------------------------------------------------------------------------
# 3. get_onboarding_status: minimax-cn fully configured → completed=True
# ---------------------------------------------------------------------------
class TestOnboardingStatusUnsupportedProvider:
def _make_status(self, chat_ready: bool, provider: str = "minimax-cn"):
import api.onboarding as mod
fake_config_path = pathlib.Path("/tmp/_test_572_config.yaml")
cfg = {"model": {"provider": provider, "default": "MiniMax-M2.7"}}
runtime = {
"chat_ready": chat_ready,
"provider_configured": True,
"provider_ready": chat_ready,
"setup_state": "ready" if chat_ready else "provider_incomplete",
"provider_note": "test",
"current_provider": provider,
"current_model": "MiniMax-M2.7",
"current_base_url": None,
"env_path": "/tmp/.env",
}
with (
mock.patch.object(mod, "load_settings", return_value={}),
mock.patch.object(mod, "get_config", return_value=cfg),
mock.patch.object(mod, "verify_hermes_imports", return_value=(True, [], {})),
mock.patch.object(mod, "_status_from_runtime", return_value=runtime),
mock.patch.object(mod, "load_workspaces", return_value=[]),
mock.patch.object(mod, "get_last_workspace", return_value=None),
mock.patch.object(mod, "get_available_models", return_value=[]),
mock.patch.object(mod, "_get_config_path", return_value=fake_config_path),
mock.patch.object(pathlib.Path, "exists", return_value=True),
):
return mod.get_onboarding_status()
def test_minimax_cn_chat_ready_skips_wizard(self):
"""minimax-cn + chat_ready=True + config.yaml exists → wizard must NOT fire."""
result = self._make_status(chat_ready=True)
assert result["completed"] is True, (
"Wizard fired for minimax-cn user with valid config! "
"config.yaml + chat_ready=True must auto-complete onboarding regardless of provider."
)
def test_minimax_cn_not_ready_shows_wizard(self):
"""minimax-cn + chat_ready=False → wizard fires so user can fix it."""
result = self._make_status(chat_ready=False)
assert result["completed"] is False
def test_current_is_oauth_set_for_unsupported_provider(self):
"""setup.current_is_oauth must be True for minimax-cn (not in quick-setup list)."""
result = self._make_status(chat_ready=True)
assert result["setup"]["current_is_oauth"] is True, (
"current_is_oauth should be True for providers not in _SUPPORTED_PROVIDER_SETUPS"
)