test(gateway): trim the reap-grace tests to the one invariant

Collapse TestReaperStartupGrace into a single test pinning the grace
invariant: a booting gateway and an undeterminable age are spared under a
positive grace while a stale orphan is still reaped. The no-grace default
is already covered by the existing reaper tests.
This commit is contained in:
kshitijk4poor
2026-09-27 15:18:30 +05:30
committed by kshitij
parent b7c5207b9e
commit f106786e15

View File

@@ -849,65 +849,37 @@ class TestReapUnsupervisedGatewayOrphansWindows:
class TestReaperStartupGrace:
"""``min_age_s`` spares a gateway that is still claiming its identity (#122533).
"""``min_age_s`` spares a gateway still claiming gateway.pid/lock (#122533).
A gateway claims ``gateway.pid``/``gateway.lock`` only after imports and runner
setup, so for the first seconds of its life it is visible to the argv sweep but
not to the record-based exclusions. Reaping it writes a planned-stop marker the
booting gateway consumes as soon as it finishes starting — a clean exit 0 with no
supervisor to revive it, so a Desktop-launched bot goes silent until a manual
start. The grace applies only at the Desktop boot sweep; stop/restart keep the
immediate reap (#75936).
A booting gateway is argv-visible before it is record-visible; reaping it writes
a planned-stop marker it consumes on startup and exits 0 with no supervisor. An
undeterminable age must read as too young, never widen the reap.
"""
@staticmethod
def _isolate_reaper(monkeypatch, candidates, ages):
killed = []
def test_grace_spares_booting_and_unknown_age_but_reaps_stale_orphan(self, monkeypatch):
booting, unknown, stale = 55501, 55502, 99998
ages = {booting: 2.0, stale: 900.0}
def _age(pid):
if pid not in ages:
raise RuntimeError("probe failed")
return ages[pid]
monkeypatch.setattr(gateway, "supports_systemd_services", lambda: False)
monkeypatch.setattr("gateway.status.get_running_pid", lambda cleanup_stale=True: None)
monkeypatch.setattr(
gateway,
"find_gateway_pids",
lambda exclude_pids=None: [p for p in candidates if p not in (exclude_pids or set())],
gateway, "find_gateway_pids",
lambda exclude_pids=None: [p for p in (booting, unknown, stale) if p not in (exclude_pids or set())],
)
monkeypatch.setattr(gateway, "_gateway_process_age_s", lambda pid: ages[pid])
monkeypatch.setattr(gateway.os, "kill", lambda pid, sig: killed.append(pid))
monkeypatch.setattr("hermes_cli.dashboard_procs._process_age_seconds", _age)
monkeypatch.setattr(gateway.os, "kill", lambda pid, sig: None)
monkeypatch.setattr("gateway.status._pid_exists", lambda pid: False)
monkeypatch.setattr("time.sleep", lambda _: None)
monkeypatch.setattr("time.monotonic", lambda: 1.0)
return killed
def test_grace_spares_a_booting_gateway_but_still_reaps_a_stale_orphan(self, monkeypatch):
booting_pid, stale_orphan_pid = 55501, 99998
self._isolate_reaper(
monkeypatch,
[booting_pid, stale_orphan_pid],
{booting_pid: 2.0, stale_orphan_pid: 900.0},
)
marked_pids = []
monkeypatch.setattr("gateway.status.write_planned_stop_marker", marked_pids.append)
marked = []
monkeypatch.setattr("gateway.status.write_planned_stop_marker", marked.append)
assert gateway._reap_unsupervised_gateway_orphans(min_age_s=180.0) is True
assert marked_pids == [stale_orphan_pid]
def test_no_grace_keeps_reaping_a_booting_gateway(self, monkeypatch):
booting_pid = 55501
self._isolate_reaper(monkeypatch, [booting_pid], {booting_pid: 0.0})
marked_pids = []
monkeypatch.setattr("gateway.status.write_planned_stop_marker", marked_pids.append)
assert gateway._reap_unsupervised_gateway_orphans() is True
assert marked_pids == [booting_pid]
def test_undeterminable_age_is_spared_under_a_grace(self, monkeypatch):
"""A failed age probe must never widen a reap: unknown reads as too young."""
booting_pid = 55501
self._isolate_reaper(monkeypatch, [booting_pid], {booting_pid: 0.0})
marked_pids = []
monkeypatch.setattr("gateway.status.write_planned_stop_marker", marked_pids.append)
assert gateway._reap_unsupervised_gateway_orphans(min_age_s=180.0) is False
assert marked_pids == []
assert marked == [stale]
class TestReaperCandidateIsSupervisorOwned: