| import json |
| import os |
| import sys |
| from unittest.mock import patch |
|
|
| sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) |
|
|
| 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.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 |
| |
| assert "gpt-5.4" 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.""" |
| |
| |
| from hermes_cli.codex_models import get_codex_model_ids as setup_import |
| assert callable(setup_import) |
|
|
|
|
| def test_get_codex_model_ids_falls_back_to_curated_defaults(tmp_path, monkeypatch): |
| codex_home = tmp_path / "codex-home" |
| codex_home.mkdir(parents=True, exist_ok=True) |
| monkeypatch.setenv("CODEX_HOME", str(codex_home)) |
|
|
| models = get_codex_model_ids() |
|
|
| assert models[: len(DEFAULT_CODEX_MODELS)] == DEFAULT_CODEX_MODELS |
| assert "gpt-5.4" in models |
| assert "gpt-5.3-codex-spark" in models |
|
|
|
|
| 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.2-codex"], |
| ) |
|
|
| models = get_codex_model_ids(access_token="codex-access-token") |
|
|
| assert models == ["gpt-5.2-codex", "gpt-5.3-codex", "gpt-5.4", "gpt-5.3-codex-spark"] |
|
|
|
|
| def test_model_command_uses_runtime_access_token_for_codex_list(monkeypatch): |
| from hermes_cli.main import _model_flow_openai_codex |
|
|
| captured = {} |
|
|
| 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=""): |
| 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 _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_bare_codex_model_passes_through(self): |
| cli = _make_cli(model="gpt-5.3-codex") |
| changed = cli._normalize_model_for_provider("openai-codex") |
| assert changed is False |
| assert cli.model == "gpt-5.3-codex" |
|
|
| def test_bare_non_codex_model_passes_through(self): |
| """gpt-5.4 (no 'codex' suffix) passes through β user chose it.""" |
| cli = _make_cli(model="gpt-5.4") |
| changed = cli._normalize_model_for_provider("openai-codex") |
| assert changed is False |
| assert cli.model == "gpt-5.4" |
|
|
| def test_any_bare_model_trusted(self): |
| """Even a non-OpenAI bare model passes through β user explicitly set it.""" |
| cli = _make_cli(model="claude-opus-4-6") |
| changed = cli._normalize_model_for_provider("openai-codex") |
| |
| assert changed is False |
| assert cli.model == "claude-opus-4-6" |
|
|
| def test_provider_prefix_stripped(self): |
| """openai/gpt-5.4 β gpt-5.4 (strip prefix, keep model).""" |
| cli = _make_cli(model="openai/gpt-5.4") |
| changed = cli._normalize_model_for_provider("openai-codex") |
| assert changed is True |
| assert cli.model == "gpt-5.4" |
|
|
| def test_any_provider_prefix_stripped(self): |
| """anthropic/claude-opus-4.6 β claude-opus-4.6 (strip prefix only). |
| User explicitly chose this β let the API decide if it works.""" |
| cli = _make_cli(model="anthropic/claude-opus-4.6") |
| changed = cli._normalize_model_for_provider("openai-codex") |
| assert changed is True |
| assert cli.model == "claude-opus-4.6" |
|
|
| def test_default_model_replaced(self): |
| """The untouched default (anthropic/claude-opus-4.6) gets swapped.""" |
| import cli as _cli_mod |
| _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"}, |
| } |
| |
| 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 |
| |
| assert cli.model == "gpt-5.3-codex" |
|
|
| def test_default_fallback_when_api_fails(self): |
| """Default model falls back to gpt-5.3-codex when API unreachable.""" |
| import cli as _cli_mod |
| _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"}, |
| } |
| 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() |
|
|
| with patch( |
| "hermes_cli.codex_models.get_codex_model_ids", |
| side_effect=Exception("offline"), |
| ): |
| changed = cli._normalize_model_for_provider("openai-codex") |
| assert changed is True |
| assert cli.model == "gpt-5.3-codex" |
|
|