mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
test(cli): cover Windows self-lock recovery guard + cmd-quote its hint
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.
This commit is contained in:
parent
b6f592dbdc
commit
7c9cdad9fd
3 changed files with 88 additions and 3 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue