125 lines
4.3 KiB
Python
125 lines
4.3 KiB
Python
"""Invariants for ``hermes_cli.config_effective.load_user_config_effective`` — the one loader every
|
|
defaults-free config reader (gateway runtime, TUI gateway, cron, ``hermes send`` bridge, doctor,
|
|
bootstrap modules) goes through."""
|
|
import textwrap
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
|
|
@pytest.fixture
|
|
def homes(tmp_path, monkeypatch):
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
managed = tmp_path / "managed"
|
|
managed.mkdir()
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
monkeypatch.setenv("HERMES_MANAGED_DIR", str(managed))
|
|
monkeypatch.setenv("FIXTURE_USER_KEY", "user-secret")
|
|
monkeypatch.setenv("FIXTURE_MANAGED_URL", "https://managed.example")
|
|
_reset_caches()
|
|
return home, managed
|
|
|
|
|
|
def _reset_caches():
|
|
import hermes_cli.config as cfg
|
|
from hermes_cli import config_effective, managed_scope
|
|
|
|
cfg._LOAD_CONFIG_CACHE.clear()
|
|
cfg._RAW_CONFIG_CACHE.clear()
|
|
config_effective._EFFECTIVE_CACHE.clear()
|
|
config_effective._LAST_GOOD_USER_RAW.clear()
|
|
managed_scope.invalidate_managed_cache()
|
|
|
|
|
|
def _write(path, body):
|
|
path.write_text(textwrap.dedent(body), encoding="utf-8")
|
|
_reset_caches()
|
|
|
|
|
|
USER_YAML = """
|
|
model:
|
|
name: user/model
|
|
api_key: ${FIXTURE_USER_KEY}
|
|
provider: custom
|
|
display:
|
|
skin: user-skin
|
|
"""
|
|
MANAGED_YAML = """
|
|
model:
|
|
base_url: ${FIXTURE_MANAGED_URL}
|
|
display:
|
|
skin: managed-skin
|
|
"""
|
|
|
|
|
|
def test_effective_is_user_plus_managed_plus_env_with_no_defaults(homes):
|
|
"""Contract as a fixture: given user config.yaml X, managed overlay Y and env Z, the effective
|
|
dict is exactly this literal — ``${VAR}`` expanded on both layers, managed keys winning,
|
|
root ``provider`` migrated under ``model``, and no DEFAULT_CONFIG key introduced (a missing
|
|
key stays missing). Per-message gateway reads (and the system prompt built from them) are
|
|
pinned by this shape, not by re-running the implementation's primitives."""
|
|
from hermes_cli.config import DEFAULT_CONFIG
|
|
from hermes_cli.config_effective import load_user_config_effective
|
|
|
|
home, managed = homes
|
|
_write(home / "config.yaml", USER_YAML)
|
|
_write(managed / "config.yaml", MANAGED_YAML)
|
|
|
|
effective = load_user_config_effective(home / "config.yaml")
|
|
|
|
assert effective == {
|
|
"model": {
|
|
"default": "user/model",
|
|
"provider": "custom",
|
|
"api_key": "user-secret",
|
|
"base_url": "https://managed.example",
|
|
},
|
|
"display": {"skin": "managed-skin"},
|
|
}
|
|
assert "agent" in DEFAULT_CONFIG # would be present if defaults had been merged
|
|
|
|
|
|
def test_broken_yaml_serves_last_good_and_fail_closed_raises(homes):
|
|
"""A torn mid-edit write must not silently drop user overrides: the fail-open path serves the last
|
|
successfully parsed user file through the same pipeline; ``fail_closed`` surfaces the error to
|
|
callers that keep their own last-good state."""
|
|
from hermes_cli.config_effective import load_user_config_effective
|
|
|
|
home, _ = homes
|
|
_write(home / "config.yaml", USER_YAML)
|
|
good = load_user_config_effective(home / "config.yaml")
|
|
|
|
(home / "config.yaml").write_text("model: [unterminated", encoding="utf-8")
|
|
_reset_caches_keep_last_good()
|
|
|
|
assert load_user_config_effective(home / "config.yaml") == good
|
|
with pytest.raises(yaml.YAMLError): # the type _refresh_fallback_model's own last-good path keys on
|
|
load_user_config_effective(home / "config.yaml", fail_closed=True)
|
|
|
|
|
|
def test_good_backup_is_written_only_for_the_active_home(homes, tmp_path):
|
|
"""Reading ANOTHER profile's config (doctor, TUI cwd lookup) is a read: it must not create
|
|
``backups/config/`` inside that profile. The active home keeps the last-good copy."""
|
|
from hermes_cli.config_effective import load_user_config_effective
|
|
|
|
home, _ = homes
|
|
other = tmp_path / "other-profile"
|
|
other.mkdir()
|
|
_write(home / "config.yaml", USER_YAML)
|
|
_write(other / "config.yaml", USER_YAML)
|
|
|
|
load_user_config_effective(other / "config.yaml")
|
|
load_user_config_effective(home / "config.yaml")
|
|
|
|
assert not (other / "backups").exists()
|
|
assert list((home / "backups" / "config").glob("config.yaml.good.*"))
|
|
|
|
|
|
def _reset_caches_keep_last_good():
|
|
import hermes_cli.config as cfg
|
|
from hermes_cli import config_effective
|
|
|
|
cfg._RAW_CONFIG_CACHE.clear()
|
|
config_effective._EFFECTIVE_CACHE.clear()
|