From 2f815f040c2e4036636be3cebedf95ab528d46cd Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Sun, 5 Jul 2026 16:33:01 -0400 Subject: [PATCH] fix(#5619): clear stale cfg overrides on profile switch --- api/config.py | 4 +- tests/test_profile_switch_1200.py | 73 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/api/config.py b/api/config.py index 8afb84297..c68707b05 100644 --- a/api/config.py +++ b/api/config.py @@ -521,7 +521,7 @@ def _refresh_config_cache(config_path: Path | None = None) -> None: Callers must hold _cfg_lock when invoking this helper because it mutates shared state. """ - global _cfg_mtime, _cfg_path, _cfg_fingerprint + global _cfg_mtime, _cfg_path, _cfg_fingerprint, cfg if config_path is None: config_path = _get_config_path() _cfg_cache.clear() @@ -582,6 +582,8 @@ def _refresh_config_cache(config_path: Path | None = None) -> None: logger.debug("Failed to load yaml config from %s", config_path) _apply_config_defaults(_cfg_cache) _cfg_fingerprint = _fingerprint_config(_cfg_cache) + # A real reload retargets the legacy cfg alias to the cache for this path. + cfg = _cfg_cache # Bust the models cache so the next request sees fresh config values. # Only delete the disk cache when config has actually changed -- not on # first-ever load (when _old_cfg_mtime == 0.0, i.e. server start or diff --git a/tests/test_profile_switch_1200.py b/tests/test_profile_switch_1200.py index 31c90b42c..f5bdf5edf 100644 --- a/tests/test_profile_switch_1200.py +++ b/tests/test_profile_switch_1200.py @@ -518,6 +518,79 @@ def test_get_config_reloads_when_request_profile_changes_even_with_in_memory_ove config._cfg_fingerprint = orig_fingerprint +def test_get_config_reloads_when_request_profile_changes_even_with_rebound_cfg_override( + tmp_path, monkeypatch +): + """A rebound cfg override must not survive a request-profile switch.""" + monkeypatch.delenv("HERMES_CONFIG_PATH", raising=False) + import api.config as config + import api.profiles as profiles + + default_home = tmp_path / ".hermes" + work_home = default_home / "profiles" / "work" + work_home.mkdir(parents=True) + default_home.mkdir(exist_ok=True) + (default_home / "config.yaml").write_text( + "model:\n provider: openai-codex\n default: gpt-5.5\n", + encoding="utf-8", + ) + (work_home / "config.yaml").write_text( + "model:\n provider: openrouter\n default: google/gemini-3-flash-preview\n" + "custom_providers:\n llamacpp:\n api_key: work-key\n", + encoding="utf-8", + ) + same_mtime = 1_700_000_000 + os.utime(default_home / "config.yaml", (same_mtime, same_mtime)) + os.utime(work_home / "config.yaml", (same_mtime, same_mtime)) + + monkeypatch.setattr( + config, + "_get_config_path", + lambda: profiles.get_active_hermes_home() / "config.yaml", + ) + + orig_default_home = profiles._DEFAULT_HERMES_HOME + orig_active = profiles._active_profile + orig_cache = dict(config._cfg_cache) + orig_mtime = config._cfg_mtime + orig_path = getattr(config, "_cfg_path", None) + orig_fingerprint = getattr(config, "_cfg_fingerprint", None) + orig_cfg = config.cfg + profiles._tls.profile = None + try: + profiles._DEFAULT_HERMES_HOME = default_home + profiles._active_profile = "default" + config.reload_config() + monkeypatch.setattr( + config, + "cfg", + {"model": {"provider": "stale-override", "default": "old-model"}}, + raising=False, + ) + assert config._cfg_has_in_memory_overrides() is True + assert config.get_config()["model"]["provider"] == "stale-override" + + profiles.set_request_profile("work") + assert config._get_config_path() == work_home / "config.yaml" + result = config.get_config() + assert result["model"]["provider"] == "openrouter" + assert result["model"]["default"] == "google/gemini-3-flash-preview" + assert result["custom_providers"]["llamacpp"]["api_key"] == "work-key" + assert config.cfg is config._cfg_cache + finally: + profiles.clear_request_profile() + profiles._DEFAULT_HERMES_HOME = orig_default_home + profiles._active_profile = orig_active + config._cfg_cache.clear() + config._cfg_cache.update(orig_cache) + config._cfg_mtime = orig_mtime + if hasattr(config, "_cfg_path"): + config._cfg_path = orig_path + if hasattr(config, "_cfg_fingerprint"): + config._cfg_fingerprint = orig_fingerprint + config.cfg = orig_cfg + + def test_chat_start_retags_empty_session_to_request_profile(monkeypatch, tmp_path): """An empty session created under profile A can be sent under profile B after a switch.""" import api.routes as routes