Files
hermes-agent/tests/hermes_cli/test_cli_fallback_chain_hot_reload.py
teknium1 064c847516 fix(tui_gateway,cli): per-turn fallback sync fails closed on a torn config; classic CLI syncs too (#95066)
Review follow-up. `_sync_agent_fallback_with_config` read the chain through `_load_cfg()`, which
fails open to `{}` on a torn/invalid config.yaml — `get_fallback_chain({}) == []` then WIPED the
live agent's chain (the messaging gateway's `_refresh_fallback_model` keeps its last known-good
chain). The sync now reads `load_user_config_effective(fail_closed=True)` and skips the apply when
the read fails, so only a config that actually loaded can remove a chain.

The wiring test drives `_prepare_turn_input` (turn admission) instead of string-matching the
prompt_turn source, and covers removal and the active-fallback cooldown skip.

Sibling surface: the classic interactive CLI holds one long-lived agent and read the chain once in
`HermesCLI.__init__`; `HermesCLI.chat` now applies the same fail-closed per-turn sync (and refreshes
`self._fallback_model` so a rebuilt agent starts from the fresh chain). Docs table caveat added.
2026-09-19 01:43:15 -07:00

57 lines
2.3 KiB
Python

"""An open classic-CLI chat adopts a ``fallback_providers`` chain added after it started (#95066).
``HermesCLI`` holds one long-lived agent and read the chain once in ``__init__``; ``hermes fallback
add`` from another terminal never reached the open chat. The turn loop (``HermesCLI.chat``) now
re-reads the chain fail-closed, so a torn config.yaml keeps the last known-good chain.
"""
from __future__ import annotations
from types import SimpleNamespace
import pytest
import cli
from hermes_cli.config import get_config_path
FALLBACK = [{"provider": "xai-oauth", "model": "grok-4.6"}]
class _StopAfterSync(Exception):
pass
def _chat_turn(monkeypatch, shell, config_text: str) -> None:
"""Drive ``HermesCLI.chat`` past the fallback sync against ``config_text`` as the live config.yaml."""
path = get_config_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(config_text, encoding="utf-8")
monkeypatch.setattr(shell, "_ensure_runtime_credentials", lambda: True)
monkeypatch.setattr(shell, "_resolve_turn_agent_config",
lambda message: {"signature": shell._active_agent_route_signature, "model": None, "runtime": None})
monkeypatch.setattr(shell, "_init_agent", lambda **kw: True)
def stop(message, images):
raise _StopAfterSync()
monkeypatch.setattr(shell, "_chat_route_images", stop)
with pytest.raises(_StopAfterSync):
shell.chat("hello")
def test_chat_turn_adopts_chain_added_after_the_cli_opened_and_keeps_it_on_torn_config(monkeypatch):
shell = cli.HermesCLI(compact=True, max_turns=1)
shell.agent = SimpleNamespace(
_fallback_chain=[], _fallback_model=None, _fallback_index=0,
_fallback_activated=False, _rate_limited_until=0, _unavailable_fallback_keys=set(),
)
_chat_turn(monkeypatch, shell, "fallback_providers:\n - provider: xai-oauth\n model: grok-4.6\n")
assert shell.agent._fallback_chain == FALLBACK
assert shell.agent._fallback_model == FALLBACK[0]
assert shell._fallback_model == FALLBACK # a rebuilt agent starts from the fresh chain too
# Torn mid-edit write: keep the last known-good chain rather than wiping it.
_chat_turn(monkeypatch, shell, "fallback_providers: [\n - provider: {{{\n")
assert shell.agent._fallback_chain == FALLBACK