From 7c9cdad9fd2c786ca931dc32915d436d0ea6c8d9 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:19:43 -0700 Subject: [PATCH] test(cli): cover Windows self-lock recovery guard + cmd-quote its hint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two tests for the self-lock guard in _recover_from_interrupted_install: one asserting it clears the marker and skips install when hermes.exe is a process ancestor (breaking the #52378/#45542 loop), one asserting it falls through to a normal recovery install when the shim is NOT an ancestor. The guard's manual-recovery hint runs only inside the Windows branch, so quote it for cmd.exe (cd /d, double-quoted paths) — the cross-platform fallback hint at the end of the function is left POSIX-correct. Map Icather in scripts/release.py AUTHOR_MAP for the salvage. --- hermes_cli/main.py | 6 +- scripts/release.py | 1 + .../test_update_interrupted_recovery.py | 84 +++++++++++++++++++ 3 files changed, 88 insertions(+), 3 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index a7b0a97b41f..c97705da11e 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -7027,10 +7027,10 @@ def _recover_from_interrupted_install() -> None: " Restart Hermes from a different terminal, " "then run the manual recovery command below:" ) - print(f" cd {PROJECT_ROOT}") + print(f' cd /d "{PROJECT_ROOT}"') print( - f" {sys.executable} -m pip install " - "-e '.[all]'" + f' "{sys.executable}" -m pip install ' + '-e ".[all]"' ) _clear_update_incomplete_marker() try: diff --git a/scripts/release.py b/scripts/release.py index 2ef65144e1b..200192d6dad 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -168,6 +168,7 @@ AUTHOR_MAP = { "290859878+synapsesx@users.noreply.github.com": "synapsesx", "157689911+itsflownium@users.noreply.github.com": "itsflownium", "dirtyren@users.noreply.github.com": "dirtyren", + "97326386+Icather@users.noreply.github.com": "Icather", # PR #45554 salvage (self-lock guard breaks Windows update-recovery infinite loop; #52378 / #45542) "--email": "andryypaez@gmail.com", "mucio@mucio.net": "francescomucio", "291572938+thestral123@users.noreply.github.com": "thestral123", diff --git a/tests/hermes_cli/test_update_interrupted_recovery.py b/tests/hermes_cli/test_update_interrupted_recovery.py index aed84f71c80..6b4aacc8bb4 100644 --- a/tests/hermes_cli/test_update_interrupted_recovery.py +++ b/tests/hermes_cli/test_update_interrupted_recovery.py @@ -139,6 +139,90 @@ def _stub_install_env(monkeypatch, m, seen): ) +def test_recovery_self_lock_guard_clears_marker_without_install(tmp_path, monkeypatch): + # Windows self-lock: hermes.exe is an ancestor of this Python process, so a + # pip-install would fail trying to replace the running launcher (WinError 32 + # / 拒绝访问). Recovery must short-circuit — clear the marker, skip install, + # break the loop (#45542 / #52378) — instead of retrying forever. + monkeypatch.setattr(m, "PROJECT_ROOT", tmp_path) + (tmp_path / "pyproject.toml").write_text("[project]\nname='x'\n") + m._write_update_incomplete_marker() + + scripts_dir = tmp_path / "venv" / "Scripts" + scripts_dir.mkdir(parents=True) + shim = scripts_dir / "hermes.exe" + shim.write_text("") + + monkeypatch.setattr(m, "_is_windows", lambda: True) + monkeypatch.setattr(m, "_venv_scripts_dir", lambda: scripts_dir) + monkeypatch.setattr(m, "_hermes_exe_shims", lambda d: [shim]) + + class FakeProc: + def __init__(self, exe_path): + self._exe = exe_path + + def exe(self): + return self._exe + + def parents(self): + return [FakeProc(str(shim))] + + monkeypatch.setattr("psutil.Process", lambda: FakeProc(sys_executable_path())) + + seen = {"install": False} + _stub_install_env(monkeypatch, m, seen) + + m._recover_from_interrupted_install() + + assert seen["install"] is False, "self-lock must skip the install" + assert not m._update_marker_path().exists(), "marker cleared to break the loop" + + +def sys_executable_path(): + import sys + + return sys.executable + + +def test_recovery_self_lock_guard_inactive_when_not_ancestor(tmp_path, monkeypatch): + # Windows, but hermes.exe is NOT in the ancestry (launched via `hermes + # dashboard` from a separate cmd, say). The guard must fall through to the + # normal install so a genuinely interrupted install still gets healed. + monkeypatch.setattr(m, "PROJECT_ROOT", tmp_path) + (tmp_path / "pyproject.toml").write_text("[project]\nname='x'\n") + m._write_update_incomplete_marker() + + scripts_dir = tmp_path / "venv" / "Scripts" + scripts_dir.mkdir(parents=True) + shim = scripts_dir / "hermes.exe" + shim.write_text("") + + monkeypatch.setattr(m, "_is_windows", lambda: True) + monkeypatch.setattr(m, "_venv_scripts_dir", lambda: scripts_dir) + monkeypatch.setattr(m, "_hermes_exe_shims", lambda d: [shim]) + + class FakeProc: + def __init__(self, exe_path): + self._exe = exe_path + + def exe(self): + return self._exe + + def parents(self): + # Ancestry is plain pythons / cmd — no hermes.exe shim. + return [FakeProc(str(tmp_path / "cmd.exe"))] + + monkeypatch.setattr("psutil.Process", lambda: FakeProc(sys_executable_path())) + + seen = {"install": False} + _stub_install_env(monkeypatch, m, seen) + + m._recover_from_interrupted_install() + + assert seen["install"] is True, "without self-lock, normal recovery runs" + assert not m._update_marker_path().exists(), "marker cleared on successful install" + + def test_recovery_skips_when_lock_held(tmp_path, monkeypatch): # Another process is mid-recovery (fresh lockfile) — this launch must skip # the install entirely and leave both marker and lock untouched.