From 66605471e9f0b0832abbefaf625ce08e948ca540 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sat, 10 Oct 2026 03:57:30 -0700 Subject: [PATCH] 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. --- .../test_posix_handoff_pid_export.py | 73 +++++++++++++------ 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/tests/scripts/desktop_update/test_posix_handoff_pid_export.py b/tests/scripts/desktop_update/test_posix_handoff_pid_export.py index 9183606e7d..6389b364db 100644 --- a/tests/scripts/desktop_update/test_posix_handoff_pid_export.py +++ b/tests/scripts/desktop_update/test_posix_handoff_pid_export.py @@ -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