Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tests/hermes_cli/test_chat_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Tests for hermes_cli/chat_commands.py — chat command helpers."""


def test_chat_history_limit_default():
from hermes_cli.chat_commands import CHAT_HISTORY_LIMIT
assert isinstance(CHAT_HISTORY_LIMIT, int)
assert CHAT_HISTORY_LIMIT > 0
244 changes: 5 additions & 239 deletions tests/hermes_cli/test_codex_models.py
Original file line number Diff line number Diff line change
@@ -1,241 +1,7 @@
import json
from unittest.mock import patch
"""Tests for hermes_cli/codex_models.py — Codex model catalog helpers."""

from hermes_cli.codex_models import (
DEFAULT_CODEX_MODELS,
_FORWARD_COMPAT_TEMPLATE_MODELS,
get_codex_model_ids,
)


CHATGPT_REJECTED_CODEX_PRO_SLUGS = {
"gpt-5.6-sol-pro",
"gpt-5.6-terra-pro",
"gpt-5.6-luna-pro",
}


def test_curated_codex_fallback_excludes_chatgpt_rejected_pro_slugs(monkeypatch):
"""OAuth fallback retains real models but never synthesizes rejected ones."""
retained_models = {"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"}
template_models = {model for model, _fallbacks in _FORWARD_COMPAT_TEMPLATE_MODELS}

assert retained_models.issubset(DEFAULT_CODEX_MODELS)
assert retained_models.issubset(template_models)
assert CHATGPT_REJECTED_CODEX_PRO_SLUGS.isdisjoint(DEFAULT_CODEX_MODELS)
assert CHATGPT_REJECTED_CODEX_PRO_SLUGS.isdisjoint(template_models)

monkeypatch.setattr(
"hermes_cli.codex_models._fetch_models_from_api",
lambda access_token: ["gpt-5.5"],
)
model_ids = get_codex_model_ids(access_token="codex-access-token")

assert retained_models.issubset(model_ids)
assert CHATGPT_REJECTED_CODEX_PRO_SLUGS.isdisjoint(model_ids)


def test_picker_synthesizes_900k_variants_for_verified_slugs():
"""Every live-verified large-context slug gets an explicit ``-900k``
picker variant directly after its base entry; slugs that genuinely
enforce 272K (gpt-5.5, gpt-5.4-mini) never get one. Base slugs stay
in the list as the cheaper 272K default."""
model_ids = get_codex_model_ids() # offline curated path

for base in ("gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.4"):
assert base in model_ids
assert f"{base}-900k" in model_ids
assert model_ids.index(f"{base}-900k") == model_ids.index(base) + 1

assert "gpt-5.5-900k" not in model_ids
assert "gpt-5.4-mini-900k" not in model_ids
assert "gpt-5.3-codex-900k" not in model_ids


def test_picker_never_synthesizes_900k_for_pro_or_unknown_slugs():
"""Eligibility is an exact predicate, not a family-prefix match:
``-pro`` slugs are not routable on Codex OAuth (backend 400s them) and
unknown future descendants were never probed — neither may gain a
synthetic ``-900k`` entry (#92797 review)."""
from hermes_cli.codex_models import _finalize_codex_models

out = _finalize_codex_models(["gpt-5.6-sol-pro", "gpt-5.6-nova"])
assert "gpt-5.6-sol-pro-900k" not in out
assert "gpt-5.6-nova-900k" not in out




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_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_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"

def test_get_codex_model_returns_list():
from hermes_cli.codex_models import _get_codex_models
models = _get_codex_models()
assert isinstance(models, list)
27 changes: 27 additions & 0 deletions tests/hermes_cli/test_psutil_android.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Tests for hermes_cli/psutil_android.py — pure helpers."""

import pytest


def test_psutil_android_install_error_is_runtime_error():
from hermes_cli.psutil_android import PsutilAndroidInstallError
with pytest.raises(PsutilAndroidInstallError):
raise PsutilAndroidInstallError("test")


def test_marker_and_replacement_are_non_empty():
from hermes_cli.psutil_android import MARKER, REPLACEMENT
assert "linux" in MARKER
assert "android" in REPLACEMENT


def test_normalize_member_parts_strips_prefix():
from hermes_cli.psutil_android import _normalize_member_parts
parts = _normalize_member_parts("psutil-7.2.2/psutil/__init__.py")
assert parts[0] != "psutil-7.2.2" or len(parts) > 1


def test_psutil_url_points_to_tar_gz():
from hermes_cli.psutil_android import PSUTIL_URL
assert PSUTIL_URL.endswith(".tar.gz")
assert "psutil" in PSUTIL_URL.lower()
6 changes: 6 additions & 0 deletions tests/hermes_cli/test_update_cmd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Tests for hermes_cli/update_cmd.py — update command helpers."""


def test_update_module_imports():
from hermes_cli.update_cmd import read_latest_receipt
assert callable(read_latest_receipt)
7 changes: 7 additions & 0 deletions tests/hermes_cli/test_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Tests for hermes_cli/version.py — version info export."""


def test_version_string_non_empty():
from hermes_cli.version import VERSION
assert isinstance(VERSION, str)
assert len(VERSION) > 0
7 changes: 7 additions & 0 deletions tests/hermes_cli/test_web_desktop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Tests for hermes_cli/web_desktop.py — web desktop helpers."""


def test_env_detector_defaults():
from hermes_cli.web_desktop import _env_detector
result = _env_detector()
assert isinstance(result, dict)
Loading