"""Model pickers under a Nous free-tier identity. A guest identity carrying inference shows one row, "Nous ยท free tier", with the single model ``nous/welcome``; the same guest with ``nous.guest: false`` shows no Nous row at all. The rule lives in one helper (``_free_tier_nous_row``) and this file exercises it through the real row builders against a temp ``HERMES_HOME`` with the network catalog fetch stubbed. """ from __future__ import annotations import os from pathlib import Path import pytest from hermes_cli.auth import _save_auth_store GUEST_STATE = { "auth_method": "anonymous", "account_tier": "anonymous", "anon_token": "anon_t", "access_token": "aaa.bbb.ccc", "expires_at": "2030-01-01T00:00:00+00:00", "inference_base_url": "https://welcome-api.nousresearch.com/v1", } @pytest.fixture def guest_home(monkeypatch, tmp_path): """Seed a guest identity as the only Nous state and keep every row builder offline.""" monkeypatch.setenv("HERMES_SHARED_AUTH_DIR", str(tmp_path / "shared-store")) monkeypatch.setenv("HERMES_GUEST_ONBOARDING", "1") for var in ("OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "NOUS_API_KEY", "LM_API_KEY", "LM_BASE_URL"): monkeypatch.delenv(var, raising=False) _save_auth_store({"active_provider": "nous", "providers": {"nous": dict(GUEST_STATE)}}) # No network from any lap: models.dev, the Portal catalog, and Ollama Cloud all stubbed. from agent import models_dev from hermes_cli import models as models_mod from hermes_cli import model_switch_providers as msp monkeypatch.setattr(models_dev, "fetch_models_dev", lambda *a, **k: {}) monkeypatch.setattr(models_mod, "get_curated_nous_model_ids", lambda *a, **k: ["anthropic/claude-x", "openai/gpt-y"]) monkeypatch.setattr(models_mod, "fetch_ollama_cloud_models", lambda *a, **k: []) monkeypatch.setattr(msp, "_nous_picker_model_ids", lambda *a, **k: pytest.fail("guest must not fetch the Portal catalog")) return Path(os.environ["HERMES_HOME"]) def _write_config(monkeypatch, home: Path, text: str) -> None: (home / "config.yaml").write_text(text) from hermes_cli import config as cfg_mod for attr in ("_config_cache", "_cached_config"): if hasattr(cfg_mod, attr): monkeypatch.setattr(cfg_mod, attr, None, raising=False) def _nous_rows(rows): return [r for r in rows if str(r.get("slug", "")).lower() == "nous"] def _cli_nous_rows(config): from hermes_cli.main_provider_setup import _build_provider_picker_rows ordered, _ = _build_provider_picker_rows(config, "nous", {}, {}) return [(key, label) for key, label, _members in ordered if key == "nous"] def test_guest_identity_shows_free_tier_row_with_only_welcome_model(guest_home, monkeypatch): from hermes_cli.model_switch_providers import list_picker_providers rows = _nous_rows(list_picker_providers("nous", "", None, None, 50, "nous/welcome")) assert len(rows) == 1 row = rows[0] assert "free tier" in row["name"] assert row["models"] == ["nous/welcome"] assert row["total_models"] == 1 rendered = repr(row).lower() assert "guest" not in rendered and "anonymous" not in rendered # The `hermes model` provider picker applies the same rule from the same helper. cli_rows = _cli_nous_rows({}) assert len(cli_rows) == 1 assert "free tier" in cli_rows[0][1] assert "guest" not in cli_rows[0][1].lower() and "anonymous" not in cli_rows[0][1].lower() def test_guest_identity_with_guest_off_hides_the_nous_row(guest_home, monkeypatch): _write_config(monkeypatch, guest_home, "nous:\n guest: false\n") from hermes_cli import anon_auth assert anon_auth.has_guest() and not anon_auth.guest_enabled() from hermes_cli.model_switch_providers import list_picker_providers assert _nous_rows(list_picker_providers("nous", "", None, None, 50, "nous/welcome")) == [] assert _cli_nous_rows({"nous": {"guest": False}}) == [] def test_desktop_picker_payload_never_locks_the_free_tier_row(guest_home, monkeypatch): """``model.options`` (the desktop's path) prices rows from caches on a normal open; the free-tier row has no Portal pricing and no entitlement to read, so it must be left alone rather than locked.""" from hermes_cli import inventory monkeypatch.setattr(inventory, "_prewarm_pricing_async", lambda *a, **k: None) payload = inventory.build_model_options_payload(inventory.load_picker_context()) rows = _nous_rows(payload["providers"]) assert len(rows) == 1 row = rows[0] assert row["free_tier_row"] is True assert row["models"] == ["nous/welcome"] assert row.get("unavailable_models", []) == [] assert not row.get("free_tier_pending") and not row.get("pricing_pending")