fix(#5619): reload config cache on profile switch

This commit is contained in:
Rod Boev
2026-07-05 16:22:35 -04:00
parent 9b18ea9f99
commit 4c3a4e011e
3 changed files with 89 additions and 12 deletions
+6 -4
View File
@@ -450,8 +450,9 @@ def reload_config_if_stale() -> None:
current_mtime = config_path.stat().st_mtime
except OSError:
current_mtime = 0.0
cache_stale = current_mtime != _cfg_mtime or _cfg_path != config_path
if not _cfg_cache or (cache_stale and not _cfg_has_in_memory_overrides()):
path_changed = _cfg_path != config_path
mtime_stale = current_mtime != _cfg_mtime
if not _cfg_cache or path_changed or (mtime_stale and not _cfg_has_in_memory_overrides()):
_refresh_config_cache(config_path)
@@ -462,8 +463,9 @@ def get_config() -> dict:
current_mtime = config_path.stat().st_mtime
except OSError:
current_mtime = 0.0
cache_stale = current_mtime != _cfg_mtime or _cfg_path != config_path
if not _cfg_cache or (cache_stale and not _cfg_has_in_memory_overrides()):
path_changed = _cfg_path != config_path
mtime_stale = current_mtime != _cfg_mtime
if not _cfg_cache or path_changed or (mtime_stale and not _cfg_has_in_memory_overrides()):
reload_config_if_stale()
# When a test (or runtime caller) has rebound ``cfg`` to a different dict
# via monkeypatch.setattr(config, "cfg", ...), return that override rather
+65
View File
@@ -453,6 +453,71 @@ def test_get_config_reloads_when_request_profile_changes(tmp_path, monkeypatch):
config._cfg_fingerprint = orig_fingerprint
def test_get_config_reloads_when_request_profile_changes_even_with_in_memory_override(
tmp_path, monkeypatch
):
"""A pinned override on one profile 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)
profiles._tls.profile = None
try:
profiles._DEFAULT_HERMES_HOME = default_home
profiles._active_profile = "default"
config.reload_config()
config._cfg_cache["__test_override"] = "pinned-default"
assert config._cfg_has_in_memory_overrides() is True
assert config.get_config()["model"]["provider"] == "openai-codex"
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"
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
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
@@ -26,6 +26,8 @@ These tests pin both prongs.
"""
from __future__ import annotations
import os
import api.config as config
@@ -74,22 +76,30 @@ def test_cfg_has_in_memory_overrides_detects_in_place_mutation(monkeypatch):
config._cfg_cache.pop("__test_key", None)
def test_get_config_does_not_reload_when_only_in_memory_override(monkeypatch, tmp_path):
"""A test that sets cfg + leaves disk untouched must not trigger reload."""
def test_get_config_does_not_reload_when_only_in_memory_override_same_path_mtime_only(
monkeypatch, tmp_path
):
"""same_path cfg overrides must survive mtime-only staleness."""
config_path = tmp_path / "config.yaml"
base_mtime = 1_700_000_000.0
config_path.write_text(
"model:\n provider: openrouter\n default: test/model-x\n",
encoding="utf-8",
)
os.utime(config_path, (base_mtime, base_mtime))
monkeypatch.setattr(config, "_get_config_path", lambda: config_path)
config.reload_config()
# Fake a config path that will have a different mtime than what's cached
fake_path = tmp_path / "missing.yaml"
monkeypatch.setattr(config, "_get_config_path", lambda: fake_path)
# Override cfg via attr rebind.
test_override = {
"model": {"provider": "openai", "default": "gpt-test"},
"providers": {},
}
monkeypatch.setattr(config, "cfg", test_override, raising=False)
# The path-aware reload would normally trigger reload (path changed),
# but the override-detection should suppress it.
os.utime(config_path, (base_mtime + 1.0, base_mtime + 1.0))
# The same_path mtime-only reload would normally trigger reload, but the
# override-detection should suppress it.
result = config.get_config()
assert result is test_override
assert result["model"]["provider"] == "openai"