fix(windows-update): a user-launched hermes serve/dashboard is never a Desktop backend

Canonicalising _is_backend_argv onto _hermes_holder_subcommand dropped the
'-m hermes_cli.main' entry-shape discriminator the old predicate had. That
widened _orphaned_desktop_backend_pids to tree-kill a standalone hermes.exe
serve / hermes dashboard whose console parent died. The Desktop's only spawn
shape is -m hermes_cli.main (apps/desktop/electron/main.ts), so _is_backend_argv
now forwards to _looks_like_desktop_control_plane — the same canonical-subcommand
AND entry-shape predicate — instead of a second copy. Test matrix gains a
'Desktop backend?' column with plain hermes serve / hermes.exe dashboard rows.
This commit is contained in:
teknium1
2026-09-12 23:54:38 -07:00
committed by Teknium
parent b57e7262e8
commit 466ccac5ab
2 changed files with 29 additions and 18 deletions

View File

@@ -424,9 +424,14 @@ def _relaunch_stopped_serves(token: dict) -> None:
def _is_backend_argv(argv_low: str) -> bool:
"""Whether an argv is a Desktop backend: the canonical holder classifier says ``serve``/``dashboard``
(a substring test matched ``kanban --preserve-cache`` and ``-m dashboard serve`` wrong, #91869)."""
return _hermes_holder_subcommand(argv_low) in ("serve", "dashboard")
"""Whether an argv is a DESKTOP backend — feeds ``taskkill /T`` via ``_orphaned_desktop_backend_pids``.
Same predicate as ``_looks_like_desktop_control_plane``: ``-m hermes_cli.main`` entry shape (the
Desktop's only spawn shape, ``apps/desktop/electron/main.ts``) AND the canonical holder classifier says
``serve``/``dashboard``. A user-launched ``hermes.exe serve`` / ``hermes dashboard`` is NOT the
Desktop's: the guard refuses on it, never reaps it.
"""
return _looks_like_desktop_control_plane(argv_low)
def _live_argv_low(psutil, pid, cmdline: str) -> str | None:

View File

@@ -11,28 +11,34 @@ from hermes_cli.update_cmd_windows import _hermes_holder_subcommand, _is_backend
LOOPBACK = "--host 127.0.0.1 --port 0"
# (cmdline, holder subcommand, desktop-local reap?). Substring scanners get every "trap" row wrong:
# "serve" appears inside --preserve-cache / observer.py / a flag value.
# (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),
("/venv/bin/hermes serve --isolated --host=127.0.0.1 --port=0 --ssh-owner-nonce abc", "serve", True),
("hermes --profile ops serve " + LOOPBACK, "serve", True),
("hermes -m serve kanban --preserve-cache " + LOOPBACK, "kanban", False),
("python -m hermes_cli.main kanban --preserve-cache " + LOOPBACK, "kanban", False),
("hermes --reasoning high dashboard " + LOOPBACK, "dashboard", False),
("hermes gateway run --replace", "gateway", False),
("hermes chat --model serve", "chat", False),
("python observer.py serve " + LOOPBACK, None, False),
("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", CMDLINES)
def test_kill_and_relaunch_predicates_agree_with_the_canonical_holder_matcher(cmdline, subcommand, reapable):
@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 (stop + relaunch path).
assert _is_backend_argv(cmdline.lower()) is (subcommand in ("serve", "dashboard"))
# 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():