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.
This commit is contained in:
205
tests/test_issue572.py
Normal file
205
tests/test_issue572.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""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"
|
||||
)
|
||||
Reference in New Issue
Block a user