From 9395f2f0d44cab2d0324b203e4acb4128ac948dc Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Tue, 15 Sep 2026 21:31:35 +0530 Subject: [PATCH] refactor(update): the purge scan has no fallback; trim to two invariants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The checkout root is where the update just pulled into, so "root unreadable" cannot happen after a successful pull — drop the OSError fallback and the static five-name tuple it fell back to (the tuple was the drift that caused the bug). Drop the phantom `hermes_cli.hermes_logging` protection entry: no such module exists; the real `hermes_logging` is root-level and now protected by name. Keep two tests: the stale root `utils` scenario (red on base) and the hermes_logging protection. --- hermes_cli/update_cmd.py | 2 +- hermes_cli/update_cmd_maint.py | 25 ++++-------- .../test_update_stale_module_purge.py | 39 ------------------- 3 files changed, 9 insertions(+), 57 deletions(-) diff --git a/hermes_cli/update_cmd.py b/hermes_cli/update_cmd.py index 1d2ca471e3..4d56344dca 100644 --- a/hermes_cli/update_cmd.py +++ b/hermes_cli/update_cmd.py @@ -98,7 +98,7 @@ from hermes_cli.update_cmd_git import ( # noqa: F401 _sync_with_upstream_if_needed) from hermes_cli.update_cmd_maint import ( # noqa: F401 _PRE_UPDATE_SNAPSHOT_KEEP, _PRE_UPDATE_SNAPSHOT_MAX_FILE_SIZE, - _STALE_PURGE_EXCLUDED_TOP_LEVEL, _STALE_PURGE_PREFIXES, _STALE_PURGE_PROTECTED, + _STALE_PURGE_EXCLUDED_TOP_LEVEL, _STALE_PURGE_PROTECTED, _UPDATE_RUNTIME_RELOAD_MODULES, _clear_stale_sqlite_sidecars, _ensure_acp_launcher, _ensure_fhs_path_guard, _finish_dashboard_update_cleanup, _format_time_ago, _post_update_sqlite_runtime_status, _print_bundled_skills_sync_report, diff --git a/hermes_cli/update_cmd_maint.py b/hermes_cli/update_cmd_maint.py index a5dfe409e3..7fb13cfd9c 100644 --- a/hermes_cli/update_cmd_maint.py +++ b/hermes_cli/update_cmd_maint.py @@ -25,10 +25,6 @@ logger = logging.getLogger("hermes_cli.update_cmd") _UPDATE_RUNTIME_RELOAD_MODULES = "hermes_constants", "tools.environments.local", "tools.lazy_deps" -#: Fallback for the purge's top-level names when the checkout scan below cannot run; the -#: live set comes from ``_stale_purge_prefixes()``. -_STALE_PURGE_PREFIXES = "hermes_cli", "gateway", "tools", "tui_gateway", "agent" - #: Owned by the checkout but never purged: pytest resolves fixtures through the identity of #: its own already-imported test modules, and evicting them mid-session breaks that. _STALE_PURGE_EXCLUDED_TOP_LEVEL = frozenset({"tests"}) @@ -38,9 +34,7 @@ _STALE_PURGE_EXCLUDED_TOP_LEVEL = frozenset({"tests"}) #: ``hermes_logging`` is protected for a different reason: its queue listener, handler list and #: ``_logging_initialized`` flag are module globals, so a fresh copy starts a SECOND #: QueueListener over the same log files while the first one keeps running. -_STALE_PURGE_PROTECTED = frozenset({ - "hermes_cli", "hermes_cli.main", "hermes_cli.hermes_logging", "hermes_logging", -}) +_STALE_PURGE_PROTECTED = frozenset({"hermes_cli", "hermes_cli.main", "hermes_logging"}) #: The updater's own module family (``update_cmd*``, ``update_receipt``, ``update_inventory``, #: ``update_lock``, ...) is protected as a prefix: these hold per-run state — the open receipt @@ -102,18 +96,15 @@ def _stale_purge_prefixes() -> frozenset: Scanned, not listed: a hardcoded tuple stops covering each newly added top-level module without anything failing, and the symbol that breaks the next update is in whichever one - drifted out — ``utils`` gaining ``base_url_origin`` was the field case. + drifted out — ``utils`` gaining ``base_url_origin`` / ``file_signature`` were the field cases. """ from hermes_cli.update_cmd import _m - names = set(_STALE_PURGE_PREFIXES) - try: - for entry in Path(_m().PROJECT_ROOT).iterdir(): - if entry.suffix == ".py" and entry.is_file(): - names.add(entry.stem) - elif (entry / "__init__.py").is_file(): - names.add(entry.name) - except OSError as exc: - logger.debug("Could not scan the checkout for purge prefixes: %s", exc) + names = set() + for entry in Path(_m().PROJECT_ROOT).iterdir(): + if entry.suffix == ".py" and entry.is_file(): + names.add(entry.stem) + elif (entry / "__init__.py").is_file(): + names.add(entry.name) return frozenset(names) - _STALE_PURGE_EXCLUDED_TOP_LEVEL diff --git a/tests/hermes_cli/test_update_stale_module_purge.py b/tests/hermes_cli/test_update_stale_module_purge.py index cbf4acf52c..c14f435e2a 100644 --- a/tests/hermes_cli/test_update_stale_module_purge.py +++ b/tests/hermes_cli/test_update_stale_module_purge.py @@ -174,16 +174,6 @@ def test_purge_keeps_plan_record_class_identity(): assert after is before -def test_purge_prefixes_cover_checkout_top_level_modules(): - # The static tuple listed 5 packages and no top-level module, so `utils`, - # `hermes_constants` and friends stayed cached through every update. - prefixes = update_cmd._stale_purge_prefixes() - for name in ("utils", "hermes_constants", "hermes_bootstrap", "plugins", "providers"): - assert name in prefixes, f"{name} is not covered by the purge" - for name in ("hermes_cli", "gateway", "tools", "tui_gateway", "agent"): - assert name in prefixes - - def test_stale_top_level_utils_scenario_end_to_end(): """The 2026-09-12 field failure: `hermes update` from a pre-`base_url_origin` checkout kept the old top-level `utils` cached, and the restart phase's import of @@ -221,32 +211,3 @@ def test_purge_protects_hermes_logging(): sys.modules.pop("hermes_logging", None) if real is not None: sys.modules["hermes_logging"] = real - - -def test_purge_spares_the_tests_package(): - # pytest resolves fixtures through the identity of already-imported test modules. - sentinel = _fake_module("tests.hermes_cli._purge_probe") - sys.modules["tests.hermes_cli._purge_probe"] = sentinel - try: - cli_main._purge_stale_hermes_modules() - assert sys.modules.get("tests.hermes_cli._purge_probe") is sentinel - finally: - sys.modules.pop("tests.hermes_cli._purge_probe", None) - - -def test_purge_prefixes_follow_project_root(tmp_path, monkeypatch): - # The scan must read main.PROJECT_ROOT, not this module's own __file__: tests and the - # installer both relocate the checkout, and a divergent root silently purges nothing. - (tmp_path / "zzz_probe.py").write_text("", encoding="utf-8") - (tmp_path / "zzz_pkg").mkdir() - (tmp_path / "zzz_pkg" / "__init__.py").write_text("", encoding="utf-8") - monkeypatch.setattr(cli_main, "PROJECT_ROOT", tmp_path) - - prefixes = update_cmd._stale_purge_prefixes() - assert {"zzz_probe", "zzz_pkg"} <= prefixes - assert set(update_cmd._STALE_PURGE_PREFIXES) <= prefixes - - -def test_purge_prefixes_fall_back_when_root_unreadable(tmp_path, monkeypatch): - monkeypatch.setattr(cli_main, "PROJECT_ROOT", tmp_path / "does-not-exist") - assert update_cmd._stale_purge_prefixes() == frozenset(update_cmd._STALE_PURGE_PREFIXES)