"""Regression tests for the /model picker's credential-discovery paths. Covers: - Normal path (tokens already in Hermes auth store) - Claude Code fallback (tokens only in ~/.claude/.credentials.json) - Negative case (no credentials anywhere) Note: auto-import from ~/.codex/auth.json was removed in #12360 — Hermes now owns its own openai-codex auth state, and users explicitly adopt existing Codex CLI tokens via `hermes auth openai-codex`. The old "Codex CLI shared file" discovery tests were removed with that change. """ import base64 import json import time from pathlib import Path import pytest def _make_fake_jwt(expiry_offset: int = 3600) -> str: """Build a fake JWT with a future expiry.""" header = base64.urlsafe_b64encode(b'{"alg":"RS256"}').rstrip(b"=").decode() exp = int(time.time()) + expiry_offset payload_bytes = json.dumps({"exp": exp, "sub": "test"}).encode() payload = base64.urlsafe_b64encode(payload_bytes).rstrip(b"=").decode() return f"{header}.{payload}.fakesig" @pytest.fixture() def hermes_auth_only_env(tmp_path, monkeypatch): """Tokens already in Hermes auth store (no Codex CLI needed).""" hermes_home = tmp_path / ".hermes" hermes_home.mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) # Point CODEX_HOME to nonexistent dir to prove it's not needed monkeypatch.setenv("CODEX_HOME", str(tmp_path / "no_codex")) (hermes_home / "auth.json").write_text(json.dumps({ "version": 2, "providers": { "openai-codex": { "tokens": { "access_token": _make_fake_jwt(), "refresh_token": "fake-refresh", }, "last_refresh": "2026-04-12T00:00:00Z", } }, })) for var in [ "OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "NOUS_API_KEY", "DEEPSEEK_API_KEY", ]: monkeypatch.delenv(var, raising=False) return hermes_home def test_normal_path_still_works(hermes_auth_only_env): """openai-codex appears when tokens are already in Hermes auth store.""" from hermes_cli.model_switch import list_authenticated_providers providers = list_authenticated_providers( current_provider="openai-codex", max_models=10, ) slugs = [p["slug"] for p in providers] assert "openai-codex" in slugs def test_codex_picker_uses_live_codex_catalog(hermes_auth_only_env, tmp_path, monkeypatch): """The gateway /model picker should surface Codex CLI-only listed models.""" from hermes_cli.model_switch import list_authenticated_providers codex_home = tmp_path / "codex-home" codex_home.mkdir() (codex_home / "models_cache.json").write_text(json.dumps({ "models": [ {"slug": "gpt-5.5", "priority": 0, "supported_in_api": True}, {"slug": "gpt-5.3-codex-spark", "priority": 7, "supported_in_api": False}, ] })) monkeypatch.setenv("CODEX_HOME", str(codex_home)) # Force the cache fallback path — without this the test issues a real # 10s HTTP probe to chatgpt.com/backend-api/codex/models which is both # slow and non-deterministic in CI/sandboxed environments. monkeypatch.setattr( "hermes_cli.codex_models._fetch_models_from_api", lambda access_token: [], ) providers = list_authenticated_providers( current_provider="openai-codex", max_models=10, ) codex = next(p for p in providers if p["slug"] == "openai-codex") assert "gpt-5.3-codex-spark" in codex["models"] assert codex["total_models"] == len(codex["models"]) @pytest.fixture() def claude_code_only_env(tmp_path, monkeypatch): """Set up an environment where Anthropic credentials only exist in ~/.claude/.credentials.json (Claude Code) — not in env vars or Hermes auth store.""" hermes_home = tmp_path / ".hermes" hermes_home.mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) # No Codex CLI monkeypatch.setenv("CODEX_HOME", str(tmp_path / "no_codex")) (hermes_home / "auth.json").write_text( json.dumps({"version": 2, "providers": {}}) ) # Claude Code credentials in the correct format claude_dir = tmp_path / ".claude" claude_dir.mkdir() (claude_dir / ".credentials.json").write_text(json.dumps({ "claudeAiOauth": { "accessToken": _make_fake_jwt(), "refreshToken": "fake-refresh", "expiresAt": int(time.time() * 1000) + 3_600_000, } })) # Patch Path.home() so the adapter finds the file monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) for var in [ "OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN", "NOUS_API_KEY", "DEEPSEEK_API_KEY", ]: monkeypatch.delenv(var, raising=False) return hermes_home def test_claude_code_file_detected_by_model_picker(claude_code_only_env): """anthropic should appear when credentials only exist in ~/.claude/.credentials.json.""" from hermes_cli.model_switch import list_authenticated_providers providers = list_authenticated_providers( current_provider="anthropic", max_models=10, ) slugs = [p["slug"] for p in providers] assert "anthropic" in slugs, ( f"anthropic not found in /model picker providers: {slugs}" ) anthropic = next(p for p in providers if p["slug"] == "anthropic") assert anthropic["is_current"] is True assert anthropic["total_models"] > 0 def test_no_codex_when_no_credentials(tmp_path, monkeypatch): """openai-codex should NOT appear when no credentials exist anywhere.""" hermes_home = tmp_path / ".hermes" hermes_home.mkdir() monkeypatch.setenv("HERMES_HOME", str(hermes_home)) monkeypatch.setenv("CODEX_HOME", str(tmp_path / "no_codex")) (hermes_home / "auth.json").write_text( json.dumps({"version": 2, "providers": {}}) ) for var in [ "OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "NOUS_API_KEY", "DEEPSEEK_API_KEY", "COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GEMINI_API_KEY", ]: monkeypatch.delenv(var, raising=False) from hermes_cli.model_switch import list_authenticated_providers providers = list_authenticated_providers( current_provider="openrouter", max_models=10, ) slugs = [p["slug"] for p in providers] assert "openai-codex" not in slugs, ( "openai-codex should not appear without any credentials" )