diff --git a/hermes_cli/update_cmd_windows.py b/hermes_cli/update_cmd_windows.py index eda8a7b7df..cd3928b558 100644 --- a/hermes_cli/update_cmd_windows.py +++ b/hermes_cli/update_cmd_windows.py @@ -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: diff --git a/tests/hermes_cli/test_process_identity_canonical_matchers.py b/tests/hermes_cli/test_process_identity_canonical_matchers.py index 93e8a84252..28037f97e9 100644 --- a/tests/hermes_cli/test_process_identity_canonical_matchers.py +++ b/tests/hermes_cli/test_process_identity_canonical_matchers.py @@ -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():