hermes-agent/tests/hermes_cli/test_codex_models.py
Teknium 6b81590c55
test: prune low-value tests suite-wide (wave 1) — 46,820 → 28,106 test functions
Systematic prune per AGENTS.md test policy, one pass over every major
test tree (gateway, hermes_cli, tools, agent, run_agent, plugins, cli,
cron, tui_gateway, honcho/openviking, root-level):

- DELETE: source-reading tests (read_text/getsource on prod files),
  change-detector tests (exact catalog counts, model-name snapshots,
  config version literals), mock-echo tests (assert a mock returns what
  it was told), assertion-free/trivial tests, near-duplicate
  parametrizations (boundaries + one representative kept), async/sync
  twin duplicates, cosmetic within-file variations.
- KEEP (mandatory): security/redaction/approval guards, message-role
  alternation invariants, prompt-caching/deterministic-call-id
  invariants, issue-number regression tests (deduped), E2E tests.
- 6 test files deleted outright (script-style/no-assert or fully
  redundant); conftest.py, fakes/, fixtures/ untouched.
- tests/acp/conftest.py added: autouse fixture stubs the live
  models.dev/GitHub/Copilot/Anthropic inventory fetches that ACP server
  tests performed on every session create — test_server.py 147s → 3.4s,
  and the tests are now genuinely hermetic.
- Sleep-based slowness shrunk where safe (codex_ttfb_watchdog,
  compression_concurrent_fork, etc.); no wall-clock assertion tightened.

Verification: full hermetic suite via scripts/run_tests.sh —
2439 files, 31,130 tests passed, 0 failed, 0 flaky retries, 315s wall
(baseline: 583s wall, 13,564s subprocess CPU).
2026-07-29 13:10:23 -07:00

313 lines
11 KiB
Python

