From 42746fcc7152cf8ec6a49baba9b71755bacfd07a Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Wed, 23 Sep 2026 04:39:20 -0700 Subject: [PATCH] test: restore model picker, command menu and CLI session guards dropped by #120071 Restored: - test_commands.py::test_includes_builtin_commands_with_required_args: Telegram menu keeps built-ins that take args (/bg, /btw, /queue, /steer) (#24312). - test_codex_models.py::test_curated_codex_fallback_excludes_chatgpt_rejected_pro_slugs: forward-compat never adds ChatGPT-rejected -pro slugs. - test_provider_live_curated_merge.py: OpenCode Go/Zen merges and offline catalog never resurrect delisted/retired models (#95914, #115496). - test_banner.py::test_build_welcome_banner_does_not_center_pad_hero_art: braille hero renders flush-left (#9879). - test_cli_new_session.py::test_new_session_resets_token_counters: /new zeroes token/cost/API counters via real reset path (#2099). --- tests/hermes_cli/test_banner.py | 23 ++++++++ tests/hermes_cli/test_cli_new_session.py | 46 +++++++++++++++ tests/hermes_cli/test_codex_models.py | 13 +++++ tests/hermes_cli/test_commands.py | 9 +++ .../test_provider_live_curated_merge.py | 57 +++++++++++++++++++ 5 files changed, 148 insertions(+) diff --git a/tests/hermes_cli/test_banner.py b/tests/hermes_cli/test_banner.py index 5a2be7a13b..e1300b9ab7 100644 --- a/tests/hermes_cli/test_banner.py +++ b/tests/hermes_cli/test_banner.py @@ -58,3 +58,26 @@ def test_empty_model_shows_the_free_tier_route_when_it_carries_inference(tmp_pat assert "no model configured" in render(False) +def test_build_welcome_banner_does_not_center_pad_hero_art(): + """A braille hero relies on its own U+2800 padding for symmetry; Rich centering inserts + ASCII spaces around the left column and distorts the silhouette (#9879). The hero line + must start flush at the column start.""" + import io + from types import SimpleNamespace + + skin = SimpleNamespace(banner_hero="[green]\u2800X[/]", banner_logo="") + buf = io.StringIO() + with ( + patch.object(model_tools, "check_tool_availability", return_value=([], [])), + patch.object(banner, "get_available_skills", return_value={}), + patch.object(banner, "get_update_result", return_value=None), + patch.object(banner, "get_latest_release_tag", return_value=None), + patch.object(tools.mcp_tool_discovery, "get_mcp_status", return_value=[]), + patch.object(banner, "_active_skin", return_value=skin), + ): + console = Console(file=buf, force_terminal=False, color_system=None, width=80) + banner.build_welcome_banner(console=console, model="m", cwd="/tmp", tools=[], + get_toolset_for_tool=lambda _: None) + + hero_line = next(line for line in buf.getvalue().splitlines() if "\u2800X" in line) + assert hero_line.startswith("\u2502 \u2800X"), repr(hero_line) diff --git a/tests/hermes_cli/test_cli_new_session.py b/tests/hermes_cli/test_cli_new_session.py index ef53127aec..b4d8641f4e 100644 --- a/tests/hermes_cli/test_cli_new_session.py +++ b/tests/hermes_cli/test_cli_new_session.py @@ -245,6 +245,52 @@ def test_clear_command_starts_new_session_before_redrawing(tmp_path): +def test_new_session_resets_token_counters(tmp_path): + """Regression test for #2099: /new must zero all token counters. + + Drives the real ``AIAgent.reset_session_state`` (and the real context-engine + ``on_session_reset``) on the fake agent's attribute bag, so this guards both the + CLI wiring (/new must call the reset) and the reset itself. + """ + import types + + from agent.context_engine import ContextEngine + from run_agent import AIAgent + + cli = _prepare_cli_with_active_session(tmp_path) + agent = cli.agent + agent.reset_session_state = types.MethodType(AIAgent.reset_session_state, agent) + agent._transition_context_engine_session = types.MethodType( + AIAgent._transition_context_engine_session, agent + ) + comp = agent.context_compressor + comp.on_session_reset = types.MethodType(ContextEngine.on_session_reset, comp) + + assert agent.session_total_tokens > 0 + assert agent.session_api_calls > 0 + assert comp.compression_count > 0 + + cli.process_command("/new") + + assert agent.session_total_tokens == 0 + assert agent.session_input_tokens == 0 + assert agent.session_output_tokens == 0 + assert agent.session_prompt_tokens == 0 + assert agent.session_completion_tokens == 0 + assert agent.session_cache_read_tokens == 0 + assert agent.session_cache_write_tokens == 0 + assert agent.session_reasoning_tokens == 0 + assert agent.session_api_calls == 0 + assert agent.session_estimated_cost_usd == 0.0 + assert agent.session_cost_status == "unknown" + assert agent.session_cost_source == "none" + + assert comp.last_prompt_tokens == 0 + assert comp.last_completion_tokens == 0 + assert comp.last_total_tokens == 0 + assert comp.compression_count == 0 + + def test_new_session_with_title(capsys): """new_session(title=...) creates a session and sets the title.""" cli = _make_cli() diff --git a/tests/hermes_cli/test_codex_models.py b/tests/hermes_cli/test_codex_models.py index 67a7fc25b8..efd6c48f0a 100644 --- a/tests/hermes_cli/test_codex_models.py +++ b/tests/hermes_cli/test_codex_models.py @@ -13,6 +13,19 @@ CHATGPT_REJECTED_CODEX_PRO_SLUGS = { } +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"} + + 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(): diff --git a/tests/hermes_cli/test_commands.py b/tests/hermes_cli/test_commands.py index 1fa8dc10b1..42ed9d8a5f 100644 --- a/tests/hermes_cli/test_commands.py +++ b/tests/hermes_cli/test_commands.py @@ -122,6 +122,15 @@ class TestTelegramBotCommands: assert ("dashy", "does a - b - c") in telegram_bot_commands( include_plugins=False) + def test_includes_builtin_commands_with_required_args(self): + """Built-in arg-taking commands (e.g. /queue, /steer, /bg, /btw) + are now included because their handlers return usage text when + invoked without arguments — issue #24312.""" + names = {name for name, _ in telegram_bot_commands()} + assert "bg" in names + assert "btw" in names + assert "queue" in names + assert "steer" in names diff --git a/tests/hermes_cli/test_provider_live_curated_merge.py b/tests/hermes_cli/test_provider_live_curated_merge.py index becc764d5a..00cb627fa7 100644 --- a/tests/hermes_cli/test_provider_live_curated_merge.py +++ b/tests/hermes_cli/test_provider_live_curated_merge.py @@ -86,6 +86,63 @@ class TestGenericProviderLiveCuratedMerge: zen_result = set(provider_model_ids("opencode-zen")) assert {"a", "b", "c"} <= zen_result + def test_opencode_go_merge_does_not_resurrect_delisted_model(self): + """#95914 bug class, end-to-end through provider_model_ids with the REAL curated floor: + the Go relay (GET /zen/go/v1/models) delisted ox-alpha-free 2026-09-09 but may keep LISTING + it (#111749). Neither the live listing nor the curated floor may resurrect it, or the picker + keeps offering a model that now 401s.""" + assert "opencode-go" in _LIVE_FIRST_PICKER_PROVIDERS + live = ["deepseek-v4-flash", "kimi-k3", "omen-alpha", "ox-alpha-free"] + + with ( + patch("providers.get_provider_profile", return_value=self._make_profile(live)), + patch( + "hermes_cli.auth.resolve_api_key_provider_credentials", + return_value={"api_key": "k", "base_url": ""}, + ), + ): + result = provider_model_ids("opencode-go") + + assert "ox-alpha-free" not in result + assert {"deepseek-v4-flash", "kimi-k3", "omen-alpha"} <= set(result) + + def test_opencode_zen_merge_does_not_resurrect_retired_model(self): + """#115496 bug class, end-to-end through provider_model_ids with the REAL curated floor: + the Zen relay (GET /zen/v1/models) retired x-preview-f-free (the picker-facing id for Ox + Alpha) 2026-09-19. The live-first merge must not resurrect it from the curated floor + (models_catalog_static.py still lists it first), or the picker keeps offering a model that + now 401s (REVERT-PROOF: a stale floor re-adds it and this fails).""" + assert "opencode-zen" in _LIVE_FIRST_PICKER_PROVIDERS + live = ["kimi-k3", "gpt-5.6-sol", "claude-opus-5"] # current Zen relay (no x-preview-f-free) + + with ( + patch("providers.get_provider_profile", return_value=self._make_profile(live)), + patch( + "hermes_cli.auth.resolve_api_key_provider_credentials", + return_value={"api_key": "k", "base_url": ""}, + ), + ): + result = provider_model_ids("opencode-zen") + + assert "x-preview-f-free" not in result + assert {"kimi-k3", "gpt-5.6-sol", "claude-opus-5"} <= set(result) + + def test_opencode_zen_offline_catalog_drops_retired_model(self): + """#115496 without a key: no live fetch, so provider_model_ids serves the curated floor merged + with models.dev — both still carry the retired x-preview-f-free. The final rows must not.""" + with ( + patch("providers.get_provider_profile", return_value=self._make_profile(None)), + patch( + "hermes_cli.auth.resolve_api_key_provider_credentials", + return_value={"api_key": "", "base_url": ""}, + ), + patch("agent.models_dev.list_agentic_models", return_value=["x-preview-f-free", "kimi-k3"]), + ): + result = provider_model_ids("opencode-zen") + + assert "x-preview-f-free" not in result + assert "kimi-k3" in result +