Files
hermes-agent/tests/hermes_cli/test_config_lkg_backup.py
T
teknium1 de2d6a1b93
CI / Detect affected areas (push) Has been cancelled
CI / OSV scan (push) Has been cancelled
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker Build, Test, and Publish / Detect affected areas (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
Nix flake check / Detect affected areas (push) Has been cancelled
Build Skills Index / build-index (push) Has been cancelled
CI / Python tests (push) Has been cancelled
CI / OS-specific tests (push) Has been cancelled
CI / Python lints (push) Has been cancelled
CI / JS & TS checks (push) Has been cancelled
CI / Installer tests (push) Has been cancelled
CI / Rust tests (push) Has been cancelled
CI / Desktop E2E (push) Has been cancelled
CI / Docs Site (push) Has been cancelled
CI / Deny unrelated histories (push) Has been cancelled
CI / Check contributors (push) Has been cancelled
CI / Check uv.lock (push) Has been cancelled
CI / Check no committed infographics (push) Has been cancelled
CI / Profile artifact check (push) Has been cancelled
CI / Check no case-colliding filenames (push) Has been cancelled
CI / package-lock.json diff (push) Has been cancelled
CI / Lint Docker scripts (push) Has been cancelled
CI / Supply-chain scan (push) Has been cancelled
CI / Review label gate (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
CI / CI timing report (push) Has been cancelled
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest-32-core) (push) Has been cancelled
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-latest-32-arm-core) (push) Has been cancelled
Docker Build, Test, and Publish / merge (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
Nix flake check / nix flake check (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
fix(config): a fresh process recovers the last good config.yaml instead of running on defaults
The in-process last-known-good (the codex#31188 port) only protects a
long-running gateway. A CLI restart or `hermes config get` against broken
YAML fell through to DEFAULT_CONFIG and silently dropped every override,
including approvals.deny (#102945).

Every successful parse now leaves a `good` copy in backups/config/ through
the existing bounded, byte-deduped backup_config(); the fallback reads the
newest one, runs it through the normal canonicalize/expand/managed-overlay
pipeline, and says so on stderr. The broken config.yaml is never modified.
The backup is the raw file, so ${VAR} templates stay templates on disk.

Redo of #61796 (which added a config.validated.yaml sibling and
re-validated the whole config on every load) on top of the backups/config/
ruling from bf53ff00a7.
2026-09-12 16:17:04 -07:00

61 lines
2.4 KiB
Python

"""A fresh process recovers the last successfully parsed config.yaml when the file is broken.
The in-process last-known-good (#31188 port) only helps a long-running gateway. A CLI restart or
``hermes config get`` against broken YAML used to run on ``DEFAULT_CONFIG`` — dropping every
override, including ``approvals.deny`` (#102945). Successful loads now leave a ``good`` copy in
``backups/config/`` and the fallback reads it.
"""
import json
import os
import subprocess
import sys
from pathlib import Path
from hermes_cli.config_backups import list_config_backups
REPO = Path(__file__).resolve().parents[2]
GOOD = (
"model:\n default: test/secure\n"
"approvals:\n deny:\n - 'curl*evil*'\n"
"custom_providers:\n - name: p\n base_url: https://x.invalid/v1\n api_key: ${LKG_TOKEN}\n"
)
BROKEN = "approvals:\n deny: [unclosed\n"
def _fresh_load(home: Path) -> tuple[dict, str]:
env = {**os.environ, "HERMES_HOME": str(home), "PYTHONPATH": str(REPO), "LKG_TOKEN": "expanded-secret"}
proc = subprocess.run(
[sys.executable, "-c", "import json; from hermes_cli.config import load_config; print(json.dumps(load_config()))"],
cwd=REPO, env=env, text=True, capture_output=True, check=True, stdin=subprocess.DEVNULL,
)
return json.loads(proc.stdout), proc.stderr
def test_fresh_process_recovers_last_good_config_and_leaves_broken_file_alone(tmp_path):
config_path = tmp_path / "config.yaml"
config_path.write_text(GOOD, encoding="utf-8")
first, _ = _fresh_load(tmp_path)
assert first["approvals"]["deny"] == ["curl*evil*"]
config_path.write_text(BROKEN, encoding="utf-8")
recovered, stderr = _fresh_load(tmp_path)
assert recovered["approvals"]["deny"] == ["curl*evil*"]
assert recovered["model"]["default"] == "test/secure"
assert recovered["custom_providers"][0]["api_key"] == "expanded-secret"
assert "LAST KNOWN GOOD" in stderr
assert config_path.read_text(encoding="utf-8") == BROKEN
def test_good_backup_keeps_env_templates_and_dedupes_repeat_loads(tmp_path):
config_path = tmp_path / "config.yaml"
config_path.write_text(GOOD, encoding="utf-8")
_fresh_load(tmp_path)
_fresh_load(tmp_path)
good = list_config_backups(config_path, "good")
assert len(good) == 1
text = good[0].read_text(encoding="utf-8")
assert "${LKG_TOKEN}" in text and "expanded-secret" not in text