hermes-agent/tests/hermes_cli/test_model_switch_persist_default.py
Alex Yates fad4b40d9d fix(model): persist /model switch by default across sessions
A plain /model <name> switch only lasted for the current session — every
new session reverted to the previously-configured model, so users had to
re-switch every time (e.g. glm-5.1 -> glm-5.2 on every launch).

Persist-by-default is now the behavior across all three /model surfaces
(CLI, gateway, TUI/dashboard), gated by a new config key
model.persist_switch_by_default (default true):

  /model <name>             switch model (persists to config.yaml)
  /model <name> --session   switch for this session only
  /model <name> --global    switch and persist (explicit, unchanged)

The effective persistence is resolved once via resolve_persist_behavior()
in hermes_cli/model_switch.py so --session opts out, --global opts in,
and the config-gated default applies otherwise. --global remains a valid
explicit no-op alias for the new default.
2026-06-19 07:07:06 -07:00

122 lines
4.2 KiB
Python

"""Tests for persist-by-default model switching.
Covers:
- ``parse_model_flags`` recognises ``--session`` (and keeps ``--global``).
- ``resolve_persist_behavior`` applies the config-gated default and the
``--session`` / ``--global`` overrides.
- The default (no flags) persists, which is the user-facing fix: a plain
``/model <name>`` survives across sessions.
"""
from unittest.mock import patch
from hermes_cli.model_switch import parse_model_flags, resolve_persist_behavior
# ---------------------------------------------------------------------------
# parse_model_flags
# ---------------------------------------------------------------------------
class TestParseModelFlagsSession:
def test_no_flags(self):
assert parse_model_flags("sonnet") == ("sonnet", "", False, False, False)
def test_global_flag(self):
assert parse_model_flags("sonnet --global") == ("sonnet", "", True, False, False)
def test_session_flag(self):
assert parse_model_flags("sonnet --session") == (
"sonnet",
"",
False,
False,
True,
)
def test_session_with_provider(self):
assert parse_model_flags("sonnet --provider anthropic --session") == (
"sonnet",
"anthropic",
False,
False,
True,
)
def test_refresh_flag_still_parsed(self):
assert parse_model_flags("--refresh") == ("", "", False, True, False)
def test_unicode_dash_session_normalized(self):
# Telegram/iOS auto-converts -- to en/em dashes.
assert parse_model_flags("sonnet \u2013session") == (
"sonnet",
"",
False,
False,
True,
)
# ---------------------------------------------------------------------------
# resolve_persist_behavior
# ---------------------------------------------------------------------------
class TestResolvePersistBehavior:
def test_session_flag_always_session_only(self):
# --session opts out even if the config default is True.
with _config({"model": {"persist_switch_by_default": True}}):
assert resolve_persist_behavior(False, True) is False
def test_global_flag_always_persists(self):
# --global forces persist even if the config default is False.
with _config({"model": {"persist_switch_by_default": False}}):
assert resolve_persist_behavior(True, False) is True
def test_default_persists_when_config_missing(self):
# No model section at all → built-in default (True).
with _config({}):
assert resolve_persist_behavior(False, False) is True
def test_default_persists_when_key_true(self):
with _config({"model": {"persist_switch_by_default": True}}):
assert resolve_persist_behavior(False, False) is True
def test_default_session_only_when_key_false(self):
with _config({"model": {"persist_switch_by_default": False}}):
assert resolve_persist_behavior(False, False) is False
def test_default_when_model_is_flat_string(self):
# Fresh install: ``model: ""`` (not a dict) → built-in default True.
with _config({"model": ""}):
assert resolve_persist_behavior(False, False) is True
def test_session_overrides_global_when_both_set(self):
# --session is the explicit opt-out and wins over --global.
with _config({"model": {"persist_switch_by_default": True}}):
assert resolve_persist_behavior(True, True) is False
# ---------------------------------------------------------------------------
# helper
# ---------------------------------------------------------------------------
class _config:
"""Context manager that patches ``load_config`` to return a fixed dict."""
def __init__(self, cfg: dict):
self.cfg = cfg
def __enter__(self):
self._patch = patch(
"hermes_cli.config.load_config",
return_value=self.cfg,
)
# resolve_persist_behavior imports load_config lazily inside the
# function, so patching the source module is sufficient.
self._patch.start()
return self
def __exit__(self, *exc):
self._patch.stop()