From 91a3ab4c854cb09e570f2f26bc2b4a3f3e5794ca Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Tue, 15 Sep 2026 06:47:09 +0800 Subject: [PATCH] fix(update): reload the cached tools_config before post-pull migrations The updater process is the pre-pull process: hermes_cli.tools_config cached in sys.modules lacks symbols the pull added, so a migration that imports them at call time fails with ImportError and the config silently stays at the old version (#111271). --- hermes_cli/update_cmd_config.py | 8 ++- .../test_update_config_reload_tools_config.py | 55 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 tests/hermes_cli/test_update_config_reload_tools_config.py diff --git a/hermes_cli/update_cmd_config.py b/hermes_cli/update_cmd_config.py index 53cc4b73d2..ad8400ddec 100644 --- a/hermes_cli/update_cmd_config.py +++ b/hermes_cli/update_cmd_config.py @@ -16,12 +16,14 @@ def _reload_config_modules() -> None: """Force-reload config modules after git pull: the updater is the PRE-pull process, so the cached modules hold OLD code and ``check_config_version()`` would report "up to date" despite a pulled migration. ``_subprocess_compat`` / ``dashboard_procs`` reload too so the later dashboard - cleanup sees symbols the update added.""" + cleanup sees symbols the update added. ``tools_config`` reloads as well: migrations import its + helpers at call time (``_migrate_to_45`` needs ``_configurable_keys``), and a pre-pull cache + lacks symbols the pull added (#111271).""" import importlib importlib.invalidate_caches() for mod_name in ( - "hermes_cli.config_defaults", "hermes_cli.config", "hermes_cli.config_migrations", - "hermes_cli._subprocess_compat", "hermes_cli.dashboard_procs"): + "hermes_cli.config_defaults", "hermes_cli.config", "hermes_cli.tools_config", + "hermes_cli.config_migrations", "hermes_cli._subprocess_compat", "hermes_cli.dashboard_procs"): mod = sys.modules.get(mod_name) if mod is not None: try: diff --git a/tests/hermes_cli/test_update_config_reload_tools_config.py b/tests/hermes_cli/test_update_config_reload_tools_config.py new file mode 100644 index 0000000000..f5f6826889 --- /dev/null +++ b/tests/hermes_cli/test_update_config_reload_tools_config.py @@ -0,0 +1,55 @@ +"""Regression tests for the ``tools_config`` leg of the post-update config reload. + +``hermes update`` runs config migrations in the updater process — the PRE-pull +process — after refreshing the config modules from the pulled tree. Migrations +import ``hermes_cli.tools_config`` helpers at call time, so a ``tools_config`` +cached in ``sys.modules`` from before the pull makes the import fail with +``ImportError`` even though the symbol exists in the freshly-written file. + +History: +- #111271: the v45 migration (``_migrate_to_45``) imports ``_configurable_keys`` + from ``hermes_cli.tools_config``; on a v0.20.6 → v0.21.3 update the cached + pre-pull module lacked that newly-added symbol, the migration aborted with + "cannot import name '_configurable_keys'", and the config silently stayed at + the old version while the code and gateway moved on (update exit 1). +""" + +from __future__ import annotations + +import importlib +import sys + + +def test_reload_config_modules_restores_missing_tools_config_symbol(): + """A pre-pull ``tools_config`` cache must be reloaded before migrations run. + + Simulates the stale updater process by deleting the migration-imported + symbol from the already-loaded module, then asserts ``_reload_config_modules`` + re-executes the on-disk code and restores it. + """ + tools_config = sys.modules.get("hermes_cli.tools_config") + if tools_config is None: + import hermes_cli.tools_config as tools_config # noqa: F811 + assert hasattr(tools_config, "_configurable_keys") + + del tools_config._configurable_keys + try: + from hermes_cli.update_cmd_config import _reload_config_modules + _reload_config_modules() + assert hasattr(sys.modules["hermes_cli.tools_config"], "_configurable_keys") + finally: + reloaded = sys.modules["hermes_cli.tools_config"] + if not hasattr(reloaded, "_configurable_keys"): + importlib.reload(reloaded) # keep other tests on a clean module + + +def test_reload_config_modules_is_noop_without_cached_tools_config(): + """The reload must stay safe when ``tools_config`` was never imported.""" + from hermes_cli.update_cmd_config import _reload_config_modules + was_loaded = sys.modules.pop("hermes_cli.tools_config", None) + try: + _reload_config_modules() + finally: + if was_loaded is not None: + sys.modules["hermes_cli.tools_config"] = was_loaded + # No assertion on the module here: the point is that the helper did not raise.