Files
hermes-agent/tests/hermes_cli/test_windows_relaunch_ready_poll.py
Teknium 6483df614d fix: post-update relaunch verification polls every target in one loop
Retiring each paused runtime on its own readiness made the wait serial: every
profile and unmapped entry ran its own 2 s confirmation window (plus a 6 s
per-target floor past the deadline), so 3 profiles took 6 s and 5 took 10 s
where the fleet-wide wait had taken one window. All targets now share one poll:
one fleet process scan per tick, each target credited only on its own hits for
a whole confirmation window, and a PID one target vouched for is never credited
to another. Measured with readiness stubbed to succeed: N=1/3/5 went from
2.0/6.0/10.0 s to 2.0 s flat (N=10 also 2.0 s).

The reconciliation tests' liveness stub moves to the probe the verifier now
polls (_live_gateway_pids); their assertions are unchanged.
2026-10-06 06:18:39 -07:00

84 lines
3.9 KiB
Python

"""Post-update relaunch verification polls every paused target at once (#132338 review, kshitij).
Each relaunched profile / unmapped gateway is retired only on its own stable readiness, and before
this every target ran its own confirmation window in turn: N targets cost N windows (5 profiles,
10 s) where the fleet-wide wait had cost one. One loop keeps the per-target crediting.
Real ``_verify_relaunched_gateways_alive`` and its poll; the seam is the PID-file read that says
which gateway each profile home runs (``get_running_pid``), so no process is spawned or signalled.
"""
from __future__ import annotations
import time
import pytest
from gateway import status
from hermes_cli import gateway_windows, update_cmd_windows
from hermes_cli.update_cmd_windows import _READY_CONFIRM_S, _verify_relaunched_gateways_alive
@pytest.fixture
def running(monkeypatch):
"""``{profile: callable -> pid or None}``: what each profile home's PID file names right now."""
table: dict = {}
monkeypatch.setattr(status, "get_running_pid",
lambda path, cleanup_stale=False: (table.get(path.parent.name) or (lambda: None))())
monkeypatch.setattr(status, "_pid_exists", lambda pid: False) # every old gateway is gone
monkeypatch.setattr(gateway_windows, "_write_start_attestation", lambda *a, **k: None)
return table
def test_many_ready_targets_cost_one_confirmation_window(running):
profiles = {f"p{i}": 40000 + i for i in range(6)}
for i, name in enumerate(profiles):
running[name] = lambda pid=50000 + i: pid
token = {"profiles": dict(profiles), "unmapped": []}
started = time.monotonic()
_verify_relaunched_gateways_alive(token, profiles, [])
elapsed = time.monotonic() - started
assert token["relaunched_profiles"] == sorted(profiles) and token["profiles"] == {}
assert elapsed < 2 * _READY_CONFIRM_S, f"6 ready targets took {elapsed:.1f}s: verified one after another"
def test_each_target_is_credited_only_on_its_own_stable_gateway(running, monkeypatch):
monkeypatch.setattr(update_cmd_windows, "_RELAUNCH_VERIFY_TIMEOUT_S", 3.0)
born = time.monotonic()
running["up"] = lambda: 50001
running["old"] = lambda: 40002 # only the process the relaunch replaces
running["flaps"] = lambda: 50003 if time.monotonic() - born < 1.0 else None # died during confirmation
profiles = {"up": 40001, "old": 40002, "flaps": 40003, "never": 40004}
token = {"profiles": dict(profiles), "unmapped": []}
with pytest.raises(RuntimeError, match="not verified alive: flaps, never, old"):
_verify_relaunched_gateways_alive(token, profiles, [])
assert token["relaunched_profiles"] == ["up"]
assert sorted(token["profiles"]) == ["flaps", "never", "old"]
@pytest.mark.platforms("linux")
@pytest.mark.spawns_gateway_lookalike # a stub child that sleeps; reaped below
def test_one_gateway_never_retires_two_unmapped_debts(running, monkeypatch, tmp_path):
import os
import subprocess
import sys
from hermes_cli import gateway
monkeypatch.setattr(update_cmd_windows, "_RELAUNCH_VERIFY_TIMEOUT_S", 3.0)
entry = tmp_path / "bin" / "hermes"
entry.parent.mkdir()
entry.write_text("import time\ntime.sleep(60)\n", encoding="utf-8")
argv = [sys.executable, str(entry), "gateway", "run"]
# Pre-home entries (an older updater's token): argv is all that identifies them.
debts = [{"pid": 40011, "argv": argv}, {"pid": 40012, "argv": argv}]
child = subprocess.Popen(argv, env=dict(os.environ))
try:
monkeypatch.setattr(gateway, "find_gateway_pids", lambda all_profiles=False: [child.pid])
token = {"profiles": {}, "unmapped": list(debts)}
with pytest.raises(RuntimeError, match="pid 40012"):
_verify_relaunched_gateways_alive(token, {}, debts)
assert token["unmapped"] == [debts[1]], "one relaunched gateway retired both debts"
finally:
child.kill()
child.wait(timeout=10)