hermes-agent/tests/hermes_cli/test_cli_model_reset.py

210 lines
6.2 KiB
Python

"""Tests for the CLI ``/model reset`` command (NS-563).
In the CLI a session-scoped ``/model`` switch lives directly in
``self.model``/``self.provider`` (no durable pin), so ``/model reset``
re-derives the config.yaml default and switches back to it session-scoped,
never persisting. Parity with the gateway/TUI ``/model reset``.
"""
from __future__ import annotations
from types import SimpleNamespace
from hermes_cli.model_switch import ModelSwitchResult
class _FakeAgent:
def __init__(self):
self.calls = []
self.model = "deepseek/deepseek-v4"
self.provider = "openrouter"
def switch_model(self, **kwargs):
self.calls.append(kwargs)
self.model = kwargs["new_model"]
self.provider = kwargs["new_provider"]
class _StubCLI:
model = "deepseek/deepseek-v4"
provider = "openrouter"
requested_provider = "openrouter"
api_key = "sk-old"
_explicit_api_key = "sk-old"
base_url = "https://openrouter.ai/api/v1"
_explicit_base_url = "https://openrouter.ai/api/v1"
api_mode = "chat_completions"
agent = None
conversation_history: list = []
_pending_model_switch_note = None
_pending_one_turn_model_restore = None
def _confirm_expensive_model_switch(self, result):
return True
def _bind_real_methods(stub, cli_mod):
"""Bind the real reset + apply methods onto the stub instance."""
for name in (
"_handle_model_reset_command",
"_apply_model_switch_result",
"_clear_persisted_context_for_model_switch",
):
method = getattr(cli_mod.HermesCLI, name, None)
if method is not None:
setattr(stub, name, method.__get__(stub))
def _patch_common(monkeypatch, cli_mod, printed, *, config_model):
monkeypatch.setattr(cli_mod, "_cprint", lambda s, *a, **k: printed.append(str(s)))
monkeypatch.setattr(
cli_mod,
"save_config_value",
lambda *a, **k: (_ for _ in ()).throw(
AssertionError("reset must never persist config")
),
)
monkeypatch.setattr(
"hermes_cli.config.load_config_readonly",
lambda: {"model": config_model},
)
monkeypatch.setattr(
"hermes_cli.inventory.load_picker_context",
lambda: SimpleNamespace(
user_providers=None,
custom_providers=None,
with_overrides=lambda **_: SimpleNamespace(
user_providers=None, custom_providers=None
),
),
)
monkeypatch.setattr(
"hermes_cli.model_switch.resolve_display_context_length",
lambda *a, **k: None,
)
def test_reset_switches_back_to_config_default(monkeypatch):
import cli as cli_mod
stub = _StubCLI()
stub.agent = _FakeAgent()
_bind_real_methods(stub, cli_mod)
stub.model = "anthropic/claude-sonnet-4.6"
stub.provider = "anthropic"
stub.agent.model = "anthropic/claude-sonnet-4.6"
stub.agent.provider = "anthropic"
printed: list = []
_patch_common(
monkeypatch,
cli_mod,
printed,
config_model={"default": "deepseek/deepseek-v4", "provider": "openrouter"},
)
captured = {}
def fake_switch_model(**kwargs):
captured.update(kwargs)
return ModelSwitchResult(
success=True,
new_model="deepseek/deepseek-v4",
target_provider="openrouter",
api_key="sk-or",
base_url="https://openrouter.ai/api/v1",
api_mode="chat_completions",
provider_label="OpenRouter",
)
monkeypatch.setattr("hermes_cli.model_switch.switch_model", fake_switch_model)
cli_mod.HermesCLI._handle_model_switch(stub, "/model reset")
assert stub.model == "deepseek/deepseek-v4"
assert stub.provider == "openrouter"
assert captured["raw_input"] == "deepseek/deepseek-v4"
assert captured["explicit_provider"] == "openrouter"
assert captured["is_global"] is False
assert any("cleared" in line for line in printed)
def test_reset_clears_pending_one_turn_restore(monkeypatch):
import cli as cli_mod
stub = _StubCLI()
stub.agent = _FakeAgent()
_bind_real_methods(stub, cli_mod)
stub._pending_one_turn_model_restore = {"model": "deepseek/deepseek-v4"}
printed: list = []
_patch_common(
monkeypatch,
cli_mod,
printed,
config_model={"default": "deepseek/deepseek-v4", "provider": "openrouter"},
)
cli_mod.HermesCLI._handle_model_switch(stub, "/model reset")
assert stub._pending_one_turn_model_restore is None
def test_reset_noop_when_already_on_default(monkeypatch):
import cli as cli_mod
stub = _StubCLI()
stub.agent = _FakeAgent()
_bind_real_methods(stub, cli_mod)
printed: list = []
_patch_common(
monkeypatch,
cli_mod,
printed,
config_model={"default": "deepseek/deepseek-v4", "provider": "openrouter"},
)
monkeypatch.setattr(
"hermes_cli.model_switch.switch_model",
lambda **_: (_ for _ in ()).throw(
AssertionError("no switch needed when already on default")
),
)
cli_mod.HermesCLI._handle_model_switch(stub, "/model reset")
assert stub.model == "deepseek/deepseek-v4"
assert any("No session model override" in line for line in printed)
def test_reset_without_config_default_errors(monkeypatch):
import cli as cli_mod
stub = _StubCLI()
stub.agent = _FakeAgent()
_bind_real_methods(stub, cli_mod)
printed: list = []
_patch_common(monkeypatch, cli_mod, printed, config_model={})
cli_mod.HermesCLI._handle_model_switch(stub, "/model reset")
assert any("No model.default configured" in line for line in printed)
# Nothing mutated.
assert stub.model == "deepseek/deepseek-v4"
def test_reset_with_flags_is_parse_error(monkeypatch):
import cli as cli_mod
stub = _StubCLI()
stub.agent = _FakeAgent()
_bind_real_methods(stub, cli_mod)
printed: list = []
_patch_common(
monkeypatch,
cli_mod,
printed,
config_model={"default": "deepseek/deepseek-v4", "provider": "openrouter"},
)
cli_mod.HermesCLI._handle_model_switch(stub, "/model reset --global")
assert any("" in line for line in printed)
assert stub.model == "deepseek/deepseek-v4"