fix(settings): accept documented HEADROOM_* env names as settings keys

This commit is contained in:
axelray-dev 2026-08-07 02:47:17 +08:00
parent 7940c05ebf
commit e4c87edf14
3 changed files with 66 additions and 0 deletions

View file

@ -697,6 +697,7 @@ SETTINGS: tuple[SettingField, ...] = (
)
_BY_KEY: dict[str, SettingField] = {f.key: f for f in SETTINGS}
_BY_ENV: dict[str, SettingField] = {f.env: f for f in SETTINGS}
class SettingsValidationError(Exception):
@ -714,6 +715,27 @@ class SettingsValidationError(Exception):
)
def _normalize_values(values: dict[str, Any]) -> dict[str, Any]:
"""Rewrite known env aliases to their JSON/API keys."""
normalized: dict[str, Any] = {}
source_keys: dict[str, str] = {}
conflicts: dict[str, str] = {}
for incoming_key, value in values.items():
field = _BY_ENV.get(incoming_key)
key = field.key if field is not None else incoming_key
if key in normalized:
if normalized[key] != value:
conflicts[key] = (
f"conflicting values supplied for {source_keys[key]!r} and {incoming_key!r}"
)
continue
normalized[key] = value
source_keys[key] = incoming_key
if conflicts:
raise SettingsValidationError([], conflicts)
return normalized
def _coerce(field: SettingField, value: Any) -> Any:
"""Coerce a raw JSON/env value to the field's Python type.
@ -792,6 +814,7 @@ def validate(values: dict[str, Any]) -> dict[str, Any]:
Raises :class:`SettingsValidationError` when any key is unknown or any value
fails coercion. Returns the coerced dict (``None`` values dropped) on success.
"""
values = _normalize_values(values)
unknown = [key for key in values if key not in _BY_KEY]
field_errors: dict[str, str] = {}
coerced: dict[str, Any] = {}
@ -879,6 +902,7 @@ def save(values: dict[str, Any]) -> None:
secret's display value verbatim when the user hasn't touched it; anything
else is validated/coerced and stored.
"""
values = _normalize_values(values)
clear_keys = {key for key, value in values.items() if value is None and key in _BY_KEY}
retained_keys = {
key

View file

@ -63,6 +63,37 @@ class TestRoundTrip:
class TestValidation:
def test_validate_accepts_env_aliases(self, workspace):
assert settings_store.validate({"HEADROOM_LOSSLESS": True, "HEADROOM_RPM": "30"}) == {
"lossless": True,
"rpm": 30,
}
def test_save_accepts_env_alias_and_persists_short_key(self, workspace):
settings_store.save({"HEADROOM_LOSSLESS": True})
assert settings_store.load() == {"lossless": True}
def test_env_alias_clear_removes_short_key(self, workspace):
settings_store.save({"lossless": True})
settings_store.save({"HEADROOM_LOSSLESS": None})
assert settings_store.load() == {}
def test_conflicting_env_alias_and_short_key_rejected(self, workspace):
with pytest.raises(settings_store.SettingsValidationError) as exc:
settings_store.validate({"HEADROOM_LOSSLESS": True, "lossless": False})
assert exc.value.unknown_keys == []
assert "lossless" in exc.value.field_errors
assert "HEADROOM_LOSSLESS" in exc.value.field_errors["lossless"]
def test_same_env_alias_and_short_key_value_is_accepted(self, workspace):
assert settings_store.validate({"HEADROOM_LOSSLESS": True, "lossless": True}) == {
"lossless": True
}
def test_save_rejects_unknown_key(self, workspace):
with pytest.raises(settings_store.SettingsValidationError) as exc:
settings_store.save({"nope": 1})
@ -156,6 +187,7 @@ class TestSecretMasking:
)
monkeypatch.setattr(settings_store, "SETTINGS", registry)
monkeypatch.setattr(settings_store, "_BY_KEY", {f.key: f for f in registry})
monkeypatch.setattr(settings_store, "_BY_ENV", {f.env: f for f in registry})
settings_store.save({"log_file": "/tmp/secret.log"})
stored = settings_store.stored_values()
@ -187,6 +219,15 @@ class TestSecretMasking:
"Saving _MASK should retain the stored value, not overwrite it"
)
def test_anthropic_extra_headers_retain_on_env_alias_mask(self, workspace, monkeypatch):
"""Saving _MASK through the env alias retains the stored value."""
_clear_env(monkeypatch)
settings_store.save({"anthropic_extra_headers": '{"Api-Key": "secret123"}'})
settings_store.save({"ANTHROPIC_TARGET_API_HEADERS": settings_store._MASK})
assert settings_store.load().get("anthropic_extra_headers") == '{"Api-Key": "secret123"}'
def test_anthropic_extra_headers_clear_on_none(self, workspace, monkeypatch):
"""Saving None for anthropic_extra_headers removes it."""
_clear_env(monkeypatch)

View file

@ -207,6 +207,7 @@ class TestSecretMasking:
)
monkeypatch.setattr(settings_store, "SETTINGS", registry)
monkeypatch.setattr(settings_store, "_BY_KEY", {f.key: f for f in registry})
monkeypatch.setattr(settings_store, "_BY_ENV", {f.env: f for f in registry})
settings_store.save({"log_file": "/tmp/secret.log"})
assert client.get("/settings").json()["log_file"] == settings_store._MASK