From d1b7d5689fe179eccb341a34ae3ca523288e322e Mon Sep 17 00:00:00 2001 From: ethernet Date: Mon, 21 Sep 2026 19:32:37 -0400 Subject: [PATCH] fix(update_stage): look the shim marker up in the process home, not ~/.hermes update_stage rebuilt the home from HERMES_HOME with a literal ~/.hermes fallback while update_lock resolves the same marker through get_process_hermes_home(). Where the platform default differs (sudo invoker, data-dir suffix) the two disagreed and the old-shim UI fallback found no marker, leaving the hand-off window frozen. hermes_constants is stdlib-only, so the lazy import keeps this module usable under -I -S -B. --- hermes_cli/update_stage.py | 14 ++++++++++---- tests/hermes_cli/test_update_stage.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/hermes_cli/update_stage.py b/hermes_cli/update_stage.py index 877dfcf238..23d2028917 100644 --- a/hermes_cli/update_stage.py +++ b/hermes_cli/update_stage.py @@ -33,6 +33,14 @@ UI_SPAWNED_ENV = "HERMES_UPDATE_UI_ACTIVE" MARKER_NAME = ".hermes-update-in-progress" +def _process_home() -> Path: + """The marker and the shim's log live in the PROCESS home (update_lock.update_marker_path): + the shim resolved ``$HERMES_HOME`` or the platform default, never a profile override, and + the platform default (sudo invoker, data-dir suffix) is not ``~/.hermes`` everywhere.""" + from hermes_constants import get_process_hermes_home + return get_process_hermes_home() + + def status_file() -> Path | None: """The watching shim's status file, or None when no UI is discoverable.""" exported = os.environ.get(STATUS_FILE_ENV, "").strip() @@ -49,9 +57,8 @@ def _status_from_marker() -> Path | None: shim's status file lives beside it, pid-suffixed. No marker (plain CLI update) or no matching file → no UI. """ - home = os.environ.get("HERMES_HOME") or str(Path.home() / ".hermes") try: - pid = int((Path(home) / MARKER_NAME).read_text(encoding="utf-8-sig") + pid = int((_process_home() / MARKER_NAME).read_text(encoding="utf-8-sig") .splitlines()[0].strip()) except (OSError, ValueError, IndexError): return None @@ -113,8 +120,7 @@ def ensure_panel(update_root: Path) -> None: if not panel.is_file(): return try: - home = os.environ.get("HERMES_HOME") or str(Path.home() / ".hermes") - log = Path(home) / "logs" / "desktop-update-handoff.log" + log = _process_home() / "logs" / "desktop-update-handoff.log" if log.is_file() and _ui_present_in_log(log.read_text(encoding="utf-8-sig", errors="replace")[-8000:]): return import subprocess diff --git a/tests/hermes_cli/test_update_stage.py b/tests/hermes_cli/test_update_stage.py index d26093b88f..842b3e2f1d 100644 --- a/tests/hermes_cli/test_update_stage.py +++ b/tests/hermes_cli/test_update_stage.py @@ -67,6 +67,24 @@ def test_marker_fallback_covers_the_old_shim(status_file, tmp_path, monkeypatch) "Updating Python dependencies (PM)") +def test_marker_fallback_uses_the_platform_home_without_env_var(status_file, tmp_path, monkeypatch): + """The shim without HERMES_HOME resolved the platform default, which is not ~/.hermes + on every host (sudo invoker, data-dir suffix); the marker must be looked up there.""" + import hermes_constants + + monkeypatch.delenv("HERMES_HOME", raising=False) + monkeypatch.setattr(hermes_constants, "_get_platform_default_hermes_home", lambda: tmp_path / "platform") + (tmp_path / "platform").mkdir() + (tmp_path / "platform" / update_stage.MARKER_NAME).write_text( + f"80335\n{int(time.time())}\n", encoding="utf-8") + status_file.write_text('{"status":"running","message":"old"}', encoding="utf-8") + monkeypatch.setenv("TMPDIR", str(tmp_path)) + + update_stage.publish_stage("Building products") + + _assert_running(status_file.read_text(encoding="utf-8"), "Building products") + + def test_no_ui_sources_is_inert(status_file, tmp_path, monkeypatch): """A plain CLI update has no env var and no marker: publish must no-op.""" monkeypatch.setenv("HERMES_HOME", str(tmp_path)) # no marker inside