fix(moa): preserve legacy preset updates and suppress warnings
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -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"]
|
||||
|
||||
17
tests/hermes_cli/test_moa_preset_config_warnings.py
Normal file
17
tests/hermes_cli/test_moa_preset_config_warnings.py
Normal 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
|
||||
@@ -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}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user