dfcef70061
Fixes #88654. After an in-place update, the manual-gateway leg of the restart phase did this for every profile-mapped gateway: restart_mode = _prepare_profile_gateway_update_restart(proc.profile, pid) if restart_mode is None: continue A None means no relaunch could be armed. The bare continue skipped the drain and the stop, and the unmapped sweep immediately below skips any pid already in profile_processes, so the process was never killed and never counted into the "Stopped N manual gateway process(es)" summary. The gateway kept running with its pre-update modules resident while the new code sat on disk, and every lazy import from that point mixed versions: cannot import name '_MAX_TOOL_ERROR_CHARS' from 'tools.registry' with no operator signal of any kind. Two changes. _prepare_profile_gateway_update_restart now falls back to replaying the process's own captured command line when the profile-derived relaunch cannot be armed. launch_detached_gateway_restart_by_cmdline already exists for exactly this case and documents itself as the companion for gateways with no profile mapping; the Windows post-update path already uses it the same way. The argv is captured a few lines earlier for the external-supervisor check, so the fallback costs nothing extra. The external-supervisor branch still short-circuits first, because replaying argv there would escape the manager and race its replacement process. When neither mechanism can arm a relaunch, the update path no longer falls through silently. It says so, naming the profile and pid, and hands the process to the existing unmapped sweep so it is stopped and reported through the established "Restart manually: hermes gateway run" contract. Leaving it running was the actual harm: a gateway on stale modules fails every lazy import for as long as it lives.
121 lines
4.3 KiB
Python
121 lines
4.3 KiB
Python
"""Regression coverage for #88654.
|
|
|
|
``hermes update`` relaunches manually-run profile gateways through
|
|
``_prepare_profile_gateway_update_restart``. When the profile-derived
|
|
relaunch could not be armed the helper returned ``None``, and the update
|
|
path's response to ``None`` was a bare ``continue`` -- so the gateway was
|
|
neither relaunched, nor stopped, nor mentioned. It kept serving from
|
|
pre-update modules while the new code sat on disk, and every lazy import
|
|
from that point mixed versions.
|
|
|
|
The helper now falls back to replaying the process's own captured command
|
|
line via ``launch_detached_gateway_restart_by_cmdline`` -- the companion
|
|
that already exists for gateways with no profile mapping, and that the
|
|
Windows post-update path already uses for exactly this case.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
import hermes_cli.gateway as gateway
|
|
|
|
|
|
_ARGV = ["python", "-m", "hermes_cli.main", "gateway", "run"]
|
|
|
|
|
|
def _stub_argv(monkeypatch, argv):
|
|
monkeypatch.setattr(gateway, "_capture_gateway_argv", lambda _pid: argv)
|
|
|
|
|
|
def test_profile_relaunch_wins_and_skips_the_cmdline_replay(monkeypatch):
|
|
"""The existing path is unchanged: a profile relaunch short-circuits."""
|
|
_stub_argv(monkeypatch, list(_ARGV))
|
|
monkeypatch.setattr(
|
|
gateway, "launch_detached_profile_gateway_restart", lambda *_a: True
|
|
)
|
|
monkeypatch.setattr(
|
|
gateway,
|
|
"launch_detached_gateway_restart_by_cmdline",
|
|
lambda *_a: pytest.fail("cmdline replay must not run when the profile path works"),
|
|
)
|
|
|
|
assert gateway._prepare_profile_gateway_update_restart("fitness", 4242) == "detached"
|
|
|
|
|
|
def test_falls_back_to_cmdline_replay_when_profile_relaunch_fails(monkeypatch):
|
|
"""The #88654 fix: an unarmable profile relaunch still gets the gateway back."""
|
|
_stub_argv(monkeypatch, list(_ARGV))
|
|
monkeypatch.setattr(
|
|
gateway, "launch_detached_profile_gateway_restart", lambda *_a: False
|
|
)
|
|
seen = []
|
|
|
|
def _by_cmdline(pid, argv):
|
|
seen.append((pid, argv))
|
|
return True
|
|
|
|
monkeypatch.setattr(
|
|
gateway, "launch_detached_gateway_restart_by_cmdline", _by_cmdline
|
|
)
|
|
|
|
assert (
|
|
gateway._prepare_profile_gateway_update_restart("fitness", 4242)
|
|
== "detached-cmdline"
|
|
)
|
|
# Replays the process's OWN argv, which is the whole point: the profile
|
|
# could not be mapped back to a run argv, so the captured one is the only
|
|
# faithful description of how to restart it.
|
|
assert seen == [(4242, _ARGV)]
|
|
|
|
|
|
def test_returns_none_when_there_is_no_argv_to_replay(monkeypatch):
|
|
"""No captured argv means no honest way to relaunch; caller must be told."""
|
|
_stub_argv(monkeypatch, [])
|
|
monkeypatch.setattr(
|
|
gateway, "launch_detached_profile_gateway_restart", lambda *_a: False
|
|
)
|
|
monkeypatch.setattr(
|
|
gateway,
|
|
"launch_detached_gateway_restart_by_cmdline",
|
|
lambda *_a: pytest.fail("must not replay an empty argv"),
|
|
)
|
|
|
|
assert gateway._prepare_profile_gateway_update_restart("fitness", 4242) is None
|
|
|
|
|
|
def test_returns_none_when_both_relaunch_paths_fail(monkeypatch):
|
|
"""Both mechanisms failing is still reported as None, not a false success."""
|
|
_stub_argv(monkeypatch, list(_ARGV))
|
|
monkeypatch.setattr(
|
|
gateway, "launch_detached_profile_gateway_restart", lambda *_a: False
|
|
)
|
|
monkeypatch.setattr(
|
|
gateway, "launch_detached_gateway_restart_by_cmdline", lambda *_a: False
|
|
)
|
|
|
|
assert gateway._prepare_profile_gateway_update_restart("fitness", 4242) is None
|
|
|
|
|
|
def test_external_supervisor_still_short_circuits_before_any_replay(monkeypatch):
|
|
"""Guardrail: the supervisor hand-back must not gain a replay behind it.
|
|
|
|
Replaying the argv for an externally supervised gateway would escape the
|
|
manager and race its replacement process, which is the exact hazard the
|
|
supervisor branch exists to avoid.
|
|
"""
|
|
_stub_argv(monkeypatch, _ARGV + ["--external-supervisor"])
|
|
monkeypatch.setattr(
|
|
gateway,
|
|
"launch_detached_profile_gateway_restart",
|
|
lambda *_a: pytest.fail("detached watcher must not be launched"),
|
|
)
|
|
monkeypatch.setattr(
|
|
gateway,
|
|
"launch_detached_gateway_restart_by_cmdline",
|
|
lambda *_a: pytest.fail("cmdline replay must not be launched"),
|
|
)
|
|
|
|
assert (
|
|
gateway._prepare_profile_gateway_update_restart("fitness", 4242)
|
|
== "external-supervisor"
|
|
)
|