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).
This commit is contained in:
teknium1
2026-09-23 04:39:20 -07:00
committed by Teknium
parent e692d61d19
commit 42746fcc71
5 changed files with 148 additions and 0 deletions

View File

@@ -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)

View File

@@ -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()

View File

@@ -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():

View File

@@ -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

View File

@@ -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