70 lines
3.6 KiB
Python
70 lines
3.6 KiB
Python
"""Process-identity contract: the two kill/relaunch predicates and the profile liveness probe
|
|
defer to the canonical matchers instead of argv substrings (root AGENTS.md process-identity rule).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from hermes_cli.dashboard_procs import _is_desktop_local_serve_cmdline
|
|
from hermes_cli.update_cmd_windows import _hermes_holder_subcommand, _is_backend_argv
|
|
|
|
LOOPBACK = "--host 127.0.0.1 --port 0"
|
|
|
|
# (cmdline, holder subcommand, desktop-local reap?, Windows updater "Desktop backend"?). Substring
|
|
# scanners get every "trap" row wrong: "serve" appears inside --preserve-cache / observer.py / a flag
|
|
# value. The updater's kill set additionally requires the Desktop's `-m hermes_cli.main` spawn shape —
|
|
# a user-launched `hermes serve` / `hermes dashboard` is refused on, never tree-killed.
|
|
CMDLINES = [
|
|
("python -m hermes_cli.main serve " + LOOPBACK, "serve", True, True),
|
|
("python -m hermes_cli.main dashboard", "dashboard", False, True),
|
|
("/venv/bin/hermes serve --isolated --host=127.0.0.1 --port=0 --ssh-owner-nonce abc", "serve", True, False),
|
|
(r"C:\hermes\.venv\Scripts\hermes.exe serve --host 100.106.105.2 --port 9119", "serve", False, False),
|
|
("hermes.exe dashboard", "dashboard", False, False),
|
|
("hermes --profile ops serve " + LOOPBACK, "serve", True, False),
|
|
("hermes -m serve kanban --preserve-cache " + LOOPBACK, "kanban", False, False),
|
|
("python -m hermes_cli.main kanban --preserve-cache " + LOOPBACK, "kanban", False, False),
|
|
("hermes --reasoning high dashboard " + LOOPBACK, "dashboard", False, False),
|
|
("hermes gateway run --replace", "gateway", False, False),
|
|
("hermes chat --model serve", "chat", False, False),
|
|
("python observer.py serve " + LOOPBACK, None, False, False),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("cmdline,subcommand,reapable,desktop_backend", CMDLINES)
|
|
def test_kill_and_relaunch_predicates_agree_with_the_canonical_holder_matcher(
|
|
cmdline, subcommand, reapable, desktop_backend):
|
|
assert _hermes_holder_subcommand(cmdline) == subcommand
|
|
# Desktop-local reap (a KILL path): serve + loopback + ephemeral port, decided by tokens.
|
|
assert _is_desktop_local_serve_cmdline(cmdline) is reapable
|
|
# Windows updater backend classifier (taskkill /T on orphans): canonical subcommand AND Desktop spawn shape.
|
|
assert _is_backend_argv(cmdline.lower()) is desktop_backend
|
|
|
|
|
|
def test_desktop_local_serve_spares_fixed_port_and_remote_hosts():
|
|
assert not _is_desktop_local_serve_cmdline("hermes serve --host 100.106.105.2 --port 9119 --skip-build")
|
|
assert not _is_desktop_local_serve_cmdline("hermes serve --host 127.0.0.1 --port 9119")
|
|
assert _is_desktop_local_serve_cmdline("hermes serve --host localhost --port 0")
|
|
|
|
|
|
def test_profile_liveness_is_the_shared_ladder(tmp_path, monkeypatch):
|
|
"""``_check_gateway_running`` is ``resolve_gateway_liveness`` scoped to the profile dir, with the
|
|
PID rung reading (never cleaning) THAT profile's ``gateway.pid``."""
|
|
import gateway.status as gw_status
|
|
from hermes_cli.profiles import _check_gateway_running
|
|
|
|
seen: dict = {}
|
|
|
|
def fake_resolve(**kwargs):
|
|
seen.update(kwargs)
|
|
return gw_status.GatewayLiveness(running=True, pid=1, source="pid")
|
|
|
|
monkeypatch.setattr(gw_status, "resolve_gateway_liveness", fake_resolve)
|
|
calls: list = []
|
|
monkeypatch.setattr(gw_status, "get_running_pid",
|
|
lambda path, cleanup_stale=True: calls.append((path, cleanup_stale)))
|
|
assert _check_gateway_running(tmp_path) is True
|
|
assert seen["profile_dir"] == tmp_path
|
|
seen["pid_probe"](tmp_path / "gateway.pid")
|
|
assert calls == [(tmp_path / "gateway.pid", False)]
|