Files
hermes-agent/tests/scripts/desktop_update/test_handoff_delegate_publication.py
Teknium 4a7c363c0b fix: an old Desktop never reads a dead owner while the POSIX hand-off dies
The previous fix let the line-2 refresher take the marker over only AFTER
the hand-off died: it polls once a second and then waits for the A7 lock,
and until it rewrote line 1 the marker named the dead hand-off. An old
packaged Desktop (judges lines 1 and 2 alone, ignores A7 and the delegate)
reading in that gap deleted the marker and booted its backend while the
`hermes update` the hand-off started, or a completion survivor, still held
the checkout lock -- and the custodian then found no marker to keep.

Now the custodian is named on line 1 BEFORE any update work starts:
marker_refresher_start forks the refresher, then, under A7, rewrites the
claim's line 1 to the refresher's exact pid + creation time and adopts that
identity (MY_PID/MY_CT) for every later marker mutation (delegate, refresh,
release). posix.sh checks its own liveness with $$. The refresher keeps
line 2 young; if the hand-off dies it keeps the marker until the delegate
and the checkout lock are gone, then releases it. The hand-off releases the
marker before it stops the custodian, so line 1 stays live until the
release. If the handover fails (A7 busy), the old takeover-on-death path
still applies.

Test: the orchestrator-killed cell now holds the A7 lock across the SIGKILL
and runs the shipped legacy reader at once (no takeover is possible), then
keeps the post-takeover phases. Red on 5f855a2392c: {'live': None,
'kept': False}. Tests that asserted line 1 == the hand-off pid now expect
the custodian (that assertion encoded the vulnerable state).
2026-10-06 05:44:21 -07:00

76 lines
3.5 KiB
Python

"""Exercise the real handoff's publication barrier with owned inert processes."""
from __future__ import annotations
import os
from pathlib import Path
import shlex
import signal
import subprocess
import time
import pytest
fcntl = pytest.importorskip("fcntl")
pytestmark = pytest.mark.platforms("linux")
from tests.scripts.desktop_update.test_desktop_update_posix_handoff_protocol import _env
from tests.scripts.desktop_update.test_desktop_update_posix_marker import POSIX, _calls, _custodian, _install
@pytest.mark.parametrize("claim", ["missing", "foreign"])
def test_delegate_publication_refuses_without_our_claim(tmp_path, claim):
marker = tmp_path / "marker"
child = subprocess.Popen(["sleep", "60"])
try:
if claim == "foreign":
marker.write_text(f"{child.pid}\n{int(time.time())}\n")
original = marker.read_bytes() if marker.exists() else None
prelude = (f"log() {{ :; }}; MARKER={shlex.quote(str(marker))}; "
"DESKTOP_PID=0 HANDOFF_RUN='' MARKER_CLAIMED=1; "
f". {shlex.quote(str(POSIX.with_name('marker.sh')))}; "
f"marker_add_delegate {child.pid}")
result = subprocess.run(["bash", "-c", prelude], cwd=tmp_path, timeout=20)
assert result.returncode != 0, "a missing/foreign claim is not a published delegate"
assert (marker.read_bytes() if marker.exists() else None) == original
finally:
child.terminate()
child.wait(timeout=10)
def test_sidecar_timeout_never_releases_the_parked_update(tmp_path):
# The installer fixture is an inert executable witness, not a real Hermes
# update. Production bash, locks, child creation and barrier all run unchanged.
home, install = _install(tmp_path)
marker = home / ".hermes-update-in-progress"
log = home / "logs" / "desktop-update-handoff.log"
script = subprocess.Popen(
["bash", str(POSIX), "--daemonized", "--no-ui", "--install-root", str(install)],
env=_env(tmp_path, home), cwd=tmp_path, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, start_new_session=True)
lock_fd = None
try:
deadline = time.monotonic() + 30
while not (marker.exists() and _custodian(home) and marker.read_text().splitlines()[0] == _custodian(home)):
assert script.poll() is None and time.monotonic() < deadline
time.sleep(0.005)
lock_fd = os.open(str(marker) + ".lock", os.O_CREAT | os.O_RDWR, 0o600)
fcntl.flock(lock_fd, fcntl.LOCK_EX)
while "update marker lock is busy" not in (log.read_text() if log.exists() else ""):
assert script.poll() is None and time.monotonic() < deadline
time.sleep(0.01)
# Lock timeout is the actual production 10-second deadline, not a
# shortened/mocked predicate. Let the caller process the refused write.
deadline = time.monotonic() + 5
while script.poll() is None and "hermes update exit code:" not in log.read_text():
assert time.monotonic() < deadline
time.sleep(0.02)
assert not any(call.startswith("update --yes") for call in _calls(tmp_path)), _calls(tmp_path)
assert "hermes update exit code: 3" in log.read_text()
assert "delegate:" not in marker.read_text()
finally:
if lock_fd is not None:
os.close(lock_fd)
if script.poll() is None:
os.killpg(script.pid, signal.SIGKILL) # windows-footgun: ok — Linux process-group fixture
script.wait(timeout=10)