| """Regression tests for the `/model` slash command in the interactive CLI.""" |
|
|
| from unittest.mock import patch, MagicMock |
|
|
| from cli import HermesCLI |
|
|
|
|
| class TestModelCommand: |
| def _make_cli(self): |
| cli_obj = HermesCLI.__new__(HermesCLI) |
| cli_obj.model = "anthropic/claude-opus-4.6" |
| cli_obj.agent = object() |
| cli_obj.provider = "openrouter" |
| cli_obj.requested_provider = "openrouter" |
| cli_obj.base_url = "https://openrouter.ai/api/v1" |
| cli_obj.api_key = "test-key" |
| cli_obj._explicit_api_key = None |
| cli_obj._explicit_base_url = None |
| return cli_obj |
|
|
| def test_valid_model_from_api_saved_to_config(self, capsys): |
| cli_obj = self._make_cli() |
|
|
| with patch("hermes_cli.models.fetch_api_models", |
| return_value=["anthropic/claude-sonnet-4.5", "openai/gpt-5.4"]), \ |
| patch("cli.save_config_value", return_value=True) as save_mock: |
| cli_obj.process_command("/model anthropic/claude-sonnet-4.5") |
|
|
| output = capsys.readouterr().out |
| assert "saved to config" in output |
| assert cli_obj.model == "anthropic/claude-sonnet-4.5" |
| save_mock.assert_called_once_with("model.default", "anthropic/claude-sonnet-4.5") |
|
|
| def test_unlisted_model_accepted_with_warning(self, capsys): |
| cli_obj = self._make_cli() |
|
|
| with patch("hermes_cli.models.fetch_api_models", |
| return_value=["anthropic/claude-opus-4.6"]), \ |
| patch("cli.save_config_value") as save_mock: |
| cli_obj.process_command("/model anthropic/fake-model") |
|
|
| output = capsys.readouterr().out |
| assert "not found" in output or "Model changed" in output |
| assert cli_obj.model == "anthropic/fake-model" |
|
|
| def test_api_unreachable_accepts_and_persists(self, capsys): |
| cli_obj = self._make_cli() |
|
|
| with patch("hermes_cli.models.fetch_api_models", return_value=None), \ |
| patch("cli.save_config_value") as save_mock: |
| cli_obj.process_command("/model anthropic/claude-sonnet-next") |
|
|
| output = capsys.readouterr().out |
| assert "saved to config" in output |
| assert cli_obj.model == "anthropic/claude-sonnet-next" |
| save_mock.assert_called_once() |
|
|
| def test_no_slash_model_accepted_with_warning(self, capsys): |
| cli_obj = self._make_cli() |
|
|
| with patch("hermes_cli.models.fetch_api_models", |
| return_value=["openai/gpt-5.4"]) as fetch_mock, \ |
| patch("cli.save_config_value") as save_mock: |
| cli_obj.process_command("/model gpt-5.4") |
|
|
| output = capsys.readouterr().out |
| |
| assert cli_obj.model == "openai/gpt-5.4" |
|
|
| def test_validation_crash_falls_back_to_save(self, capsys): |
| cli_obj = self._make_cli() |
|
|
| with patch("hermes_cli.models.validate_requested_model", |
| side_effect=RuntimeError("boom")), \ |
| patch("cli.save_config_value", return_value=True) as save_mock: |
| cli_obj.process_command("/model anthropic/claude-sonnet-4.5") |
|
|
| output = capsys.readouterr().out |
| assert "saved to config" in output |
| assert cli_obj.model == "anthropic/claude-sonnet-4.5" |
| save_mock.assert_called_once() |
|
|
| def test_show_model_when_no_argument(self, capsys): |
| cli_obj = self._make_cli() |
| cli_obj.process_command("/model") |
|
|
| output = capsys.readouterr().out |
| assert "anthropic/claude-opus-4.6" in output |
| assert "OpenRouter" in output |
| assert "Authenticated providers" in output or "Switch model" in output |
| assert "provider" in output and "model" in output |
|
|
| |
|
|
| def test_provider_colon_model_switches_provider(self, capsys): |
| cli_obj = self._make_cli() |
|
|
| with patch("hermes_cli.runtime_provider.resolve_runtime_provider", return_value={ |
| "provider": "zai", |
| "api_key": "zai-key", |
| "base_url": "https://api.z.ai/api/paas/v4", |
| }), \ |
| patch("hermes_cli.models.fetch_api_models", |
| return_value=["glm-5", "glm-4.7"]), \ |
| patch("cli.save_config_value", return_value=True) as save_mock: |
| cli_obj.process_command("/model zai:glm-5") |
|
|
| output = capsys.readouterr().out |
| assert "glm-5" in output |
| assert "provider:" in output.lower() or "Z.AI" in output |
| assert cli_obj.model == "glm-5" |
| assert cli_obj.provider == "zai" |
| assert cli_obj.base_url == "https://api.z.ai/api/paas/v4" |
| |
| assert save_mock.call_count == 3 |
| save_calls = [c.args for c in save_mock.call_args_list] |
| assert ("model.default", "glm-5") in save_calls |
| assert ("model.provider", "zai") in save_calls |
| |
| assert any(c[0] == "model.base_url" for c in save_calls) |
|
|
| def test_provider_switch_fails_on_bad_credentials(self, capsys): |
| cli_obj = self._make_cli() |
|
|
| with patch("hermes_cli.runtime_provider.resolve_runtime_provider", |
| side_effect=Exception("No API key found")): |
| cli_obj.process_command("/model nous:hermes-3") |
|
|
| output = capsys.readouterr().out |
| assert "Could not resolve credentials" in output |
| assert cli_obj.model == "anthropic/claude-opus-4.6" |
| assert cli_obj.provider == "openrouter" |
|
|