fix: normalize managed config overlay before merge in load_config
The shared load-boundary flatten added for dict-valued model.default only
ran on the user/default merge; _load_config_impl then deep-merged the raw
managed overlay without normalizing, so a managed model.default:
{provider, model} still reached status/fallback/runtime readers as a dict.
Normalize the managed overlay (same _normalize_root_model_keys pass, plus
the bare model-string -> model.default promotion used by
managed_scope.apply_managed_overlay) before expanding and merging, so
every overlay is canonical before load_config returns.
Adds load_config() regressions for a nested managed default and a bare
managed model string.
This commit is contained in:
+12
-1
@@ -3586,7 +3586,18 @@ def _load_config_impl(*, want_deepcopy: bool) -> Dict[str, Any]:
|
||||
# keys the managed layer pins — see docs/design/managed-scope.md §4.1.
|
||||
managed_config = managed_scope.load_managed_config()
|
||||
if managed_config:
|
||||
managed_expanded = _expand_env_vars(managed_config)
|
||||
# Normalize the managed overlay through the same canonicalization as
|
||||
# the user config BEFORE merging (parity with
|
||||
# managed_scope.apply_managed_overlay): a dict-valued
|
||||
# ``model.default`` (``{provider: ..., model: ...}``) or a bare
|
||||
# ``model: <string>`` must be flattened to a string ``default``
|
||||
# paired with ``provider`` so the merged result never exposes a
|
||||
# nested dict to status/fallback/runtime readers.
|
||||
managed_normalized = _normalize_root_model_keys(managed_config)
|
||||
if isinstance(managed_normalized.get("model"), str):
|
||||
managed_normalized = dict(managed_normalized)
|
||||
managed_normalized["model"] = {"default": managed_normalized["model"]}
|
||||
managed_expanded = _expand_env_vars(managed_normalized)
|
||||
expanded = _deep_merge(expanded, managed_expanded)
|
||||
_LAST_EXPANDED_CONFIG_BY_PATH[path_key] = copy.deepcopy(expanded)
|
||||
if cache_sig is not None:
|
||||
|
||||
@@ -64,3 +64,40 @@ def test_user_cannot_shadow_managed_literal_via_envref(homes, monkeypatch):
|
||||
_write(home / "config.yaml", "model:\n default: ${EVIL}\n")
|
||||
_write(managed / "config.yaml", "model:\n default: managed/locked\n")
|
||||
assert cfg_get(load_config(), "model", "default") == "managed/locked"
|
||||
|
||||
|
||||
def test_managed_nested_dict_default_flattens_on_load(homes):
|
||||
"""A dict-valued managed ``model.default`` must flatten on load.
|
||||
|
||||
``load_config()`` merges the managed overlay after its single
|
||||
normalization pass, so a managed ``model.default: {provider: ...,
|
||||
model: ...}`` used to reach runtime readers as a raw dict. The overlay
|
||||
is now normalized before merging (parity with
|
||||
``managed_scope.apply_managed_overlay``), so the merged config exposes a
|
||||
string ``default`` paired with the nested ``provider``.
|
||||
"""
|
||||
from hermes_cli.config import load_config, cfg_get
|
||||
|
||||
home, managed = homes
|
||||
_write(home / "config.yaml", "model:\n default: user/model\n")
|
||||
_write(managed / "config.yaml", "model:\n default:\n provider: nous\n model: managed/nested\n")
|
||||
cfg = load_config()
|
||||
assert cfg_get(cfg, "model", "default") == "managed/nested"
|
||||
assert cfg_get(cfg, "model", "provider") == "nous"
|
||||
|
||||
|
||||
def test_managed_bare_string_model_flattens_to_default_on_load(homes):
|
||||
"""A bare ``model: <string>`` in the managed file stays a dict shape.
|
||||
|
||||
Mirrors the existing managed-overlay contract: a bare string model must
|
||||
merge as ``model.default`` so readers that do
|
||||
``cfg["model"]["default"]`` keep working (never a bare string at
|
||||
``cfg["model"]``).
|
||||
"""
|
||||
from hermes_cli.config import load_config, cfg_get
|
||||
|
||||
home, managed = homes
|
||||
_write(home / "config.yaml", "model:\n default: user/model\n")
|
||||
_write(managed / "config.yaml", "model: managed/bare\n")
|
||||
cfg = load_config()
|
||||
assert cfg_get(cfg, "model", "default") == "managed/bare"
|
||||
|
||||
Reference in New Issue
Block a user