Files
hermes-agent/tests/hermes_cli/test_sibling_config_migration.py
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

129 lines
4.7 KiB
Python

"""Fleet-wide config migration (#91277 Phase 2 — #20438/#54926/#79048 class).
`hermes update` migrated only the active profile's config.yaml; sibling
profiles silently drifted config versions until their gateway hit a config
the new code couldn't read. `_migrate_sibling_profile_configs()` runs the
same non-interactive safe migration for every sibling home, scoped via the
context-local HERMES_HOME override.
These tests use REAL config files on disk and the REAL migration pipeline —
only the profile-root location is pointed at tmp_path.
"""
import yaml
from pathlib import Path
import hermes_cli.update_cmd as update_cmd
import hermes_cli.update_cmd_config as update_cmd_config
def _write_profile(root: Path, name: str, version: int) -> Path:
home = root / name
home.mkdir(parents=True)
(home / "config.yaml").write_text(
yaml.safe_dump({"_config_version": version, "model": {"provider": "nous"}}),
encoding="utf-8",
)
return home
def _latest_version() -> int:
from hermes_cli.config import DEFAULT_CONFIG
return int(DEFAULT_CONFIG["_config_version"])
def _setup(monkeypatch, tmp_path, active_home: Path):
import hermes_cli.profiles as profiles_mod
monkeypatch.setattr(profiles_mod, "_get_profiles_root", lambda: tmp_path / "profiles")
import hermes_constants
monkeypatch.setattr(
hermes_constants, "get_process_hermes_home", lambda: active_home
)
monkeypatch.setattr(
update_cmd, "_reload_config_modules", lambda: None
) # module reload is orthogonal here; the real one re-imports from disk
monkeypatch.setattr(
update_cmd_config, "_reload_config_modules", lambda: None
) # module reload is orthogonal here; the real one re-imports from disk
def test_sibling_behind_is_migrated_on_disk(monkeypatch, tmp_path):
active = _write_profile(tmp_path / "profiles", "active", _latest_version())
sibling = _write_profile(tmp_path / "profiles", "research", 12)
_setup(monkeypatch, tmp_path, active)
migrated = update_cmd._migrate_sibling_profile_configs()
names = [m[0] for m in migrated]
assert "research" in names
entry = next(m for m in migrated if m[0] == "research")
assert entry[1] == 12 and entry[2] == _latest_version()
# REAL file on disk carries the new version
on_disk = yaml.safe_load((sibling / "config.yaml").read_text())
assert on_disk["_config_version"] == _latest_version()
# and user settings survived
assert on_disk["model"]["provider"] == "nous"
def test_active_profile_is_skipped(monkeypatch, tmp_path):
active = _write_profile(tmp_path / "profiles", "active", 12)
_setup(monkeypatch, tmp_path, active)
migrated = update_cmd._migrate_sibling_profile_configs()
assert "active" not in [m[0] for m in migrated]
# active home untouched (the caller's own migration handles it)
on_disk = yaml.safe_load((active / "config.yaml").read_text())
assert on_disk["_config_version"] == 12
def test_current_sibling_untouched(monkeypatch, tmp_path):
active = _write_profile(tmp_path / "profiles", "active", _latest_version())
sibling = _write_profile(tmp_path / "profiles", "work", _latest_version())
_setup(monkeypatch, tmp_path, active)
before = (sibling / "config.yaml").read_bytes()
migrated = update_cmd._migrate_sibling_profile_configs()
assert migrated == []
assert (sibling / "config.yaml").read_bytes() == before
def test_unconfigured_profile_skipped(monkeypatch, tmp_path):
active = _write_profile(tmp_path / "profiles", "active", _latest_version())
bare = tmp_path / "profiles" / "empty"
bare.mkdir()
_setup(monkeypatch, tmp_path, active)
assert update_cmd._migrate_sibling_profile_configs() == []
assert not (bare / "config.yaml").exists()
def test_one_broken_profile_does_not_block_others(monkeypatch, tmp_path):
active = _write_profile(tmp_path / "profiles", "active", _latest_version())
broken_home = tmp_path / "profiles" / "broken"
broken_home.mkdir()
(broken_home / "config.yaml").write_text(":\nnot yaml: [", encoding="utf-8")
_write_profile(tmp_path / "profiles", "healthy", 12)
_setup(monkeypatch, tmp_path, active)
migrated = update_cmd._migrate_sibling_profile_configs()
assert [m[0] for m in migrated] == ["healthy"]
def test_override_is_reset_after_run(monkeypatch, tmp_path):
"""The ContextVar override must not leak past the sweep."""
from hermes_constants import get_hermes_home_override
active = _write_profile(tmp_path / "profiles", "active", _latest_version())
_write_profile(tmp_path / "profiles", "research", 12)
_setup(monkeypatch, tmp_path, active)
before = get_hermes_home_override()
update_cmd._migrate_sibling_profile_configs()
assert get_hermes_home_override() == before