Something went wrong. Try again.
The agent that grows with you
Something went wrong. Try again.
3.9 kB · 98 lines
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899"""Tests for save_config_value() in cli.py — atomic write behavior."""
import osimport yamlfrom pathlib import Pathfrom unittest.mock import patch, MagicMock
import pytest
class TestSaveConfigValueAtomic: """save_config_value() must use atomic_yaml_write to avoid data loss."""
@pytest.fixture def config_env(self, tmp_path, monkeypatch): """Isolated config environment with a writable config.yaml.""" hermes_home = tmp_path / ".hermes" hermes_home.mkdir() config_path = hermes_home / "config.yaml" config_path.write_text(yaml.dump({ "model": {"default": "test-model", "provider": "openrouter"}, "display": {"skin": "default"}, })) monkeypatch.setattr("cli._hermes_home", hermes_home) return config_path
def test_calls_atomic_yaml_write(self, config_env, monkeypatch): """save_config_value must route through atomic_yaml_write, not bare open().""" mock_atomic = MagicMock() monkeypatch.setattr("utils.atomic_yaml_write", mock_atomic)
from cli import save_config_value save_config_value("display.skin", "mono")
mock_atomic.assert_called_once() written_path, written_data = mock_atomic.call_args[0] assert Path(written_path) == config_env assert written_data["display"]["skin"] == "mono"
def test_preserves_existing_keys(self, config_env): """Writing a new key must not clobber existing config entries.""" from cli import save_config_value save_config_value("agent.max_turns", 50)
result = yaml.safe_load(config_env.read_text()) assert result["model"]["default"] == "test-model" assert result["model"]["provider"] == "openrouter" assert result["display"]["skin"] == "default" assert result["agent"]["max_turns"] == 50
def test_creates_nested_keys(self, config_env): """Dot-separated paths create intermediate dicts as needed.""" from cli import save_config_value save_config_value("auxiliary.compression.model", "google/gemini-3-flash-preview")
result = yaml.safe_load(config_env.read_text()) assert result["auxiliary"]["compression"]["model"] == "google/gemini-3-flash-preview"
def test_overwrites_existing_value(self, config_env): """Updating an existing key replaces the value.""" from cli import save_config_value save_config_value("display.skin", "ares")
result = yaml.safe_load(config_env.read_text()) assert result["display"]["skin"] == "ares"
def test_preserves_env_ref_templates_in_unrelated_fields(self, config_env): """The /model --global persistence path must not inline env-backed secrets.""" config_env.write_text(yaml.dump({ "custom_providers": [{ "name": "tuzi", "api_key": "${TU_ZI_API_KEY}", "model": "claude-opus-4-6", }], "model": {"default": "test-model", "provider": "openrouter"}, }))
from cli import save_config_value save_config_value("model.default", "doubao-pro")
result = yaml.safe_load(config_env.read_text()) assert result["model"]["default"] == "doubao-pro" assert result["custom_providers"][0]["api_key"] == "${TU_ZI_API_KEY}"
def test_file_not_truncated_on_error(self, config_env, monkeypatch): """If atomic_yaml_write raises, the original file is untouched.""" original_content = config_env.read_text()
def exploding_write(*args, **kwargs): raise OSError("disk full")
monkeypatch.setattr("utils.atomic_yaml_write", exploding_write)
from cli import save_config_value result = save_config_value("display.skin", "broken")
assert result is False assert config_env.read_text() == original_content