mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
fix(config): widen empty-section guard to _deep_merge in load_config
Sibling site of the load_cli_config fix (#58277): _deep_merge treated a YAML-null section (terminal: with no value) as an override, replacing the entire DEFAULT_CONFIG dict for that section with None. Every downstream consumer expecting a mapping was a latent crash, and default sub-keys were silently lost. A None override of a dict default is now ignored, matching the CLI loader's behavior. Scalar-null overrides are unchanged.
This commit is contained in:
parent
bdecf0ab94
commit
46613071e4
2 changed files with 36 additions and 0 deletions
|
|
@ -6243,6 +6243,12 @@ def _deep_merge(base: dict, override: dict) -> dict:
|
|||
Keys in *override* take precedence. If both values are dicts the merge
|
||||
recurses, so a user who overrides only ``tts.elevenlabs.voice_id`` will
|
||||
keep the default ``tts.elevenlabs.model_id`` intact.
|
||||
|
||||
An empty section key in config.yaml (``terminal:`` with no value) parses
|
||||
as YAML ``None``; treating that as an override would replace the entire
|
||||
default dict with ``None`` and crash every downstream consumer that
|
||||
expects a mapping (#58277). A ``None`` override of a dict default is
|
||||
ignored — same as the key being absent.
|
||||
"""
|
||||
result = base.copy()
|
||||
for key, value in override.items():
|
||||
|
|
@ -6252,6 +6258,8 @@ def _deep_merge(base: dict, override: dict) -> dict:
|
|||
and isinstance(value, dict)
|
||||
):
|
||||
result[key] = _deep_merge(result[key], value)
|
||||
elif key in result and isinstance(result[key], dict) and value is None:
|
||||
continue
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -360,6 +360,34 @@ class TestLoadConfigParseFailure:
|
|||
assert capsys.readouterr().err == ""
|
||||
|
||||
|
||||
class TestEmptyConfigSections:
|
||||
"""Empty section keys (``terminal:`` with no value) parse as YAML None
|
||||
and must not replace the default dict for that section (#58277)."""
|
||||
|
||||
def test_null_section_keeps_defaults_in_load_config(self, tmp_path):
|
||||
with patch.dict(os.environ, {"HERMES_HOME": str(tmp_path)}):
|
||||
(tmp_path / "config.yaml").write_text(
|
||||
"model:\n default: test/custom\n"
|
||||
"terminal:\n"
|
||||
"display:\n"
|
||||
)
|
||||
config = load_config()
|
||||
assert config["model"]["default"] == "test/custom"
|
||||
assert isinstance(config["terminal"], dict)
|
||||
assert config["terminal"] == DEFAULT_CONFIG["terminal"]
|
||||
assert isinstance(config["display"], dict)
|
||||
|
||||
def test_null_override_of_non_dict_default_still_applies(self, tmp_path):
|
||||
"""None only shields dict defaults — explicit null for a scalar
|
||||
key remains an override (unchanged behavior)."""
|
||||
from hermes_cli.config import _deep_merge
|
||||
|
||||
merged = _deep_merge({"scalar": 5, "section": {"a": 1}},
|
||||
{"scalar": None, "section": None})
|
||||
assert merged["scalar"] is None
|
||||
assert merged["section"] == {"a": 1}
|
||||
|
||||
|
||||
class TestSaveAndLoadRoundtrip:
|
||||
@staticmethod
|
||||
def _deny_config_reads(config_path):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue