Files
hermes-agent/tests/hermes_cli/test_update_lock_protocol.py
Teknium c36819f063 fix: the F2 replacement-claim regressions run under a psutil-less live-system guard
Both SIGSTOP/SIGCONT reader regressions resume their own stopped reader child with os.kill.
The live-system guard proves a pid is the test's descendant through psutil; in a test
interpreter without psutil it cannot, and it blocked the SIGCONT before the assertion (the
reviewer's F2 re-run hit exactly this). Reproduced by hiding psutil on PYTHONPATH: both tests
raised the guard's RuntimeError on os.kill(<reader>, 18); with the marker both pass. They signal
only fixture-owned children, so they carry live_system_guard_bypass like the custody tests.
2026-10-06 04:36:02 -07:00

610 lines
25 KiB
Python

"""Marker mutation protocol (A7) and checkout custody (R2/R11) with real processes and files.
Each test reproduces a review finding on the pre-A7 head: a stale reader deleting a claim
published after its read (R3), a hand-off owner's killed claim surviving its orphaned delegate
(A7 rule 5, the native Windows ``orphaned_update`` artifact), git / the Node build writing a
checkout a contender already owns (R2), a completion from another home entering its build
while the checkout is owned (R2), and a live pre-root-marker profile claim going unseen (R11).
"""
from __future__ import annotations
import os
import signal
import subprocess
import sys
import textwrap
import time
from pathlib import Path
import pytest
from hermes_cli.update_lock import UpdateLock, marker_mutex_path, process_create_time
REPO_ROOT = Path(__file__).resolve().parents[2]
posix_only = pytest.mark.skipif(sys.platform == "win32", reason="POSIX signals / fd inheritance")
def _script(tmp_path: Path, code: str) -> Path:
path = tmp_path / f"child_{abs(hash(code))}.py"
path.write_text(textwrap.dedent(code), encoding="utf-8")
return path
def _python(tmp_path: Path, code: str, *args, **kwargs) -> subprocess.Popen:
return subprocess.Popen([sys.executable, str(_script(tmp_path, code)), str(REPO_ROOT), *map(str, args)],
stdin=subprocess.DEVNULL, **kwargs)
def _wait_for(path: Path, proc: subprocess.Popen | None = None, timeout: float = 30.0) -> str:
end = time.monotonic() + timeout
while not path.exists():
assert proc is None or proc.poll() is None, f"child exited {proc.returncode} before {path.name}"
assert time.monotonic() < end, f"timed out waiting for {path}"
time.sleep(0.02)
return path.read_text(encoding="utf-8")
def _alive(pid: int) -> bool:
from hermes_cli.update_lock import _pid_alive
return _pid_alive(pid)
_STALE_READER = """
import linecache, os, signal, sys
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli import update_lock
src = update_lock.__file__
def trace(frame, event, arg):
if event == "line" and frame.f_code.co_filename == src \\
and ".unlink()" in linecache.getline(src, frame.f_lineno):
sys.settrace(None)
os.kill(os.getpid(), signal.SIGSTOP) # a scheduling pause, nothing replaced
return trace
sys.settrace(trace)
update_lock.read_live_update(path=Path(sys.argv[2]))
"""
_CLAIMANT = """
import sys, time
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli.update_lock import UpdateLock
assert UpdateLock(path=Path(sys.argv[2])).acquire()
Path(sys.argv[3]).touch()
time.sleep(60)
"""
@posix_only
@pytest.mark.live_system_guard_bypass # SIGCONTs only its own reader child; without psutil the guard cannot prove that
def test_a_stale_reclaim_never_deletes_a_claim_published_after_its_read(tmp_path):
"""R3: the reader judged a dead marker, then paused right at its unlink. A second real
process claims meanwhile. Pre-A7 the resumed reader deleted that live claim; now the
decision and the unlink share one hold of the marker mutex, so the claimant waits."""
marker = tmp_path / ".hermes-update-in-progress"
marker.write_bytes(b"0\n0\n") # malformed: dead
reader = _python(tmp_path, _STALE_READER, marker)
claimant = None
try:
_pid, status = os.waitpid(reader.pid, os.WUNTRACED)
assert os.WIFSTOPPED(status)
claimant = _python(tmp_path, _CLAIMANT, marker, tmp_path / "claimed")
time.sleep(1.0)
os.kill(reader.pid, signal.SIGCONT)
assert reader.wait(timeout=30) == 0
_wait_for(tmp_path / "claimed", claimant)
assert marker.read_text(encoding="utf-8").startswith(f"{claimant.pid}\n"), \
"a stale reader deleted a live claim published after its read"
assert marker_mutex_path(marker).exists(), "the sidecar is never deleted"
finally:
if reader.poll() is None:
os.kill(reader.pid, signal.SIGCONT)
reader.kill()
reader.wait()
if claimant is not None:
claimant.kill()
claimant.wait()
_RECLAIM_PAUSED_READER = """
import os, signal, sys
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli import update_lock
def trace(frame, event, arg):
if event == "call" and frame.f_code.co_name == "_reclaim_dead":
sys.settrace(None)
os.kill(os.getpid(), signal.SIGSTOP) # stale snapshot taken; a scheduling pause, nothing replaced
return None
sys.settrace(trace)
print(update_lock.read_live_update(path=Path(sys.argv[2]), install_root=Path(sys.argv[3])) is not None, flush=True)
"""
_MARKER_ONLY_CLAIMANT = """
import sys, time
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli.update_lock import UpdateLock
assert UpdateLock(path=Path(sys.argv[2]), install_root=Path(sys.argv[3]), checkout_first=False).acquire()
Path(sys.argv[4]).touch()
time.sleep(60)
"""
@posix_only
@pytest.mark.live_system_guard_bypass # SIGCONTs only its own reader child; without psutil the guard cannot prove that
def test_a_reader_reports_the_live_claim_that_replaced_its_stale_snapshot(tmp_path):
"""F2: the reader read a dead marker and paused before the locked recheck; a real marker-only
claimant (no checkout lease, as the launch hand-off does) replaced it meanwhile. The recheck
kept the replacement but the reader still answered "no update" — a skew-retirement vote."""
home, install = tmp_path / "home", tmp_path / "install"
home.mkdir()
install.mkdir()
marker = home / ".hermes-update-in-progress"
_dead_marker(marker)
reader = _python(tmp_path, _RECLAIM_PAUSED_READER, marker, install, stdout=subprocess.PIPE, text=True)
claimant = None
try:
_pid, status = os.waitpid(reader.pid, os.WUNTRACED)
assert os.WIFSTOPPED(status)
claimant = _python(tmp_path, _MARKER_ONLY_CLAIMANT, marker, install, tmp_path / "claimed")
_wait_for(tmp_path / "claimed", claimant)
claim = marker.read_bytes()
os.kill(reader.pid, signal.SIGCONT)
out, _err = reader.communicate(timeout=30)
assert reader.returncode == 0
assert out.strip() == "True", "the reader reported a live replacement claim as no update"
assert marker.read_bytes() == claim, "the live replacement claim was not preserved byte-for-byte"
from hermes_cli.update_lock import checkout_lock_held
# update_in_progress() is this answer OR the checkout lease, which a marker-only claim lacks.
assert checkout_lock_held(install) is False, "the claim is marker-only: no checkout lease answers for it"
finally:
if reader.poll() is None:
os.kill(reader.pid, signal.SIGCONT)
reader.kill()
reader.wait()
if claimant is not None:
claimant.kill()
claimant.wait()
_MUTEX_HOLDER = """
import sys, time
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli.update_lock import marker_mutex
with marker_mutex(Path(sys.argv[2])):
Path(sys.argv[3]).touch()
time.sleep(1.5)
"""
def test_marker_mutations_wait_for_the_mutex_another_process_holds(tmp_path):
"""A7 rule 1 on every OS (POSIX flock / Windows share-none open of ``<marker>.lock``):
while another process holds the marker mutex, a reclaim of even a dead marker waits."""
marker = tmp_path / ".hermes-update-in-progress"
marker.write_bytes(b"0\n0\n")
holder = _python(tmp_path, _MUTEX_HOLDER, marker, tmp_path / "held")
try:
_wait_for(tmp_path / "held", holder)
started = time.monotonic()
lock = UpdateLock(path=marker)
assert lock.acquire() and lock.acquired
assert time.monotonic() - started >= 0.9, "the reclaim ran inside another process's hold"
lock.release()
assert marker_mutex_path(marker).exists(), "the sidecar is never deleted"
finally:
holder.wait(timeout=30)
_ORPHAN_UPDATE = """
import sys, time
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli.update_lock import UpdateLock
base = Path(sys.argv[2])
while not (base / "delegate-written").exists(): time.sleep(0.02)
lock = UpdateLock(path=base / "marker")
assert lock.acquire(), lock.holder
(base / "ready").touch()
while not (base / "go").exists(): time.sleep(0.02)
lock.release()
"""
_HANDOFF_OWNER = """
import os, subprocess, sys, time
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli.update_lock import process_create_time
base = Path(sys.argv[2])
marker = base / "marker"
marker.write_text(f"{os.getpid()}\\n{int(time.time())}\\nct:{process_create_time():.3f}\\n")
child = subprocess.Popen([sys.executable, sys.argv[3], sys.argv[1], str(base)], start_new_session=True)
with open(marker, "a") as fh: # the hand-off scripts name their update child right after spawn
fh.write(f"delegate:{child.pid} ct:{process_create_time(child.pid):.3f}\\n")
(base / "delegate-written").write_text(str(child.pid))
child.wait()
"""
@posix_only
def test_an_orphaned_delegate_removes_its_killed_owners_marker(tmp_path):
"""A7 rule 5 / Windows ``orphaned_update``: the hand-off script claims, names its `hermes
update` child as delegate, and is killed alone. Pre-A7 the child adopted the claim without
writing it, so its exit released nothing: '<dead owner> … delegate:<exited child>' survived
(native artifact crash-12db). The delegate's release now removes a claim whose owner died."""
owner = _python(tmp_path, _HANDOFF_OWNER, tmp_path, _script(tmp_path, _ORPHAN_UPDATE))
try:
child = int(_wait_for(tmp_path / "delegate-written", owner))
_wait_for(tmp_path / "ready", owner)
owner.kill()
owner.wait()
(tmp_path / "go").touch()
end = time.monotonic() + 30
while _alive(child) and time.monotonic() < end:
time.sleep(0.05)
assert not _alive(child)
assert not (tmp_path / "marker").exists(), "the orphaned update's exit left its dead claim behind"
finally:
owner.kill()
owner.wait()
@posix_only
def test_owner_release_hands_the_claim_to_a_live_delegate(tmp_path):
"""A7 rule 5, the other half: the owner's release never deletes the claim a live delegate
still runs under; it rewrites it with that delegate as owner (and keeps the run line)."""
marker = tmp_path / "marker"
delegate = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"], stdin=subprocess.DEVNULL)
try:
lock = UpdateLock(path=marker)
assert lock.acquire() and lock.acquired
ct = f"{process_create_time(delegate.pid):.3f}"
marker.write_bytes(marker.read_bytes() + f"delegate:{delegate.pid} ct:{ct}\nrun:desk-1\n".encode())
started = marker.read_text(encoding="utf-8").splitlines()[1]
lock.release()
assert marker.read_text(encoding="utf-8") == f"{delegate.pid}\n{started}\nct:{ct}\nrun:desk-1\n"
finally:
delegate.kill()
delegate.wait()
_GIT_OWNER = """
import sys
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli.update_lock import UpdateLock
from hermes_cli.update_cmd import _git_run
root = Path(sys.argv[2])
assert UpdateLock(path=root / "owner-marker", install_root=root).acquire()
_git_run(["git"], ["add", "tracked"], cwd=root, check=True)
"""
def _git(root: Path, *args: str) -> str:
env = {**os.environ, "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": os.devnull}
return subprocess.check_output(["git", "-C", str(root), *args], text=True, env=env).strip()
@posix_only
def test_git_keeps_checkout_custody_after_its_updater_is_killed(tmp_path):
"""R2: `_git_run`'s git (and any program it runs) inherits the checkout lock. Pre-fix the
killed updater's lock fell free while its git still wrote the index, and a contender acquired.
A clean filter stalls git: repository hooks never run under the updater (F1)."""
root = tmp_path / "checkout"
root.mkdir()
_git(root, "init", "-q")
_git(root, "config", "user.name", "probe")
_git(root, "config", "user.email", "probe@invalid.local")
(root / "tracked").write_text("before\n")
_git(root, "add", "tracked")
_git(root, "commit", "-qm", "before")
stall = tmp_path / "stall.py"
stall.write_text(f"import os, sys, time\nfrom pathlib import Path\nr = Path({str(tmp_path)!r})\n"
"(r / 'stalled').write_text(str(os.getpid()))\n"
"while not (r / 'go').exists(): time.sleep(0.02)\n"
"sys.stdout.buffer.write(sys.stdin.buffer.read())\n")
_git(root, "config", "filter.stall.clean", f"'{sys.executable}' '{stall}'")
(root / ".git/info/attributes").write_text("tracked filter=stall\n")
(root / "tracked").write_text("after\n")
owner = _python(tmp_path, _GIT_OWNER, root, env={**os.environ, "GIT_CONFIG_NOSYSTEM": "1",
"GIT_CONFIG_GLOBAL": os.devnull})
contender = UpdateLock(path=tmp_path / "contender-marker", install_root=root)
try:
stalled_pid = int(_wait_for(tmp_path / "stalled", owner))
owner.kill()
owner.wait()
assert _alive(stalled_pid)
assert contender.acquire() is False, "a contender owns the checkout while git still writes"
(tmp_path / "go").touch()
end = time.monotonic() + 30
while not contender.acquire():
assert time.monotonic() < end, "the checkout stayed locked after git exited"
time.sleep(0.05)
finally:
(tmp_path / "go").touch()
contender.release()
owner.kill()
owner.wait()
_BUILD_OWNER = """
import os, sys
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli.update_lock import UpdateLock
from hermes_cli.source_build import run_source_script
root = Path(sys.argv[2])
assert UpdateLock(path=root / "owner-marker", install_root=root).acquire()
run_source_script(root, "build.mjs", env={**os.environ, "PATH": sys.argv[3]}, label="probe build")
"""
@posix_only
def test_the_source_build_keeps_checkout_custody_after_its_updater_is_killed(tmp_path):
"""R2: the Node build `run_source_script` starts keeps the lock fd. A real `node` stand-in
(an executable on the build's PATH) outlives the killed updater; the checkout stays locked."""
root = tmp_path / "checkout"
(root / ".git").mkdir(parents=True)
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
node = bin_dir / "node"
node.write_text(f"#!{sys.executable}\nimport os, time\nfrom pathlib import Path\nr = Path({str(tmp_path)!r})\n"
"(r / 'node').write_text(str(os.getpid()))\n"
"while not (r / 'go').exists(): time.sleep(0.02)\n")
node.chmod(0o700)
owner = _python(tmp_path, _BUILD_OWNER, root, bin_dir)
contender = UpdateLock(path=tmp_path / "contender-marker", install_root=root)
try:
node_pid = int(_wait_for(tmp_path / "node", owner))
owner.kill()
owner.wait()
assert _alive(node_pid)
assert contender.acquire() is False, "a contender owns the checkout while the build still writes"
finally:
(tmp_path / "go").touch()
contender.release()
owner.kill()
owner.wait()
_HOLDER = """
import sys, time
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli.update_lock import UpdateLock
assert UpdateLock(path=Path(sys.argv[3]), install_root=sys.argv[2]).acquire()
print("held", flush=True)
time.sleep(120)
"""
def test_a_completion_from_another_home_never_builds_an_owned_checkout(tmp_path, monkeypatch):
"""R2 admission: `complete_source_checkout` ACQUIRES the checkout lock. Pre-fix it claimed
only its own home's marker and entered the build body beside the owning update."""
from hermes_cli import source_completion
root = tmp_path / "checkout"
(root / ".git").mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "other-home"))
entered = []
monkeypatch.setattr(source_completion, "_complete_locked", lambda *a, **k: entered.append(a))
holder = subprocess.Popen([sys.executable, str(_script(tmp_path, _HOLDER)), str(REPO_ROOT), str(root),
str(tmp_path / "owner-marker")],
stdout=subprocess.PIPE, stdin=subprocess.DEVNULL, text=True, encoding="utf-8")
try:
assert holder.stdout is not None and holder.stdout.readline().strip() == "held"
with pytest.raises(RuntimeError, match="an update is still running"):
source_completion.complete_source_checkout(root, desktop=False, assume_yes=True)
assert entered == [], "the completion body ran while another update owned the checkout"
finally:
holder.kill()
holder.wait()
def test_a_live_legacy_profile_claim_is_honored(tmp_path, monkeypatch):
"""R11: before the root marker, an update under a named profile claimed
``<root>/profiles/<p>/.hermes-update-in-progress`` (v1) and never took the checkout lock.
While such a claim is live, a new update from that root refuses."""
root = tmp_path / "root"
profile = root / "profiles" / "work"
profile.mkdir(parents=True)
monkeypatch.setenv("HERMES_HOME", str(profile))
legacy = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(60)"], stdin=subprocess.DEVNULL)
try:
(profile / ".hermes-update-in-progress").write_text(f"{legacy.pid}\n{int(time.time())}\n")
lock = UpdateLock(path=root / ".hermes-update-in-progress")
assert lock.acquire() is False
assert lock.holder is not None and lock.holder.pid == legacy.pid
assert not (root / ".hermes-update-in-progress").exists()
finally:
legacy.kill()
legacy.wait()
# Dead: no longer honored (and left for the old updater that wrote it).
lock = UpdateLock(path=root / ".hermes-update-in-progress")
assert lock.acquire() is True
lock.release()
# --- R6: a dead marker is never reclaimed while the checkout lock is held ---------------------
_LOCK_HOLDER = """
import sys, time
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli import update_lock
# A killed updater's completion child: holds the checkout lock, never wrote the marker.
assert update_lock._acquire_checkout(Path(sys.argv[2])) is None
Path(sys.argv[3]).write_text("held", encoding="utf-8")
time.sleep(120)
"""
def test_dead_marker_is_kept_and_reported_held_while_the_checkout_lock_is_held(tmp_path):
from hermes_cli import update_lock
install = tmp_path / "install"
install.mkdir()
marker = tmp_path / ".hermes-update-in-progress"
dead = subprocess.Popen([sys.executable, "-c", "pass"])
dead.wait()
marker.write_text(f"{dead.pid}\n{int(time.time())}\nct:1.5\n", encoding="utf-8")
holder = _python(tmp_path, _LOCK_HOLDER, install, tmp_path / "ready")
try:
_wait_for(tmp_path / "ready", holder)
verdict = update_lock.read_live_update(path=marker, install_root=install)
assert marker.exists(), "a reader deleted the dead marker while the update tree still held the checkout"
assert verdict is not None and verdict.held, f"expected the hand-off scripts' 'held', got {verdict!r}"
finally:
holder.kill()
holder.wait()
assert update_lock.read_live_update(path=marker, install_root=install) is None
assert not marker.exists(), "with the tree gone the dead marker is reclaimed"
def _dead_marker(marker: Path) -> None:
dead = subprocess.Popen([sys.executable, "-c", "pass"])
dead.wait()
marker.write_text(f"{dead.pid}\n{int(time.time())}\nct:1.5\n", encoding="utf-8")
def test_a_claimer_never_reclaims_a_dead_marker_while_the_checkout_lock_is_held(tmp_path):
"""M1 (round 7): the launch path claims the marker before it takes the checkout lock
(venv_sync). Its claim step deleted the dead marker over a held lock, then dropped its own
claim when the checkout refused: the marker was gone while the killed update's tree ran."""
from hermes_cli import update_lock
install = tmp_path / "install"
install.mkdir()
marker = tmp_path / ".hermes-update-in-progress"
_dead_marker(marker)
dead_bytes = marker.read_bytes()
holder = _python(tmp_path, _LOCK_HOLDER, install, tmp_path / "ready")
try:
_wait_for(tmp_path / "ready", holder)
claim = UpdateLock(path=marker, install_root=install)
assert claim._claim_marker() is False, "claimed over a dead marker while the checkout lock is held"
assert marker.read_bytes() == dead_bytes, "the claim step replaced the held update's marker"
assert claim.holder is not None and claim.holder.held
launch = UpdateLock(path=marker, install_root=install, checkout_first=False) # venv_sync
assert launch.acquire() is False
launch.release()
assert launch.holder is not None and launch.holder.held and launch.holder.pid == 0
assert marker.read_bytes() == dead_bytes
finally:
holder.kill()
holder.wait()
with UpdateLock(path=marker, install_root=install, checkout_first=False) as launch:
assert launch.acquired, "with the tree gone the dead marker is reclaimed and claimed"
assert not marker.exists()
assert update_lock.checkout_lock_held(install) is False
_ORPHANING_HOLDER = """
import subprocess, sys
from pathlib import Path
sys.path.insert(0, sys.argv[1])
from hermes_cli import update_lock
root = Path(sys.argv[2])
assert update_lock._acquire_checkout(root) is None # the record names this (soon dead) updater
release = Path(sys.argv[3]).with_name("release")
wait = "import sys, time, pathlib\\nwhile not pathlib.Path(sys.argv[1]).exists(): time.sleep(0.05)"
child = subprocess.Popen([sys.executable, "-c", wait, str(release)],
pass_fds=update_lock.checkout_lock_fds(root))
Path(sys.argv[3]).write_text(str(child.pid), encoding="utf-8")
"""
@posix_only
def test_a_refusal_over_a_dead_updaters_lock_uses_the_held_wording(tmp_path):
"""m6: `hermes update`, launch repair and source completion refuse through the lock's
holder record. Its updater is dead and a child it started holds the lock: say so, never
name the dead process."""
from hermes_cli import update_lock
install = tmp_path / "install"
install.mkdir()
updater = _python(tmp_path, _ORPHANING_HOLDER, install, tmp_path / "child")
updater.wait(timeout=30)
dead_pid = updater.pid
_wait_for(tmp_path / "child")
try:
lock = UpdateLock(path=tmp_path / ".hermes-update-in-progress", install_root=install)
assert lock.acquire() is False
assert lock.holder is not None and lock.holder.held and lock.holder.pid == 0
text = update_lock.describe_holder(lock.holder)
assert f"process {dead_pid}" not in text
assert "still holds the checkout" in text
finally:
(tmp_path / "release").touch() # the orphaned child is not ours to signal
# --- D17: a "held?" probe never makes a concurrent update fail --------------------------------
_PROBER = """
import fcntl, os, sys, time
from pathlib import Path
path = sys.argv[2]
end = time.monotonic() + float(sys.argv[3])
Path(sys.argv[4]).write_text("go", encoding="utf-8")
n = 0
while time.monotonic() < end:
fd = os.open(path, os.O_RDONLY)
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
time.sleep(0.0001) # held for the instant of one `flock -n` probe
fcntl.flock(fd, fcntl.LOCK_UN)
except OSError:
pass
os.close(fd)
n += 1
time.sleep(0.001) # the next probe's process spawn
print(n)
"""
@posix_only
def test_held_probe_never_fails_a_concurrent_acquire(tmp_path):
from hermes_cli import update_lock
install = tmp_path / "install"
install.mkdir()
lock_path = update_lock.checkout_lock_path(install)
lock_path.touch()
prober = _python(tmp_path, _PROBER, lock_path, 4, tmp_path / "go", stdout=subprocess.PIPE, text=True)
_wait_for(tmp_path / "go", prober)
attempts = refused = 0
try:
while prober.poll() is None:
attempts += 1
if update_lock._acquire_checkout(install) is not None:
refused += 1
else:
update_lock._release_checkout()
finally:
prober.kill()
prober.wait()
assert attempts > 50
assert refused == 0, f"{refused}/{attempts} acquires failed against a probe with no real holder"
# --- oversized numeric marker fields never raise ---------------------------------------------
def test_oversized_marker_numbers_never_raise_and_are_reclaimed(tmp_path):
from hermes_cli import update_lock
install = tmp_path / "install"
install.mkdir()
marker = tmp_path / ".hermes-update-in-progress"
for text in ("1" * 5000 + "\n1\nct:1.5\n", "4242\n" + "9" * 5000 + "\nct:1.5\n",
"4242\n1\nct:1.5\ndelegate:" + "7" * 5000 + " ct:1.5\n"):
marker.write_text(text, encoding="utf-8")
assert update_lock.read_live_update(path=marker, install_root=install) is None
marker.write_text(text, encoding="utf-8")
lock = UpdateLock(path=marker, install_root=install)
assert lock.acquire() is True, lock.holder
lock.release()