refactor(update): the purge scan has no fallback; trim to two invariants

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.
This commit is contained in:
kshitijk4poor
2026-09-15 21:31:35 +05:30
committed by kshitij
parent 29758a4eb2
commit 9395f2f0d4
3 changed files with 9 additions and 57 deletions
+1 -1
View File
@@ -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,
+8 -17
View File
@@ -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
@@ -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)