import json
from unittest.mock import patch
from hermes_cli.codex_models import DEFAULT_CODEX_MODELS, get_codex_model_ids
def test_get_codex_model_ids_prioritizes_default_and_cache(tmp_path, monkeypatch):
codex_home = tmp_path / "codex-home"
codex_home.mkdir(parents=True, exist_ok=True)
(codex_home / "config.toml").write_text('model = "gpt-5.2-codex"\n')
(codex_home / "models_cache.json").write_text(
json.dumps(
{
"models": [
{"slug": "gpt-5.3-codex", "priority": 20, "supported_in_api": True},
{"slug": "gpt-5.3-codex-spark", "priority": 6, "supported_in_api": False},
{"slug": "gpt-5.1-codex", "priority": 5, "supported_in_api": True},
{"slug": "gpt-5.4", "priority": 1, "supported_in_api": True},
{"slug": "gpt-5-hidden-codex", "priority": 2, "visibility": "hidden"},
]
}
)
)
monkeypatch.setenv("CODEX_HOME", str(codex_home))
models = get_codex_model_ids()
assert models[0] == "gpt-5.2-codex"
assert "gpt-5.1-codex" in models
assert "gpt-5.3-codex" in models
# Codex CLI marks Spark unsupported in the public API, but the Codex
# backend still accepts it via the OAuth-backed CLI/Hermes route.
assert "gpt-5.3-codex-spark" in models
# Non-codex-suffixed models are included when the cache says they're available
assert "gpt-5.4" in models
assert "gpt-5.4-mini" in models
assert "gpt-5-hidden-codex" not in models
def test_setup_wizard_codex_import_resolves():
"""Regression test for #712: setup.py must import the correct function name."""
# This mirrors the exact import used in hermes_cli/setup.py line 873.
# A prior bug had 'get_codex_models' (wrong) instead of 'get_codex_model_ids'.
from hermes_cli.codex_models import get_codex_model_ids as setup_import
assert callable(setup_import)
def test_get_codex_model_ids_adds_forward_compat_models_from_templates(monkeypatch):
monkeypatch.setattr(
"hermes_cli.codex_models._fetch_models_from_api",
lambda access_token: ["gpt-5.3-codex"],
)
models = get_codex_model_ids(access_token="codex-access-token")
# When live discovery only returns gpt-5.3-codex, forward-compat synthesis
# should surface gpt-5.5, gpt-5.4, gpt-5.4-mini, and gpt-5.3-codex-spark
# (each is templated off gpt-5.3-codex).
assert models == [
"gpt-5.3-codex",
"gpt-5.5",
"gpt-5.4-mini",
"gpt-5.4",
"gpt-5.3-codex-spark",
]
def test_fetch_from_api_keeps_supported_in_api_false_models(monkeypatch):
"""Regression: gpt-5.3-codex-spark is returned by the live Codex backend
with ``supported_in_api: false`` because it isn't in the public OpenAI
API. The Codex CLI / OAuth route still serves it for ChatGPT Pro
accounts, so we must not drop it on that flag. visibility=hidden is
the separate signal that *should* still filter entries out.
"""
import sys
from hermes_cli import codex_models
class _FakeResp:
status_code = 200
def json(self):
return {
"models": [
{"slug": "gpt-5.5", "priority": 0, "supported_in_api": True},
{"slug": "gpt-5.3-codex-spark", "priority": 7, "supported_in_api": False},
{"slug": "gpt-5-internal", "priority": 99, "visibility": "hidden"},
]
}
class _FakeHttpx:
@staticmethod
def get(url, headers=None, timeout=None):
return _FakeResp()
monkeypatch.setitem(sys.modules, "httpx", _FakeHttpx)
models = codex_models._fetch_models_from_api(access_token="tok")
assert "gpt-5.5" in models
assert "gpt-5.3-codex-spark" in models
assert "gpt-5-internal" not in models
def test_fetch_from_api_sends_chatgpt_account_id_header(monkeypatch):
"""The Codex /models endpoint only returns the per-account catalog when
the ``ChatGPT-Account-Id`` header is present. Without it, the response
is ``{"models":[]}`` (HTTP 200), which makes the picker silently
degrade to the curated fallback list and send invalid slugs on later
requests. Regression test for the upstream bug behind slow first
responses and HTTP 520/120s SSE hangs.
"""
import sys
from hermes_cli import codex_models
captured = {}
class _FakeResp:
status_code = 200
def json(self):
return {"models": [{"slug": "gpt-5.6-sol", "priority": 0}]}
class _FakeHttpx:
@staticmethod
def get(url, headers=None, timeout=None):
captured["url"] = url
captured["headers"] = dict(headers or {})
return _FakeResp()
monkeypatch.setitem(sys.modules, "httpx", _FakeHttpx)
# Hand-crafted JWT carrying the chatgpt_account_id claim.
import base64
import json
payload = base64.urlsafe_b64encode(
json.dumps(
{"https://api.openai.com/auth": {"chatgpt_account_id": "acct-test-123"}}
).encode()
).rstrip(b"=").decode()
fake_jwt = f"header.{payload}.sig"
models = codex_models._fetch_models_from_api(access_token=fake_jwt)
assert captured["headers"]["Authorization"] == f"Bearer {fake_jwt}"
assert captured["headers"].get("ChatGPT-Account-Id") == "acct-test-123"
assert "gpt-5.6-sol" in models
def test_model_command_uses_runtime_access_token_for_codex_list(monkeypatch):
from hermes_cli.main import _model_flow_openai_codex
captured = {}
choices = iter(["1"])
monkeypatch.setattr("builtins.input", lambda prompt="": next(choices))
monkeypatch.setattr(
"hermes_cli.auth.get_codex_auth_status",
lambda: {"logged_in": True},
)
monkeypatch.setattr(
"hermes_cli.auth.resolve_codex_runtime_credentials",
lambda *args, **kwargs: {"api_key": "codex-access-token"},
)
def _fake_get_codex_model_ids(access_token=None):
captured["access_token"] = access_token
return ["gpt-5.2-codex", "gpt-5.2"]
def _fake_prompt_model_selection(model_ids, current_model="", **_kwargs):
captured["model_ids"] = list(model_ids)
captured["current_model"] = current_model
return None
monkeypatch.setattr(
"hermes_cli.codex_models.get_codex_model_ids",
_fake_get_codex_model_ids,
)
monkeypatch.setattr(
"hermes_cli.auth._prompt_model_selection",
_fake_prompt_model_selection,
)
_model_flow_openai_codex({}, current_model="openai/gpt-5.4")
assert captured["access_token"] == "codex-access-token"
assert captured["model_ids"] == ["gpt-5.2-codex", "gpt-5.2"]
assert captured["current_model"] == "openai/gpt-5.4"
def test_model_command_prompts_to_reuse_or_reauthenticate_codex_session(monkeypatch, capsys):
from hermes_cli.main import _model_flow_openai_codex
captured = {"login_calls": 0}
choices = iter(["2"])
monkeypatch.setattr("builtins.input", lambda prompt="": next(choices))
monkeypatch.setattr(
"hermes_cli.auth.get_codex_auth_status",
lambda: {"logged_in": True, "source": "hermes-auth-store"},
)
monkeypatch.setattr(
"hermes_cli.auth.resolve_codex_runtime_credentials",
lambda *args, **kwargs: {"api_key": "fresh-codex-token"},
)
def _fake_login(*args, force_new_login=False, **kwargs):
captured["login_calls"] += 1
captured["force_new_login"] = force_new_login
monkeypatch.setattr("hermes_cli.auth._login_openai_codex", _fake_login)
monkeypatch.setattr(
"hermes_cli.codex_models.get_codex_model_ids",
lambda access_token=None: ["gpt-5.4", "gpt-5.3-codex"],
)
monkeypatch.setattr(
"hermes_cli.auth._prompt_model_selection",
lambda model_ids, current_model="", **_kwargs: None,
)
_model_flow_openai_codex({}, current_model="gpt-5.4")
out = capsys.readouterr().out
assert "Use existing credentials" in out
assert "Reauthenticate (new OAuth login)" in out
assert captured["login_calls"] == 1
assert captured["force_new_login"] is True
# ── Tests for _normalize_model_for_provider ──────────────────────────
def _make_cli(model="anthropic/claude-opus-4.6", **kwargs):
"""Create a HermesCLI with minimal mocking."""
import cli as _cli_mod
from cli import HermesCLI
_clean_config = {
"model": {
"default": "anthropic/claude-opus-4.6",
"base_url": "https://openrouter.ai/api/v1",
"provider": "auto",
},
"display": {"compact": False, "tool_progress": "all", "resume_display": "full"},
"agent": {},
"terminal": {"env_type": "local"},
}
clean_env = {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}
with (
patch("cli.get_tool_definitions", return_value=[]),
patch.dict("os.environ", clean_env, clear=False),
patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": _clean_config}),
):
cli = HermesCLI(model=model, **kwargs)
return cli
class TestNormalizeModelForProvider:
"""_normalize_model_for_provider() trusts user-selected models.
Only two things happen:
1. Provider prefixes are stripped (API needs bare slugs)
2. The *untouched default* model is swapped for a Codex model
Everything else passes through — the API is the judge.
"""
def test_non_codex_provider_is_noop(self):
cli = _make_cli(model="gpt-5.4")
changed = cli._normalize_model_for_provider("openrouter")
assert changed is False
assert cli.model == "gpt-5.4"
def test_opencode_zen_claude_sets_messages_mode(self):
cli = _make_cli(model="opencode-zen/claude-sonnet-4-6")
cli.api_mode = "chat_completions"
changed = cli._normalize_model_for_provider("opencode-zen")
assert changed is True
assert cli.model == "claude-sonnet-4-6"
assert cli.api_mode == "anthropic_messages"
def test_default_model_replaced(self):
"""No model configured (empty default) gets swapped for codex."""
import cli as _cli_mod
_clean_config = {
"model": {
"default": "",
"base_url": "",
"provider": "auto",
},
"display": {"compact": False, "tool_progress": "all", "resume_display": "full"},
"agent": {},
"terminal": {"env_type": "local"},
}
# Don't pass model= so _model_is_default is True
with (
patch("cli.get_tool_definitions", return_value=[]),
patch.dict("os.environ", {"LLM_MODEL": "", "HERMES_MAX_ITERATIONS": ""}, clear=False),
patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": _clean_config}),
):
from cli import HermesCLI
cli = HermesCLI()
assert cli._model_is_default is True
with patch(
"hermes_cli.codex_models.get_codex_model_ids",
return_value=["gpt-5.3-codex", "gpt-5.4"],
):
changed = cli._normalize_model_for_provider("openai-codex")
assert changed is True
# Uses first from available list
assert cli.model == "gpt-5.3-codex"