test: a relaunched hand-off update adopting its own claim runs on the macOS lane

The hand-off pid test only checked which pid the first update child was
told, and it was gated to Linux, so the macOS runner (where the refusal
was reported) never ran it.

It now drives the real posix.sh hand-off, has the update start a second
generation detached from it (the relaunch / takeover shape), and runs the
real UpdateLock there: on main that generation refuses with the custodian
as holder; with the fix it adopts. Gated "posix" so the macOS lane runs it
against marker.sh's whole-second creation times.
This commit is contained in:
Teknium
2026-10-10 03:57:30 -07:00
parent 7ef7df7773
commit 66605471e9

View File

@@ -1,42 +1,69 @@
"""The update a POSIX Desktop hand-off spawns is told the marker's live owner as its hand-off pid.
"""A later generation of a POSIX Desktop hand-off's update adopts the hand-off's own claim.
``hermes update`` adopts an existing claim only when the live owner is its own pid, the
``hermes update`` adopts an existing claim only when a live partner is its own pid, the
``HERMES_UPDATE_HANDOFF_PID`` value, or an ancestor (``update_lock.UpdateLock._is_partner``).
``posix.sh`` hands marker line 1 to a custodian before any update work starts, and the custodian is
a sibling of the update child, never an ancestor. On macOS the delegate line cannot rescue it (a
whole-second creation time never matches the child's own), so a hand-off pid naming the script
instead of the custodian made every Desktop-initiated update refuse its own claim with exit 2
(#133992, #134268, #134309, #134381).
``posix.sh`` hands marker line 1 to a custodian before any update work starts; the custodian is a
sibling of the update, never an ancestor. The first ``hermes update`` is named on the delegate
line, but a later generation (a relaunch or a takeover detached from it) has a new pid, so the
hand-off pid is its only way to recognise the custodian. Naming the script there made those
updates refuse their own claim with exit 2 (#134268, #134602).
Runs on macOS too (the CI macOS lane), where the marker's creation times come from ``ps`` in
whole seconds: that host is where the refusal was reported.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from tests.scripts.desktop_update.test_desktop_update_posix_marker import FAKE_CLI, _custodian, _install, _run
pytestmark = pytest.mark.platforms("linux") # /proc creation times, like the rest of the posix marker suite
pytestmark = pytest.mark.platforms("posix")
RECORD_UPDATE_VIEW = """
import os, sys
REPOSITORY = Path(__file__).resolve().parents[3]
# The update child (generation 1) starts generation 2 through an intermediate that exits at once,
# so generation 2 is reparented away from it (no ancestry to lean on), then waits for its verdict.
# Generation 2 runs this repository's real UpdateLock against the marker the real posix.sh wrote.
SECOND_GENERATION = """
import os, sys, json, subprocess, time
from pathlib import Path
if sys.argv[1:2] == ['update']:
marker = Path(os.environ['HERMES_HOME']) / '.hermes-update-in-progress'
owner = marker.read_text(encoding='utf-8-sig').splitlines()[0]
Path(os.environ['HANDOFF_VIEW']).write_text(
f"{os.environ.get('HERMES_UPDATE_HANDOFF_PID', '')} {owner}", encoding='utf-8')
if sys.argv[1:2] == ['update'] and '--help' not in sys.argv: # not the --keep-stash probe
out = Path(os.environ['GEN2_OUT'])
probe = (
"import json, os, sys; from pathlib import Path\\n"
"from hermes_cli.update_lock import UpdateLock, update_marker_path\\n"
"lock = UpdateLock(path=update_marker_path())\\n"
"ok = lock.acquire()\\n"
"Path(sys.argv[1]).write_text(json.dumps({'adopted': ok, 'pid': os.getpid(), "
"'holder': lock.holder.pid if lock.holder else None, "
"'told': os.environ.get('HERMES_UPDATE_HANDOFF_PID')}), encoding='utf-8')\\n"
"ok and lock.release()\\n")
env = {k: v for k, v in os.environ.items() if not k.startswith('PYTHON')}
env['PYTHONPATH'] = os.environ['GEN2_REPO']
subprocess.run([sys.executable, '-c',
'import subprocess, sys; subprocess.Popen(sys.argv[1:], start_new_session=True)',
sys.executable, '-c', probe, str(out)],
cwd=os.environ['GEN2_REPO'], env=env, check=True)
deadline = time.monotonic() + 60
while not out.exists() and time.monotonic() < deadline:
time.sleep(0.05)
sys.exit(0 if out.exists() and json.loads(out.read_text(encoding='utf-8-sig'))['adopted'] else 2)
"""
def test_the_update_child_is_told_the_live_marker_owner(tmp_path):
def test_a_later_update_generation_adopts_the_handoffs_claim(tmp_path):
home, install = _install(tmp_path)
(install / "hermes_cli" / "main.py").write_text(RECORD_UPDATE_VIEW + FAKE_CLI, encoding="utf-8")
view = tmp_path / "view.txt"
(install / "hermes_cli" / "main.py").write_text(SECOND_GENERATION + FAKE_CLI, encoding="utf-8")
out = tmp_path / "gen2.json"
result = _run(tmp_path, home, install, HANDOFF_VIEW=str(view))
result = _run(tmp_path, home, install, GEN2_OUT=str(out), GEN2_REPO=str(REPOSITORY))
assert result.returncode == 0, result.stdout + result.stderr
custodian = _custodian(home)
assert custodian, "the hand-off never named a custodian"
handoff_pid, owner = view.read_text(encoding="utf-8-sig").split()
assert owner == custodian, "line 1 must name the custodian while the update runs"
assert handoff_pid == owner, "the update child must be told the marker's owner, not the hand-off script"
verdict = json.loads(out.read_text(encoding="utf-8-sig"))
assert verdict["adopted"], f"generation 2 refused its own hand-off's claim (holder {verdict['holder']})"
assert verdict["told"] == custodian, "the update must be told the marker's owner, not the hand-off script"
assert result.returncode == 0, result.stdout + result.stderr