Files
hermes-agent/tests/hermes_cli/test_aux_config.py
T
Teknium ba030bc0db fix(test-seams): monkeypatch.setattr facade aliases — also patch the defining module (57 files)
Tests did monkeypatch.setattr(<facade module>, name) where name is now defined in a
sibling module and the production path reads the sibling's binding. Where production
reads through BOTH bindings the setattr is duplicated onto the defining module (import
added next to the existing alias import); where only the sibling reads it the target is
repointed. Seams whose production readers go through the facade are left alone.
2026-09-03 19:33:28 -07:00

197 lines
7.3 KiB
Python

"""Tests for the auxiliary-model configuration UI in ``hermes model``.
Covers the helper functions:
- ``_save_aux_choice`` writes to config.yaml without touching main model config
- ``_reset_aux_to_auto`` clears routing fields but preserves timeouts
- ``_format_aux_current`` renders current task config for the menu
- ``_AUX_TASKS`` stays in sync with ``DEFAULT_CONFIG["auxiliary"]``
These are pure-function tests — the interactive menu loops are not covered
here (they're stdin-driven curses prompts).
"""
from __future__ import annotations
import pytest
from hermes_cli.config import DEFAULT_CONFIG, load_config
from hermes_cli.main_provider_setup import _AUX_TASKS, _DELEGATION_TASK_KEY, _delegation_cfg_as_task, _format_aux_current, _reset_aux_to_auto, _save_aux_choice
# ── Default config ──────────────────────────────────────────────────────────
def test_title_generation_present_in_default_config():
"""`title_generation` task must be defined in DEFAULT_CONFIG.
Regression for an existing gap: title_generator.py calls
``call_llm(task="title_generation", ...)`` but the task was missing
from DEFAULT_CONFIG["auxiliary"], so the config-backed timeout/provider
overrides never worked for that task.
"""
assert "title_generation" in DEFAULT_CONFIG["auxiliary"]
tg = DEFAULT_CONFIG["auxiliary"]["title_generation"]
assert tg["enabled"] is True
assert tg["provider"] == "auto"
assert tg["model"] == ""
assert tg["prefer_fast_model"] is False
assert tg["timeout"] > 0
assert tg["extra_body"] == {}
# ── _format_aux_current ─────────────────────────────────────────────────────
# ── _save_aux_choice ────────────────────────────────────────────────────────
def test_save_aux_choice_persists_to_config_yaml(tmp_path, monkeypatch):
"""Saving a task writes provider/model/base_url/api_key to auxiliary.<task>."""
from pathlib import Path
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
monkeypatch.setattr(Path, "home", lambda: tmp_path)
(tmp_path / ".hermes").mkdir(exist_ok=True)
_save_aux_choice(
"vision", provider="openrouter", model="google/gemini-2.5-flash",
)
cfg = load_config()
v = cfg["auxiliary"]["vision"]
assert v["provider"] == "openrouter"
assert v["model"] == "google/gemini-2.5-flash"
assert v["base_url"] == ""
assert v["api_key"] == ""
# ── _reset_aux_to_auto ──────────────────────────────────────────────────────
# ── Menu dispatch ───────────────────────────────────────────────────────────
# ── Delegation entry (top-level `delegation.*`, not `auxiliary.*`) ──────────
def _isolate_home(tmp_path, monkeypatch):
from pathlib import Path
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
monkeypatch.setattr(Path, "home", lambda: tmp_path)
(tmp_path / ".hermes").mkdir(exist_ok=True)
def test_save_delegation_writes_top_level_section(tmp_path, monkeypatch):
"""Delegation picks write to delegation.*, never auxiliary.delegation."""
_isolate_home(tmp_path, monkeypatch)
_save_aux_choice(
_DELEGATION_TASK_KEY, provider="openrouter", model="google/gemini-3-flash",
)
cfg = load_config()
d = cfg["delegation"]
assert d["provider"] == "openrouter"
assert d["model"] == "google/gemini-3-flash"
assert d["base_url"] == ""
assert d["api_key"] == ""
aux = cfg.get("auxiliary", {})
entry = aux.get("delegation", {}) if isinstance(aux, dict) else {}
assert not (isinstance(entry, dict) and entry.get("provider")), (
"delegation routing leaked into auxiliary.delegation"
)
def test_save_delegation_auto_stores_empty_provider(tmp_path, monkeypatch):
"""'auto' (inherit parent) persists as empty strings — never the literal
'auto', which delegate_tool would resolve as a provider name."""
_isolate_home(tmp_path, monkeypatch)
_save_aux_choice(_DELEGATION_TASK_KEY, provider="openrouter", model="m")
_save_aux_choice(_DELEGATION_TASK_KEY, provider="auto", model="", base_url="", api_key="")
cfg = load_config()
d = cfg["delegation"]
assert d["provider"] == ""
assert d["model"] == ""
assert d["base_url"] == ""
assert d["api_key"] == ""
def test_reset_aux_clears_delegation_routing_preserves_settings(tmp_path, monkeypatch):
"""Reset-all clears delegation provider/model/base_url/api_key but leaves
non-routing delegation settings (max_concurrent_children, etc.) alone."""
from hermes_cli.config import load_config as _lc, save_config
_isolate_home(tmp_path, monkeypatch)
cfg = _lc()
cfg.setdefault("delegation", {})
cfg["delegation"].update(
{"provider": "openrouter", "model": "x", "max_concurrent_children": 7}
)
save_config(cfg)
n = _reset_aux_to_auto()
assert n >= 1
cfg = _lc()
d = cfg["delegation"]
assert d["provider"] == ""
assert d["model"] == ""
assert d["max_concurrent_children"] == 7
def test_delegation_cfg_as_task_projection():
"""Projection renders empty provider as auto via _format_aux_current."""
assert _format_aux_current(_delegation_cfg_as_task({})) == "auto"
shaped = _delegation_cfg_as_task(
{"delegation": {"provider": "nous", "model": "Hermes-4.5"}}
)
assert _format_aux_current(shaped) == "nous · Hermes-4.5"
# Non-dict delegation section must not crash
assert _format_aux_current(_delegation_cfg_as_task({"delegation": "bogus"})) == "auto"
def test_leave_unchanged_replaces_cancel_label(tmp_path, monkeypatch):
"""The bottom cancel entry now reads 'Leave unchanged' (UX polish)."""
from pathlib import Path
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
monkeypatch.setattr(Path, "home", lambda: tmp_path)
(tmp_path / ".hermes").mkdir(exist_ok=True)
from hermes_cli import main as main_mod
import hermes_cli.main_provider_setup as hermes_cli_main_provider_setup
captured: list[list[str]] = []
def fake_prompt(choices, *, default=0):
captured.append(list(choices))
# Pick 'Leave unchanged' (last item) to exit cleanly
for i, label in enumerate(choices):
if label == "Leave unchanged":
return i
raise AssertionError("Leave unchanged not in provider list")
monkeypatch.setattr(main_mod, "_prompt_provider_choice", fake_prompt)
monkeypatch.setattr(hermes_cli_main_provider_setup, "_prompt_provider_choice", fake_prompt)
main_mod.select_provider_and_model()
assert captured, "provider menu never rendered"
labels = captured[0]
assert "Leave unchanged" in labels
assert "Cancel" not in labels, "Cancel label should be replaced"
assert any("Configure auxiliary models" in label for label in labels)