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:
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user