File size: 5,658 Bytes
9aa5185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
"""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"  # accepted

    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
        # Auto-detection remaps bare model names to proper OpenRouter slugs
        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

    # -- provider switching tests -------------------------------------------

    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"
        # Model, provider, and base_url should be saved
        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
        # base_url is also persisted on provider change (Phase 2 fix)
        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"  # unchanged
        assert cli_obj.provider == "openrouter"  # unchanged