diff --git a/tests/hermes_cli/test_model_provider_persistence.py b/tests/hermes_cli/test_model_provider_persistence.py index 75eb5b8dc70..bc4c1dfa32e 100644 --- a/tests/hermes_cli/test_model_provider_persistence.py +++ b/tests/hermes_cli/test_model_provider_persistence.py @@ -319,31 +319,37 @@ class TestProviderPersistsAfterModelSave: class TestBaseUrlValidation: - """Reject non-URL values in the base URL prompt (e.g. shell commands).""" + """Reject non-URL values in the base URL prompt (e.g. shell commands). + + Uses MiniMax instead of Z.AI because Z.AI now uses a curses-based + endpoint picker (_select_zai_endpoint) rather than the plain text + input() prompt. Z.AI picker behavior is covered in + TestZaiEndpointPicker below. + """ def test_invalid_base_url_rejected(self, config_home, monkeypatch, capsys): """Typing a non-URL string should not be saved as the base URL.""" from hermes_cli.auth import PROVIDER_REGISTRY - pconfig = PROVIDER_REGISTRY.get("zai") + pconfig = PROVIDER_REGISTRY.get("minimax") if not pconfig: - pytest.skip("zai not in PROVIDER_REGISTRY") + pytest.skip("minimax not in PROVIDER_REGISTRY") - monkeypatch.setenv("GLM_API_KEY", "test-key") + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") from hermes_cli.main import _model_flow_api_key_provider from hermes_cli.config import load_config, get_env_value # User types a shell command instead of a URL at the base URL prompt - with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + with patch("hermes_cli.auth._prompt_model_selection", return_value="MiniMax-M2"), \ patch("hermes_cli.auth.deactivate_provider"), \ patch("builtins.input", return_value="nano ~/.hermes/.env"): - _model_flow_api_key_provider(load_config(), "zai", "old-model") + _model_flow_api_key_provider(load_config(), "minimax", "old-model") # The garbage value should NOT have been saved - saved = get_env_value("GLM_BASE_URL") or "" + saved = get_env_value("MINIMAX_BASE_URL") or "" assert not saved or saved.startswith(("http://", "https://")), \ - f"Non-URL value was saved as GLM_BASE_URL: {saved}" + f"Non-URL value was saved as MINIMAX_BASE_URL: {saved}" captured = capsys.readouterr() assert "Invalid URL" in captured.out @@ -351,42 +357,199 @@ class TestBaseUrlValidation: """A proper URL should be saved normally.""" from hermes_cli.auth import PROVIDER_REGISTRY - pconfig = PROVIDER_REGISTRY.get("zai") + pconfig = PROVIDER_REGISTRY.get("minimax") if not pconfig: - pytest.skip("zai not in PROVIDER_REGISTRY") + pytest.skip("minimax not in PROVIDER_REGISTRY") - monkeypatch.setenv("GLM_API_KEY", "test-key") + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") from hermes_cli.main import _model_flow_api_key_provider from hermes_cli.config import load_config, get_env_value - with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + with patch("hermes_cli.auth._prompt_model_selection", return_value="MiniMax-M2"), \ patch("hermes_cli.auth.deactivate_provider"), \ - patch("builtins.input", return_value="https://custom.z.ai/api/paas/v4"): - _model_flow_api_key_provider(load_config(), "zai", "old-model") + patch("builtins.input", return_value="https://custom.minimax.example/v1"): + _model_flow_api_key_provider(load_config(), "minimax", "old-model") - saved = get_env_value("GLM_BASE_URL") or "" - assert saved == "https://custom.z.ai/api/paas/v4" + saved = get_env_value("MINIMAX_BASE_URL") or "" + assert saved == "https://custom.minimax.example/v1" def test_empty_base_url_keeps_default(self, config_home, monkeypatch): """Pressing Enter (empty) should not change the base URL.""" from hermes_cli.auth import PROVIDER_REGISTRY - pconfig = PROVIDER_REGISTRY.get("zai") + pconfig = PROVIDER_REGISTRY.get("minimax") if not pconfig: - pytest.skip("zai not in PROVIDER_REGISTRY") + pytest.skip("minimax not in PROVIDER_REGISTRY") - monkeypatch.setenv("GLM_API_KEY", "test-key") - monkeypatch.delenv("GLM_BASE_URL", raising=False) + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + monkeypatch.delenv("MINIMAX_BASE_URL", raising=False) from hermes_cli.main import _model_flow_api_key_provider from hermes_cli.config import load_config, get_env_value - with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + with patch("hermes_cli.auth._prompt_model_selection", return_value="MiniMax-M2"), \ + patch("hermes_cli.auth.deactivate_provider"), \ + patch("builtins.input", return_value=""): + _model_flow_api_key_provider(load_config(), "minimax", "old-model") + + saved = get_env_value("MINIMAX_BASE_URL") or "" + assert saved == "", "Empty input should not save a base URL" + + +class TestZaiEndpointPicker: + """Z.AI setup should present a curses picker for endpoint selection.""" + + def test_select_global_endpoint(self, config_home, monkeypatch): + """Selecting Global should save the direct API base URL.""" + from hermes_cli.auth import ZAI_ENDPOINTS + from hermes_cli.main import _model_flow_api_key_provider + from hermes_cli.config import load_config + + global_url = ZAI_ENDPOINTS[0][1] # "https://api.z.ai/api/paas/v4" + monkeypatch.setenv("GLM_API_KEY", "test-key") + + with patch("hermes_cli.main._prompt_provider_choice", return_value=0), \ + patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ patch("hermes_cli.auth.deactivate_provider"), \ patch("builtins.input", return_value=""): _model_flow_api_key_provider(load_config(), "zai", "old-model") - saved = get_env_value("GLM_BASE_URL") or "" - assert saved == "", "Empty input should not save a base URL" + model = load_config()["model"] + assert model["base_url"] == global_url + + def test_select_coding_plan_global_endpoint(self, config_home, monkeypatch): + """Selecting Coding Plan Global should save the coding base URL.""" + from hermes_cli.auth import ZAI_ENDPOINTS + from hermes_cli.main import _model_flow_api_key_provider + from hermes_cli.config import load_config + + coding_url = ZAI_ENDPOINTS[2][1] # coding-global + monkeypatch.setenv("GLM_API_KEY", "test-key") + + # Index 2 = Coding Plan Global in ZAI_ENDPOINTS + with patch("hermes_cli.main._prompt_provider_choice", return_value=2), \ + patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5.2"), \ + patch("hermes_cli.auth.deactivate_provider"), \ + patch("builtins.input", return_value=""): + _model_flow_api_key_provider(load_config(), "zai", "old-model") + + model = load_config()["model"] + assert model["base_url"] == coding_url + + def test_select_china_endpoint(self, config_home, monkeypatch): + """Selecting China should save the bigmodel.cn base URL.""" + from hermes_cli.auth import ZAI_ENDPOINTS + from hermes_cli.main import _model_flow_api_key_provider + from hermes_cli.config import load_config + + cn_url = ZAI_ENDPOINTS[1][1] # "https://open.bigmodel.cn/api/paas/v4" + monkeypatch.setenv("GLM_API_KEY", "test-key") + + with patch("hermes_cli.main._prompt_provider_choice", return_value=1), \ + patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + patch("hermes_cli.auth.deactivate_provider"), \ + patch("builtins.input", return_value=""): + _model_flow_api_key_provider(load_config(), "zai", "old-model") + + model = load_config()["model"] + assert model["base_url"] == cn_url + + def test_select_custom_proxy_url(self, config_home, monkeypatch): + """Selecting Custom proxy should prompt for a URL and save it.""" + from hermes_cli.main import _model_flow_api_key_provider + from hermes_cli.config import load_config, get_env_value + + monkeypatch.setenv("GLM_API_KEY", "test-key") + + # Last option index = custom proxy + with patch("hermes_cli.main._prompt_provider_choice", return_value=4), \ + patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + patch("hermes_cli.auth.deactivate_provider"), \ + patch("builtins.input", return_value="https://proxy.example.com/glm/v4"): + _model_flow_api_key_provider(load_config(), "zai", "old-model") + + saved = get_env_value("GLM_BASE_URL") or "" + assert saved == "https://proxy.example.com/glm/v4" + + def test_custom_proxy_rejects_invalid_url(self, config_home, monkeypatch, capsys): + """Custom proxy must start with http:// or https://.""" + from hermes_cli.main import _model_flow_api_key_provider + from hermes_cli.config import load_config + + monkeypatch.setenv("GLM_API_KEY", "test-key") + monkeypatch.delenv("GLM_BASE_URL", raising=False) + + with patch("hermes_cli.main._prompt_provider_choice", return_value=4), \ + patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + patch("hermes_cli.auth.deactivate_provider"), \ + patch("builtins.input", return_value="not-a-url"): + _model_flow_api_key_provider(load_config(), "zai", "old-model") + + # The invalid URL should not have been saved as base_url + model = load_config()["model"] + assert model["base_url"] != "not-a-url" + captured = capsys.readouterr() + assert "Invalid URL" in captured.out + + def test_cancel_keeps_existing_base_url(self, config_home, monkeypatch): + """Cancelling the picker should not change the base URL.""" + from hermes_cli.main import _model_flow_api_key_provider + from hermes_cli.config import load_config, get_env_value + + monkeypatch.setenv("GLM_API_KEY", "test-key") + monkeypatch.setenv("GLM_BASE_URL", "https://existing.example/v4") + + # _prompt_provider_choice returns None on cancel + with patch("hermes_cli.main._prompt_provider_choice", return_value=None), \ + patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \ + patch("hermes_cli.auth.deactivate_provider"), \ + patch("builtins.input", return_value=""): + _model_flow_api_key_provider(load_config(), "zai", "old-model") + + # env var is preserved (not overwritten on cancel) + saved = get_env_value("GLM_BASE_URL") or "" + assert saved == "https://existing.example/v4" + + def test_current_endpoint_is_default_choice(self, config_home, monkeypatch): + """When a known endpoint is already active, it should be the default.""" + from hermes_cli.auth import ZAI_ENDPOINTS + from hermes_cli.model_setup_flows import _select_zai_endpoint + + coding_url = ZAI_ENDPOINTS[2][1] # coding-global + + captured = {} + + def fake_choice(choices, *, default=0, title=""): + captured["default"] = default + captured["choices"] = choices + return default + + with patch("hermes_cli.main._prompt_provider_choice", side_effect=fake_choice): + result = _select_zai_endpoint(coding_url) + + # Default should point at index 2 (coding-global) + assert captured["default"] == 2 + assert result == coding_url + + def test_custom_url_active_defaults_to_custom_option(self, config_home, monkeypatch): + """When a non-standard URL is active, Custom proxy should be default.""" + from hermes_cli.auth import ZAI_ENDPOINTS + from hermes_cli.model_setup_flows import _select_zai_endpoint + + custom_url = "https://my-proxy.example.com/v4" + # 4 official endpoints → custom is index 4 + expected_default = len(ZAI_ENDPOINTS) + + captured = {} + + def fake_choice(choices, *, default=0, title=""): + captured["default"] = default + return default + + with patch("hermes_cli.main._prompt_provider_choice", side_effect=fake_choice), \ + patch("builtins.input", return_value=""): + _select_zai_endpoint(custom_url) + + assert captured["default"] == expected_default