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.
206 lines
9.7 KiB
Python
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"
|
|
)
|