From d0df264213664bf3bf77119ddcbac178fb410423 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:35:36 +0530 Subject: [PATCH] test(setup): add ZAI endpoint picker tests, move base-URL tests to MiniMax Z.AI now uses a curses picker instead of plain text input for base URL, so the existing TestBaseUrlValidation tests (which used zai as their test subject) are migrated to MiniMax, which still uses the text input path. Add TestZaiEndpointPicker covering: - Selecting each official endpoint (Global, China, Coding Plan Global, Coding Plan China) saves the correct base URL to config - Custom proxy URL entry (valid + invalid rejection) - Cancel keeps the existing base URL - Current endpoint is the default choice in the picker - Non-standard URL defaults to the Custom proxy option --- .../test_model_provider_persistence.py | 209 ++++++++++++++++-- 1 file changed, 186 insertions(+), 23 deletions(-) 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