fix(moa): preserve legacy preset updates and suppress warnings

This commit is contained in:
Tharanee
2026-09-23 10:50:39 -04:00
committed by brooklyn!
parent 7e42461d1b
commit 35f398eb5e
4 changed files with 113 additions and 3 deletions

View File

@@ -904,7 +904,11 @@ def get_missing_config_fields() -> List[Dict[str, Any]]:
if key not in current:
missing.append({"key": full_key, "default": default_value,
"description": f"New config option: {full_key}"})
elif isinstance(default_value, dict) and isinstance(current.get(key), dict):
elif (
isinstance(default_value, dict)
and isinstance(current.get(key), dict)
and not (prefix == "moa" and key == "presets")
):
_check(default_value, current[key], full_key)
_check(DEFAULT_CONFIG, load_config())

View File

@@ -310,7 +310,8 @@ def set_moa_models(body: MoaConfigPayload, profile: Optional[str] = None):
# desktop's debounced PUT /api/config autosave races it, so the whole
# span holds _CONFIG_MUTATION_LOCK or one of the two saves is dropped.
with config_write_scope(body.profile or profile):
if body.presets:
legacy_flat_update = not body.presets
if not legacy_flat_update:
raw = {
"default_preset": body.default_preset,
"active_preset": body.active_preset,
@@ -339,9 +340,28 @@ def set_moa_models(body: MoaConfigPayload, profile: Optional[str] = None):
# MoaConfigPayload; an editor round-trip must not reset it.
raw["privacy_filter"] = existing_moa["privacy_filter"]
normalized = normalize_moa_config(raw)
if legacy_flat_update and isinstance(existing_moa, dict):
existing_presets = existing_moa.get("presets")
existing_default = existing_moa.get("default_preset")
if isinstance(existing_presets, dict):
if isinstance(existing_default, str) and existing_default in existing_presets:
# Older clients update the flat default preset; keep the existing
# default name and any valid active selection from the stored map.
synthesized_default = normalized.get("default_preset")
if (
isinstance(synthesized_default, str)
and synthesized_default in normalized["presets"]
):
normalized["presets"][existing_default] = normalized["presets"].pop(
synthesized_default
)
normalized["default_preset"] = existing_default
existing_active = existing_moa.get("active_preset")
if isinstance(existing_active, str) and existing_active in existing_presets:
normalized["active_preset"] = existing_active
from hermes_cli import config as config_mod
updated = config_mod._merge_partial_save(raw_cfg, {"moa": normalized})
if body.presets:
if not legacy_flat_update:
# ``_merge_partial_save`` retains undeclared metadata on surviving presets;
# filter after that merge so omitted names are authoritative deletions.
merged_presets = updated["moa"]["presets"]

View File

@@ -0,0 +1,17 @@
"""A user's named MoA presets are not schema-defaulted config options."""
from hermes_cli import config as hermes_config
def test_missing_config_fields_ignores_user_named_presets_but_checks_schema_options(monkeypatch):
"""Custom preset names must not be compared against the built-in ``default`` name."""
monkeypatch.setattr(
hermes_config,
"load_config",
lambda: {"moa": {"presets": {"keep_a": {}}}, "terminal": {}},
)
missing_keys = {field["key"] for field in hermes_config.get_missing_config_fields()}
assert not any(key.startswith("moa.presets.") for key in missing_keys)
assert "terminal.backend" in missing_keys

View File

@@ -20,6 +20,7 @@ import subprocess
import sys
from pathlib import Path
import pytest
import yaml
from hermes_cli.web_models import MoaConfigPayload, MoaModelSlot, MoaPresetPayload
@@ -216,3 +217,71 @@ def test_deletion_preserves_privacy_filter_and_retained_preset_metadata(tmp_path
assert on_disk["save_traces"] is True
assert on_disk["trace_dir"] == "/custom/traces"
assert on_disk["presets"]["keep_a"]["operator_note"] == "retain me"
@pytest.mark.parametrize("include_presets", [False, True], ids=["missing-presets", "empty-presets"])
def test_legacy_flat_update_preserves_named_presets_and_valid_default(
tmp_path, monkeypatch, include_presets
):
"""Older flat clients preserve named presets and a valid persisted active selection."""
home = tmp_path / ".hermes"
home.mkdir()
seeded = _three_presets()
seeded["active_preset"] = "keep_b"
_seed(home, monkeypatch, seeded)
payload = {
"active_preset": "",
"reference_models": [MoaModelSlot(provider="openai-codex", model="gpt-5.8")],
"aggregator": MoaModelSlot(provider="anthropic", model="claude-opus-5"),
"enabled": True,
}
if include_presets:
payload["presets"] = {}
set_moa_models(MoaConfigPayload(**payload))
moa = _on_disk(home)["moa"]
assert set(moa["presets"]) == {"keep_a", "doomed", "keep_b"}
assert moa["default_preset"] == "keep_a"
assert moa["active_preset"] == "keep_b"
assert moa["active_preset"] in moa["presets"]
assert "default" not in moa["presets"]
assert moa["presets"]["keep_a"]["reference_models"] == [
{"provider": "openai-codex", "model": "gpt-5.8", "enabled": True}
]
assert moa["presets"]["keep_a"]["aggregator"] == {
"provider": "anthropic", "model": "claude-opus-5"
}
# Legacy flat fields represent the saved default preset. Runtime resolution uses
# default_preset when no preset name is explicitly requested; active_preset is metadata.
from hermes_cli.moa_config import resolve_moa_preset
assert resolve_moa_preset(moa)["reference_models"] == moa["presets"]["keep_a"]["reference_models"]
assert moa["presets"]["keep_b"] == seeded["presets"]["keep_b"]
assert moa["presets"]["doomed"] == seeded["presets"]["doomed"]
def test_legacy_flat_update_with_named_default_and_empty_map_does_not_fail(tmp_path, monkeypatch):
"""A legacy flat payload's default_preset field cannot rename its synthesized map."""
home = tmp_path / ".hermes"
home.mkdir()
_seed(home, monkeypatch, _three_presets())
result = set_moa_models(
MoaConfigPayload(
default_preset="keep_b",
presets={},
reference_models=[MoaModelSlot(provider="openai-codex", model="gpt-5.8")],
aggregator=MoaModelSlot(provider="anthropic", model="claude-opus-5"),
enabled=True,
)
)
moa = _on_disk(home)["moa"]
assert result["ok"] is True
assert set(moa["presets"]) == {"keep_a", "doomed", "keep_b"}
assert moa["default_preset"] == "keep_a"
assert moa["presets"]["keep_a"]["reference_models"] == [
{"provider": "openai-codex", "model": "gpt-5.8", "enabled": True}
]