Files
hermes-agent/tests/hermes_cli/test_serve_parent_watchdog.py
ethernet 071ccfbcdf test: restore platforms() host gating that merges reverted to the legacy trio
Four origin/main merges brought back `linux_only` / `macos_only` /
`windows_only` marks in 41 test files, along with the pre-platforms()
versions of scripts/ci/list_os_marked_tests.py and check_os_marker_fakes.py.
Because the legacy names are no longer registered, pytest treated them as
unknown marks — a warning — so every Windows- or macOS-only test RAN on
Linux (test_local_runtime_recovery.py tripped the live-system kill guard).

Rewrite the marks, restore the platforms()-aware CI scripts (keeping main's
os.walk fix for vanishing __pycache__ dirs), drop the stale _BASELINE entries,
and make the conftest reject the retired marks outright so the next merge
cannot resurrect them silently.
2026-09-18 19:06:52 -04:00

222 lines
7.9 KiB
Python

"""Regression tests for Desktop-owned ``hermes serve`` lifecycle tracking."""
import pytest
from hermes_cli.web_server_lifecycle import (
_is_serve_orphaned,
_parent_start_marker_mismatch_is_conclusive,
_valid_parent_start_marker,
)
def test_parent_watchdog_tracks_recorded_desktop_pid_not_immediate_ppid():
"""Windows venv launch shims must not make a live Desktop look orphaned."""
assert _is_serve_orphaned(4242, pid_exists=lambda pid: pid == 4242) is False
assert _is_serve_orphaned(4242, pid_exists=lambda _pid: False) is True
def test_parent_watchdog_fails_safe_when_liveness_probe_errors():
def broken_probe(_pid: int) -> bool:
raise OSError("process table temporarily unavailable")
assert _is_serve_orphaned(4242, pid_exists=broken_probe) is False
def test_parent_watchdog_accepts_electron_windows_creation_time_marker():
unix_ms = 1_723_456_789_123
dotnet_ticks = 621_355_968_000_000_000 + unix_ms * 10_000 + 9_999
assert _valid_parent_start_marker(f"winms:{unix_ms}") is True
assert (
_is_serve_orphaned(
4242,
f"winms:{unix_ms}",
process_start_marker=lambda _pid: f"win:{dotnet_ticks}",
)
is False
)
def test_parent_watchdog_rejects_reused_pid_with_different_windows_creation_time():
unix_ms = 1_723_456_789_123
next_process_ticks = 621_355_968_000_000_000 + (unix_ms + 1) * 10_000
assert (
_is_serve_orphaned(
4242,
f"winms:{unix_ms}",
process_start_marker=lambda _pid: f"win:{next_process_ticks}",
)
is True
)
def test_parent_watchdog_preserves_legacy_exact_windows_marker():
marker = "win:638908765432109876"
assert (
_is_serve_orphaned(
4242,
marker,
process_start_marker=lambda _pid: marker,
)
is False
)
def test_parent_watchdog_does_not_kill_a_live_parent_on_macos_timezone_drift():
"""#95693: the SAME instant rendered by `ps -o lstart=` under EDT (cached by
Electron before a TZ change) vs CEST (probed by a fresh backend after) must
degrade to the PID-only check, not count as proof the parent died."""
expected = "ps:Thu Aug 20 22:33:11 2026"
actual = "ps:Fri Aug 21 04:33:11 2026"
assert (
_is_serve_orphaned(
4242, expected, pid_exists=lambda _pid: True, process_start_marker=lambda _pid: actual
)
is False
)
def test_parent_watchdog_still_detects_a_genuinely_dead_parent_despite_ps_marker_mismatch():
expected = "ps:Thu Aug 20 22:33:11 2026"
actual = "ps:Fri Aug 21 04:33:11 2026"
assert (
_is_serve_orphaned(
4242, expected, pid_exists=lambda _pid: False, process_start_marker=lambda _pid: actual
)
is True
)
def test_parent_watchdog_exact_ps_marker_match_still_short_circuits():
marker = "ps:Thu Aug 20 22:33:11 2026"
assert (
_is_serve_orphaned(
4242, marker, pid_exists=lambda _pid: False, process_start_marker=lambda _pid: marker
)
is False
)
def test_parent_watchdog_still_rejects_recycled_pid_via_stable_linux_marker():
"""linux:/win: markers are machine values -- a mismatch stays conclusive."""
assert (
_is_serve_orphaned(
4242,
"linux:12345",
pid_exists=lambda _pid: True,
process_start_marker=lambda _pid: "linux:99999",
)
is True
)
assert _parent_start_marker_mismatch_is_conclusive("linux:123", "linux:456") is True
assert _parent_start_marker_mismatch_is_conclusive("win:1", "winms:2") is True
def test_macos_ps_marker_requires_full_lstart_not_a_truncated_weekday():
"""#98132: a whitespace-split ``ps:Sat`` must not arm the watchdog."""
assert _valid_parent_start_marker("ps:Sat Aug 29 15:04:31 2026") is True
assert _valid_parent_start_marker("ps:Sat") is False
assert _valid_parent_start_marker("ps:Sat Aug 29") is False
def test_parent_watchdog_treats_empty_marker_env_as_absent(monkeypatch):
"""Blank inherited HERMES_PARENT_START_MARKER/NONCE degrade to PID-only tracking."""
from hermes_cli import web_server_lifecycle
monkeypatch.setenv("HERMES_PARENT_PID", "4242")
monkeypatch.setenv("HERMES_PARENT_START_MARKER", "")
monkeypatch.setenv("HERMES_PARENT_NONCE", "")
seen = {}
def fake_orphaned(pid, marker):
seen["args"] = (pid, marker)
raise SystemExit # stop the loop thread before it sleeps/exits
monkeypatch.setattr(web_server_lifecycle, "_is_serve_orphaned", fake_orphaned)
class _Thread:
def __init__(self, target, **_kw):
self.target = target
def start(self):
try:
self.target()
except SystemExit:
pass
monkeypatch.setattr(web_server_lifecycle.threading, "Thread", _Thread)
web_server_lifecycle._start_parent_death_watchdog()
assert seen["args"] == (4242, None)
def test_parent_watchdog_warns_when_disarmed_by_unusable_marker(monkeypatch, caplog):
"""Disarming is fail-safe but must leave a trace in the log."""
import logging
from hermes_cli import web_server_lifecycle
monkeypatch.setenv("HERMES_PARENT_PID", "4242")
monkeypatch.setenv("HERMES_PARENT_START_MARKER", "ps:Sat")
monkeypatch.setenv("HERMES_PARENT_NONCE", "n")
started = []
monkeypatch.setattr(
web_server_lifecycle.threading, "Thread", lambda *a, **k: started.append(1) or _NoThread()
)
with caplog.at_level(logging.WARNING, logger="hermes_cli.web_server"):
web_server_lifecycle._start_parent_death_watchdog()
assert started == [], "an unusable marker must disarm, not arm, the watchdog"
assert any("watchdog disabled" in r.getMessage() for r in caplog.records)
class _NoThread:
def start(self):
raise AssertionError("watchdog thread must not start")
def test_parent_watchdog_degrades_to_pid_liveness_when_marker_probe_raises_oserror():
"""#80204: a probe failure must fall through to ``pid_exists`` instead of pinning the
watchdog to "not orphaned" forever on a dead Desktop parent."""
def broken_marker_probe(pid: int) -> str:
raise OSError(f"ps could not inspect PID {pid}: process table temporarily unavailable")
marker = "ps:Thu Aug 20 22:33:11 2026"
assert _is_serve_orphaned(4242, marker, pid_exists=lambda _pid: False,
process_start_marker=broken_marker_probe) is True
assert _is_serve_orphaned(4242, marker, pid_exists=lambda _pid: True,
process_start_marker=broken_marker_probe) is False
def lookup_error_probe(pid: int) -> str:
raise ProcessLookupError(pid)
# ProcessLookupError is conclusive on its own, whatever a recycled-pid liveness check says.
assert _is_serve_orphaned(4242, marker, pid_exists=lambda _pid: True,
process_start_marker=lookup_error_probe) is True
@pytest.mark.platforms("macos")
def test_ps_marker_probe_classifies_missing_process_vs_other_ps_failures(monkeypatch):
"""The darwin ``ps`` probe raises ``ProcessLookupError`` only for an explicit missing-process
message; any other unknown failure stays a plain ``OSError`` so the watchdog degrades instead
of killing a healthy backend."""
import subprocess
from hermes_cli import web_server_lifecycle
def fake_run(stderr):
return lambda *a, **k: subprocess.CompletedProcess(args=a, returncode=2, stdout="", stderr=stderr)
monkeypatch.setattr(subprocess, "run", fake_run("ps: 4242: No such process"))
with pytest.raises(ProcessLookupError):
web_server_lifecycle._process_start_marker(4242)
monkeypatch.setattr(subprocess, "run", fake_run("ps: temporary process table failure"))
with pytest.raises(OSError) as excinfo:
web_server_lifecycle._process_start_marker(4242)
assert not isinstance(excinfo.value, ProcessLookupError)