/ tests / hermes_cli / test_status_model_provider.py
test_status_model_provider.py
  1  """Tests for hermes_cli.status model/provider display."""
  2  
  3  from types import SimpleNamespace
  4  
  5  from hermes_cli.nous_subscription import NousFeatureState, NousSubscriptionFeatures
  6  
  7  
  8  def _patch_common_status_deps(monkeypatch, status_mod, tmp_path, *, openai_base_url=""):
  9      import hermes_cli.auth as auth_mod
 10  
 11      monkeypatch.setattr(status_mod, "get_env_path", lambda: tmp_path / ".env", raising=False)
 12      monkeypatch.setattr(status_mod, "get_hermes_home", lambda: tmp_path, raising=False)
 13  
 14      def _get_env_value(name: str):
 15          if name == "OPENAI_BASE_URL":
 16              return openai_base_url
 17          return ""
 18  
 19      monkeypatch.setattr(status_mod, "get_env_value", _get_env_value, raising=False)
 20      monkeypatch.setattr(auth_mod, "get_nous_auth_status", lambda: {}, raising=False)
 21      monkeypatch.setattr(auth_mod, "get_codex_auth_status", lambda: {}, raising=False)
 22      monkeypatch.setattr(
 23          status_mod.subprocess,
 24          "run",
 25          lambda *args, **kwargs: SimpleNamespace(stdout="inactive\n", returncode=3),
 26      )
 27  
 28  
 29  def test_show_status_displays_configured_dict_model_and_provider_label(monkeypatch, capsys, tmp_path):
 30      from hermes_cli import status as status_mod
 31  
 32      _patch_common_status_deps(monkeypatch, status_mod, tmp_path)
 33      monkeypatch.setattr(
 34          status_mod,
 35          "load_config",
 36          lambda: {"model": {"default": "anthropic/claude-sonnet-4", "provider": "anthropic"}},
 37          raising=False,
 38      )
 39      monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "anthropic", raising=False)
 40      monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "anthropic", raising=False)
 41      monkeypatch.setattr(status_mod, "provider_label", lambda provider: "Anthropic", raising=False)
 42  
 43      status_mod.show_status(SimpleNamespace(all=False, deep=False))
 44  
 45      out = capsys.readouterr().out
 46      assert "Model:        anthropic/claude-sonnet-4" in out
 47      assert "Provider:     Anthropic" in out
 48  
 49  
 50  def test_show_status_displays_legacy_string_model_and_custom_endpoint(monkeypatch, capsys, tmp_path):
 51      from hermes_cli import status as status_mod
 52  
 53      _patch_common_status_deps(monkeypatch, status_mod, tmp_path, openai_base_url="http://localhost:8080/v1")
 54      monkeypatch.setattr(status_mod, "load_config", lambda: {"model": "qwen3:latest"}, raising=False)
 55      monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "auto", raising=False)
 56      monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "openrouter", raising=False)
 57      monkeypatch.setattr(status_mod, "provider_label", lambda provider: "Custom endpoint" if provider == "custom" else provider, raising=False)
 58  
 59      status_mod.show_status(SimpleNamespace(all=False, deep=False))
 60  
 61      out = capsys.readouterr().out
 62      assert "Model:        qwen3:latest" in out
 63      assert "Provider:     Custom endpoint" in out
 64  
 65  
 66  def test_show_status_reports_managed_nous_features(monkeypatch, capsys, tmp_path):
 67      monkeypatch.setattr("hermes_cli.status.managed_nous_tools_enabled", lambda: True)
 68      from hermes_cli import status as status_mod
 69  
 70      _patch_common_status_deps(monkeypatch, status_mod, tmp_path)
 71      monkeypatch.setattr(
 72          status_mod,
 73          "load_config",
 74          lambda: {"model": {"default": "claude-opus-4-6", "provider": "nous"}},
 75          raising=False,
 76      )
 77      monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "nous", raising=False)
 78      monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "nous", raising=False)
 79      monkeypatch.setattr(status_mod, "provider_label", lambda provider: "Nous Portal", raising=False)
 80      monkeypatch.setattr(
 81          status_mod,
 82          "get_nous_subscription_features",
 83          lambda config: NousSubscriptionFeatures(
 84              subscribed=True,
 85              nous_auth_present=True,
 86              provider_is_nous=True,
 87              features={
 88                  "web": NousFeatureState("web", "Web tools", True, True, True, True, False, True, "firecrawl"),
 89                  "image_gen": NousFeatureState("image_gen", "Image generation", True, True, True, True, False, True, "Nous Subscription"),
 90                  "tts": NousFeatureState("tts", "OpenAI TTS", True, True, True, True, False, True, "OpenAI TTS"),
 91                  "browser": NousFeatureState("browser", "Browser automation", True, True, True, True, False, True, "Browser Use"),
 92                  "modal": NousFeatureState("modal", "Modal execution", False, True, False, False, False, True, "local"),
 93              },
 94          ),
 95          raising=False,
 96      )
 97  
 98      status_mod.show_status(SimpleNamespace(all=False, deep=False))
 99  
100      out = capsys.readouterr().out
101      assert "Nous Tool Gateway" in out
102      assert "Browser automation" in out
103      assert "active via Nous subscription" in out
104  
105  
106  def test_show_status_hides_nous_subscription_section_when_feature_flag_is_off(monkeypatch, capsys, tmp_path):
107      monkeypatch.setattr("hermes_cli.status.managed_nous_tools_enabled", lambda: False)
108      from hermes_cli import status as status_mod
109  
110      _patch_common_status_deps(monkeypatch, status_mod, tmp_path)
111      monkeypatch.setattr(
112          status_mod,
113          "load_config",
114          lambda: {"model": {"default": "claude-opus-4-6", "provider": "nous"}},
115          raising=False,
116      )
117      monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "nous", raising=False)
118      monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "nous", raising=False)
119      monkeypatch.setattr(status_mod, "provider_label", lambda provider: "Nous Portal", raising=False)
120  
121      status_mod.show_status(SimpleNamespace(all=False, deep=False))
122  
123      out = capsys.readouterr().out
124      assert "Nous Tool Gateway" not in out
125  
126  
127  def test_show_status_reports_empty_lmstudio_listing_as_reachable(monkeypatch, capsys, tmp_path):
128      from hermes_cli import status as status_mod
129  
130      _patch_common_status_deps(monkeypatch, status_mod, tmp_path)
131      monkeypatch.setattr(
132          status_mod,
133          "load_config",
134          lambda: {
135              "model": {
136                  "default": "qwen/qwen3-coder-30b",
137                  "provider": "lmstudio",
138                  "base_url": "http://127.0.0.1:1234/v1",
139              }
140          },
141          raising=False,
142      )
143      monkeypatch.setattr(status_mod, "resolve_requested_provider", lambda requested=None: "lmstudio", raising=False)
144      monkeypatch.setattr(status_mod, "resolve_provider", lambda requested=None, **kwargs: "lmstudio", raising=False)
145      monkeypatch.setattr(status_mod, "provider_label", lambda provider: "LM Studio", raising=False)
146      monkeypatch.setattr(
147          "hermes_cli.models.probe_lmstudio_models",
148          lambda api_key=None, base_url=None, timeout=5.0: [],
149      )
150  
151      status_mod.show_status(SimpleNamespace(all=False, deep=False))
152  
153      out = capsys.readouterr().out
154      assert "LM Studio" in out
155      assert "reachable (0 model(s)) at http://127.0.0.1:1234/v1" in out