Four writers each dropped their own uniquely-named copy of config.yaml next to the real file and none of them ever deleted anything: hermes setup (config.yaml.bak.YYYYMMDD_HHMMSS, one per run even with no change), the corrupt-YAML snapshot (config.yaml.corrupt.<ts>.bak), hermes migrate xai (config.yaml.bak-pre-migrate-xai-<ts>) and the Docker boot migration (config.yaml.bak-<ts>, .env.bak-<ts>). A home dir accumulated a dozen variants with no way to tell which mattered. hermes_cli/config_backups.py::backup_config is now the single writer: backups/config/config.yaml.<reason>.<YYYYMMDD-HHMMSS>, skipped when the newest copy for that reason is byte-identical, rotated to the newest five per reason. backups/ is already excluded from full backups so nothing nests. Legacy siblings written by the old schemes are moved into the dir on first use; hand-named copies (config.yaml.bak-my-note) are left alone. Live: three `hermes setup --non-interactive` runs against an unchanged config went from three .bak files in HERMES_HOME to one pre-setup copy under backups/config/; repeated loads of broken YAML produce one corrupt copy instead of one per process (deduped by content).
41 lines
1.9 KiB
Python
41 lines
1.9 KiB
Python
"""config.yaml backups: one dir, deduped, bounded — never a pile of siblings in HERMES_HOME."""
|
|
from pathlib import Path
|
|
|
|
from hermes_cli.config_backups import backup_config, list_config_backups
|
|
|
|
|
|
def test_repeat_backups_dedupe_and_rotate(tmp_path: Path, monkeypatch):
|
|
cfg = tmp_path / "config.yaml"
|
|
cfg.write_text("model: a\n")
|
|
stamps = iter(f"2026010100000{i}" for i in range(10))
|
|
monkeypatch.setattr("hermes_cli.config_backups.time.strftime", lambda _fmt: next(stamps))
|
|
|
|
first = backup_config(cfg, "pre-setup", keep=2)
|
|
assert first is not None and first.parent == tmp_path / "backups" / "config"
|
|
# Same bytes again → no new file (the hermes-setup-three-times case).
|
|
assert backup_config(cfg, "pre-setup", keep=2) is None
|
|
for i in range(3):
|
|
cfg.write_text(f"model: {i}\n")
|
|
backup_config(cfg, "pre-setup", keep=2)
|
|
kept = list_config_backups(cfg, "pre-setup")
|
|
assert len(kept) == 2 and kept[0].read_text() == "model: 2\n"
|
|
# Nothing left beside config.yaml in the home root.
|
|
assert [p.name for p in tmp_path.iterdir() if p.is_file()] == ["config.yaml"]
|
|
|
|
|
|
def test_legacy_siblings_move_but_user_named_copies_stay(tmp_path: Path):
|
|
cfg = tmp_path / "config.yaml"
|
|
cfg.write_text("model: a\n")
|
|
for name in ("config.yaml.bak.1778718391", "config.yaml.corrupt.20260729-093706.bak",
|
|
"config.yaml.bak-pre-migrate-xai-20260515-120000"):
|
|
(tmp_path / name).write_text("old")
|
|
(tmp_path / "config.yaml.bak-my-note").write_text("mine")
|
|
|
|
backup_config(cfg, "corrupt")
|
|
|
|
root = tmp_path / "backups" / "config"
|
|
assert (root / "config.yaml.bak.1778718391").exists()
|
|
assert (root / "config.yaml.corrupt.20260729-093706.bak").exists()
|
|
assert (tmp_path / "config.yaml.bak-my-note").read_text() == "mine"
|
|
assert not list(tmp_path.glob("config.yaml.bak.*")) and not list(tmp_path.glob("config.yaml.corrupt.*"))
|