Files
hermes-agent/tests/hermes_cli/test_update_cold_start_gateway_liveness.py
T
Casey 790e1eb6bd fix(update): pause SCM-supervised Windows gateway services before venv mutation
On Windows installs where the gateway runs as an SCM service (WinSW,
NSSM, sc.exe create), the existing pause machinery kills the gateway
process directly — and the service wrapper's failure ladder resurrects
it within seconds, re-taking the venv file locks mid-update. The update
then dies partway through dependency sync with access-denied errors.

This extends _pause_windows_gateways_for_update() to detect when a
gateway's process tree is owned by a running SCM service, and to stop
the SERVICE through sc.exe instead of killing the child:

- gateway/status.py: expose service-ownership discovery for gateway
  runtimes (find_windows_gateway_services maps validated gateway PIDs
  through process ancestry to running SCM service PIDs, with
  create-time identity checks against PID reuse).
- hermes_cli/update_cmd.py: stop verified services via sc.exe before
  venv mutation and restart them afterward. Stops wait for a stable
  SCM 'stopped' state AND for the original descendant processes to
  exit (service 'Stopped' is not proof the child released its
  handles). Failure to prove ownership, stop a service, or restart it
  fails closed; rollback restores attempted services, and rollback
  failures are surfaced rather than swallowed.
- Fail-closed throughout: unreadable identities, ambiguous ancestry,
  or a service that will not reach a stable state abort the update
  before any file mutation.

Complements #37039 (gateway-only concurrent instances no longer abort):
that fix lets the update proceed past the gate; this one makes the
pause actually stick when the gateway is service-supervised.

Note: tests/gateway/test_status.py::TestReadProcessCmdlinePsFallback::
test_ps_fallback_when_proc_unavailable fails on Windows on current main
before this change as well (POSIX ps fallback asserted on a platform
without it); all other touched suites pass (155 passed, 5 skipped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 16:45:31 -07:00

53 lines
2.0 KiB
Python

"""#84185: a Windows gateway cold-started after update that dies immediately
(e.g. a job object denying breakaway) must not be reported as started.
``_cold_start_windows_gateway_after_update`` used to print the success line
straight off a successful ``Popen`` return, which only proves the process was
created, not that it survived. This asserts the observable output: the
success line is gated on the process actually being found alive afterwards,
same as every other ``_spawn_detached`` caller.
"""
from __future__ import annotations
import pytest
from hermes_cli import gateway as hermes_gateway
from hermes_cli import gateway_windows
from hermes_cli import main as cli_main
from hermes_cli import update_cmd
def _run_cold_start(monkeypatch, capsys, *, surviving_pids):
monkeypatch.setattr(cli_main, "_is_windows", lambda: True)
# The pre-spawn re-check (``all_profiles=True``) must find nothing
# running so the cold-start path proceeds and actually spawns.
monkeypatch.setattr(
hermes_gateway,
"find_gateway_pids",
lambda all_profiles=False: [] if all_profiles else surviving_pids,
)
monkeypatch.setattr(gateway_windows, "_spawn_detached", lambda: 4242)
# Avoid the real 6s/0.4s poll loop in _report_gateway_start.
monkeypatch.setattr(
gateway_windows, "_wait_for_gateway_ready", lambda *a, **k: surviving_pids
)
update_cmd._cold_start_windows_gateway_after_update()
return capsys.readouterr().out
def test_cold_start_raises_when_process_does_not_survive(monkeypatch, capsys):
with pytest.raises(RuntimeError, match="did not become ready"):
_run_cold_start(monkeypatch, capsys, surviving_pids=[])
assert "✓ Starting Windows gateway after update" not in capsys.readouterr().out
def test_cold_start_reports_success_when_process_survives(monkeypatch, capsys):
out = _run_cold_start(monkeypatch, capsys, surviving_pids=[4242])
assert "✓ Gateway started via cold-start after update" in out