* refactor: re-architect tests to mirror the codebase
* Update tests.yml
* fix: add missing tool_error imports after registry refactor
* fix(tests): replace patch.dict with monkeypatch to prevent env var leaks under xdist
patch.dict(os.environ) can leak TERMINAL_ENV across xdist workers,
causing test_code_execution tests to hit the Modal remote path.
* fix(tests): fix update_check and telegram xdist failures
- test_update_check: replace patch("hermes_cli.banner.os.getenv") with
monkeypatch.setenv("HERMES_HOME") — banner.py no longer imports os
directly, it uses get_hermes_home() from hermes_constants.
- test_telegram_conflict/approval_buttons: provide real exception classes
for telegram.error mock (NetworkError, TimedOut, BadRequest) so the
except clause in connect() doesn't fail with "catching classes that do
not inherit from BaseException" when xdist pollutes sys.modules.
* fix(tests): accept unavailable_models kwarg in _prompt_model_selection mock
156 lines
6.8 KiB
Python
156 lines
6.8 KiB
Python
"""Tests for _setup_provider_model_selection and the zai/kimi/minimax branch.
|
|
|
|
Regression test for the is_coding_plan NameError that crashed setup when
|
|
selecting zai, kimi-coding, minimax, or minimax-cn providers.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_provider_registry():
|
|
"""Minimal PROVIDER_REGISTRY entries for tested providers."""
|
|
class FakePConfig:
|
|
def __init__(self, name, env_vars, base_url_env, inference_url):
|
|
self.name = name
|
|
self.api_key_env_vars = env_vars
|
|
self.base_url_env_var = base_url_env
|
|
self.inference_base_url = inference_url
|
|
|
|
return {
|
|
"zai": FakePConfig("ZAI", ["ZAI_API_KEY"], "ZAI_BASE_URL", "https://api.zai.example"),
|
|
"kimi-coding": FakePConfig("Kimi Coding", ["KIMI_API_KEY"], "KIMI_BASE_URL", "https://api.kimi.example"),
|
|
"minimax": FakePConfig("MiniMax", ["MINIMAX_API_KEY"], "MINIMAX_BASE_URL", "https://api.minimax.example"),
|
|
"minimax-cn": FakePConfig("MiniMax CN", ["MINIMAX_API_KEY"], "MINIMAX_CN_BASE_URL", "https://api.minimax-cn.example"),
|
|
"opencode-zen": FakePConfig("OpenCode Zen", ["OPENCODE_ZEN_API_KEY"], "OPENCODE_ZEN_BASE_URL", "https://opencode.ai/zen/v1"),
|
|
"opencode-go": FakePConfig("OpenCode Go", ["OPENCODE_GO_API_KEY"], "OPENCODE_GO_BASE_URL", "https://opencode.ai/zen/go/v1"),
|
|
}
|
|
|
|
|
|
class TestSetupProviderModelSelection:
|
|
"""Verify _setup_provider_model_selection works for all providers
|
|
that previously hit the is_coding_plan NameError."""
|
|
|
|
@pytest.mark.parametrize("provider_id,expected_defaults", [
|
|
("zai", ["glm-5", "glm-4.7", "glm-4.5", "glm-4.5-flash"]),
|
|
("kimi-coding", ["kimi-k2.5", "kimi-k2-thinking", "kimi-k2-turbo-preview"]),
|
|
("minimax", ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1"]),
|
|
("minimax-cn", ["MiniMax-M2.7", "MiniMax-M2.7-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.1"]),
|
|
("opencode-zen", ["gpt-5.4", "gpt-5.3-codex", "claude-sonnet-4-6", "gemini-3-flash"]),
|
|
("opencode-go", ["glm-5", "kimi-k2.5", "minimax-m2.5", "minimax-m2.7"]),
|
|
])
|
|
@patch("hermes_cli.models.fetch_api_models", return_value=[])
|
|
@patch("hermes_cli.config.get_env_value", return_value="fake-key")
|
|
def test_falls_back_to_default_models_without_crashing(
|
|
self, mock_env, mock_fetch, provider_id, expected_defaults, mock_provider_registry
|
|
):
|
|
"""Previously this code path raised NameError: 'is_coding_plan'.
|
|
Now it delegates to _setup_provider_model_selection which uses
|
|
_DEFAULT_PROVIDER_MODELS -- no crash, correct model list."""
|
|
from hermes_cli.setup import _setup_provider_model_selection
|
|
|
|
captured_choices = {}
|
|
|
|
def fake_prompt_choice(label, choices, default):
|
|
captured_choices["choices"] = choices
|
|
# Select "Keep current" (last item)
|
|
return len(choices) - 1
|
|
|
|
with patch("hermes_cli.auth.PROVIDER_REGISTRY", mock_provider_registry):
|
|
_setup_provider_model_selection(
|
|
config={"model": {}},
|
|
provider_id=provider_id,
|
|
current_model="some-model",
|
|
prompt_choice=fake_prompt_choice,
|
|
prompt_fn=lambda _: None,
|
|
)
|
|
|
|
# The offered model list should start with the default models
|
|
offered = captured_choices["choices"]
|
|
for model in expected_defaults:
|
|
assert model in offered, f"{model} not in choices for {provider_id}"
|
|
|
|
@patch("hermes_cli.models.fetch_api_models")
|
|
@patch("hermes_cli.config.get_env_value", return_value="fake-key")
|
|
def test_live_models_used_when_available(
|
|
self, mock_env, mock_fetch, mock_provider_registry
|
|
):
|
|
"""When fetch_api_models returns results, those are used instead of defaults."""
|
|
from hermes_cli.setup import _setup_provider_model_selection
|
|
|
|
live = ["live-model-1", "live-model-2"]
|
|
mock_fetch.return_value = live
|
|
|
|
captured_choices = {}
|
|
|
|
def fake_prompt_choice(label, choices, default):
|
|
captured_choices["choices"] = choices
|
|
return len(choices) - 1
|
|
|
|
with patch("hermes_cli.auth.PROVIDER_REGISTRY", mock_provider_registry):
|
|
_setup_provider_model_selection(
|
|
config={"model": {}},
|
|
provider_id="zai",
|
|
current_model="some-model",
|
|
prompt_choice=fake_prompt_choice,
|
|
prompt_fn=lambda _: None,
|
|
)
|
|
|
|
offered = captured_choices["choices"]
|
|
assert "live-model-1" in offered
|
|
assert "live-model-2" in offered
|
|
|
|
@patch("hermes_cli.models.fetch_api_models", return_value=[])
|
|
@patch("hermes_cli.config.get_env_value", return_value="fake-key")
|
|
def test_custom_model_selection(
|
|
self, mock_env, mock_fetch, mock_provider_registry
|
|
):
|
|
"""Selecting 'Custom model' lets user type a model name."""
|
|
from hermes_cli.setup import _setup_provider_model_selection, _DEFAULT_PROVIDER_MODELS
|
|
|
|
defaults = _DEFAULT_PROVIDER_MODELS["zai"]
|
|
custom_model_idx = len(defaults) # "Custom model" is right after defaults
|
|
|
|
config = {"model": {}}
|
|
|
|
def fake_prompt_choice(label, choices, default):
|
|
return custom_model_idx
|
|
|
|
with patch("hermes_cli.auth.PROVIDER_REGISTRY", mock_provider_registry):
|
|
_setup_provider_model_selection(
|
|
config=config,
|
|
provider_id="zai",
|
|
current_model="some-model",
|
|
prompt_choice=fake_prompt_choice,
|
|
prompt_fn=lambda _: "my-custom-model",
|
|
)
|
|
|
|
assert config["model"]["default"] == "my-custom-model"
|
|
|
|
@patch("hermes_cli.models.fetch_api_models", return_value=["opencode-go/kimi-k2.5", "opencode-go/minimax-m2.7"])
|
|
@patch("hermes_cli.config.get_env_value", return_value="fake-key")
|
|
def test_opencode_live_models_are_normalized_for_selection(
|
|
self, mock_env, mock_fetch, mock_provider_registry
|
|
):
|
|
from hermes_cli.setup import _setup_provider_model_selection
|
|
|
|
captured_choices = {}
|
|
|
|
def fake_prompt_choice(label, choices, default):
|
|
captured_choices["choices"] = choices
|
|
return len(choices) - 1
|
|
|
|
with patch("hermes_cli.auth.PROVIDER_REGISTRY", mock_provider_registry):
|
|
_setup_provider_model_selection(
|
|
config={"model": {}},
|
|
provider_id="opencode-go",
|
|
current_model="opencode-go/kimi-k2.5",
|
|
prompt_choice=fake_prompt_choice,
|
|
prompt_fn=lambda _: None,
|
|
)
|
|
|
|
offered = captured_choices["choices"]
|
|
assert "kimi-k2.5" in offered
|
|
assert "minimax-m2.7" in offered
|
|
assert all("opencode-go/" not in choice for choice in offered)
|