Files
hermes-agent/tests/hermes_cli/test_config_unversioned_migration.py
ethernet 2fb89006cd Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	agent/learning_mutations.py
2026-09-24 06:08:20 -04:00

83 lines
3.3 KiB
Python

"""A config.yaml without ``_config_version`` is current-schema content that was never stamped:
the installers seed it from cli-config.yaml.example and targeted writers (``hermes config set``,
/personality) never stamp. The one-time migration ladder must not treat it as a v0 install —
its value- and absence-based steps would overwrite what the user chose."""
import shutil
from pathlib import Path
import pytest
import hermes_yaml as yaml
TEMPLATE = Path(__file__).resolve().parents[2] / "cli-config.yaml.example"
USER_CHOICES = {
"delegation.max_concurrent_children": "3",
"delegation.max_iterations": "50",
"display.background_process_notifications": "all",
"agent.verify_on_stop": "true",
"curator.stale_after_days": "30",
"curator.archive_after_days": "90",
"model_catalog.ttl_hours": "24",
}
WATCHED = [*USER_CHOICES, "display.personality", "plugins.enabled"]
@pytest.fixture
def hermes_home(tmp_path, monkeypatch):
home = tmp_path / ".hermes"
home.mkdir()
monkeypatch.setattr(Path, "home", lambda: tmp_path) # sibling-profile scan stays in tmp
monkeypatch.setenv("HERMES_HOME", str(home))
return home
def _raw(home: Path) -> dict:
return yaml.safe_load((home / "config.yaml").read_text(encoding="utf-8")) or {}
def _at(raw: dict, dotted: str):
for part in dotted.split("."):
raw = raw.get(part) if isinstance(raw, dict) else None
return raw
def test_update_keeps_user_values_of_an_unversioned_config_and_migrates_legacy_keys(hermes_home):
from hermes_cli.config import DEFAULT_CONFIG, set_config_value
from hermes_cli.personality import persist_personality
from hermes_cli.update_cmd import _check_and_apply_config_migration
# Seeded before the template carried a stamp; early-2026 templates shipped this retired key.
(hermes_home / "config.yaml").write_text(
"compression:\n summary_model: google/gemini-3-flash-preview\n", encoding="utf-8")
# Installed with `hermes plugins install` and never enabled.
plugin = hermes_home / "plugins" / "notes-helper"
plugin.mkdir(parents=True)
(plugin / "plugin.yaml").write_text("name: notes-helper\nversion: 0.1.0\n", encoding="utf-8")
# User-authored SOUL.md section that happens to carry the v41 heading.
soul = "# Me\n\n## Messaging other agents\nmy own notes\n\n## Prefs\nkeep\n"
(hermes_home / "SOUL.md").write_text(soul, encoding="utf-8")
assert persist_personality("kawaii")
for key, value in USER_CHOICES.items():
set_config_value(key, value)
before = _raw(hermes_home)
_check_and_apply_config_migration()
after = _raw(hermes_home)
assert {k: _at(after, k) for k in WATCHED} == {k: _at(before, k) for k in WATCHED}
assert "summary_model" not in after["compression"]
assert _at(after, "auxiliary.compression.model") == _at(before, "compression.summary_model")
assert after["_config_version"] == DEFAULT_CONFIG["_config_version"]
assert (hermes_home / "SOUL.md").read_text(encoding="utf-8") == soul
def test_config_seeded_from_the_template_reads_as_current(hermes_home):
"""install.sh, install.ps1, docker/stage2-hook.sh and `hermes doctor --fix` copy the template."""
from hermes_cli.config import check_config_version
shutil.copy(TEMPLATE, hermes_home / "config.yaml")
current, latest = check_config_version(raise_on_parse_error=True)
assert current == latest