fix(update): token-based control-plane classifier + live E2E for the Desktop-lifecycle cold-start skip (#76129 salvage follow-up)

On top of @686f6c61's premise-corrected #76745:

- _looks_like_desktop_control_plane now uses the parser-derived
  _hermes_holder_subcommand instead of substring matching — the
  #90778/#91869 class ('-m dashboard chat' and 'kanban --preserve-cache'
  argv no longer read as control planes). Regression test added,
  sabotage-verified (reverting to substrings fails it).
- Live E2E (this host, real processes + real spawn ledger): live
  supervised serve owns lifecycle; killed spawner (orphan) does not;
  dead serve entry excluded; empty ledger does not.
- Live Windows E2E for the wine2e lane: real self-registered ledger
  entry suppresses the actual cold-start plan; dead serve restores it;
  holder-scan fallback rung proves the token classifier live.

Co-authored-by: 686f6c61 <github@00b.tech>
This commit is contained in:
Teknium
2026-08-22 21:19:24 -07:00
parent 4ccc4b6931
commit f4067774aa
3 changed files with 176 additions and 2 deletions

View File

@@ -4680,9 +4680,16 @@ def _looks_like_desktop_control_plane(cmdline: str) -> bool:
That is the Desktop control plane, not the messaging gateway. Serve and
dashboard do not host platform adapters (#92091); do not feed this into
``looks_like_gateway_command_line``.
Token-based via the parser-derived subcommand classifier — never
substring (#90778/#91869: ``kanban --preserve-cache`` contains "serve",
``-m dashboard chat`` contains " dashboard"; both are NOT control
planes). A cmdline whose subcommand cannot be determined is NOT a
control plane — callers must not guess ownership.
"""
low = (cmdline or "").lower()
return "hermes_cli.main" in low and (" serve" in low or " dashboard" in low)
if "hermes_cli.main" not in (cmdline or "").lower():
return False
return _hermes_holder_subcommand(cmdline) in ("serve", "dashboard")
def _desktop_owns_gateway_lifecycle() -> bool:

View File

@@ -0,0 +1,144 @@
"""LIVE Windows E2E for the Desktop-lifecycle cold-start skip (#76129/#76745).
Runs ONLY on a real Windows host (the on-demand ``windows-venv-e2e.yml``
lane). Exercises the REAL ownership predicate against REAL processes:
1. A real child process self-registers in the REAL spawn ledger as a
``serve`` purpose with THIS process as its live spawner — ownership
must hold, and ``_pause_windows_gateways_for_update`` must return None
(no cold-start plan) even with an autostart artifact present.
2. Kill the child (dead serve) — ownership drops, the pause plan carries
``cold_start_if_installed`` again.
3. Venv-holder fallback rung: a real venv-python process with true
``serve`` argv is detected by the scan and, having a live parent,
confers ownership; the token classifier rejects a ``kanban
--preserve-cache`` lookalike (the #90778 class the salvage fixed).
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import time
from pathlib import Path
import pytest
pytestmark = pytest.mark.skipif(
sys.platform != "win32", reason="live Windows lifecycle E2E"
)
PROJECT_ROOT = Path(__file__).resolve().parents[2]
@pytest.fixture()
def sleeper():
procs: list[subprocess.Popen] = []
def _spawn(*tail: str) -> subprocess.Popen:
p = subprocess.Popen(
[sys.executable, "-c", "import time; time.sleep(120)", *tail],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
procs.append(p)
time.sleep(0.5)
assert p.poll() is None
return p
yield _spawn
for p in procs:
if p.poll() is None:
p.kill()
p.wait()
def _write_ledger(entries: list[dict]) -> None:
from hermes_cli import process_identity as pid_mod
path = pid_mod._ledger_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(entries), encoding="utf-8")
def _entry(proc: subprocess.Popen, purpose: str = "serve") -> dict:
import psutil
from hermes_cli import process_identity as pid_mod
return {
"install": pid_mod.install_id(None),
"pid": proc.pid,
"create_time": psutil.Process(proc.pid).create_time(),
"purpose": purpose,
"spawner_pid": os.getpid(),
"spawner_create": psutil.Process(os.getpid()).create_time(),
}
def test_live_supervised_serve_suppresses_cold_start(sleeper, monkeypatch, tmp_path):
from hermes_cli import gateway as hermes_gateway
from hermes_cli import gateway_windows
from hermes_cli import update_cmd
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
(tmp_path / ".hermes").mkdir()
serve = sleeper()
_write_ledger([_entry(serve)])
assert update_cmd._desktop_owns_gateway_lifecycle() is True
# Autostart artifact present + no gateway running: WITHOUT ownership the
# pause phase would plan a cold start; WITH it, no plan.
monkeypatch.setattr(hermes_gateway, "find_gateway_pids", lambda **_k: [])
monkeypatch.setattr(gateway_windows, "is_installed", lambda: True)
assert update_cmd._pause_windows_gateways_for_update() is None
# Dead serve → ownership drops → plan returns.
serve.kill()
serve.wait()
time.sleep(0.5)
assert update_cmd._desktop_owns_gateway_lifecycle() is False
token = update_cmd._pause_windows_gateways_for_update()
assert token is not None and token.get("cold_start_if_installed") is True
def test_holder_scan_fallback_respects_token_classifier(sleeper, monkeypatch, tmp_path):
from hermes_cli import update_cmd
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
(tmp_path / ".hermes").mkdir()
_write_ledger([]) # force the fallback rung
# Real process whose argv carries genuine serve shape, visible to psutil.
serve_like = sleeper("-m", "hermes_cli.main", "serve")
# Lookalike from the #90778 class — must NOT confer ownership.
kanban_like = sleeper("-m", "hermes_cli.main", "kanban", "--preserve-cache")
def fake_holders():
import psutil
out = []
for p in (serve_like, kanban_like):
proc = psutil.Process(p.pid)
out.append((p.pid, proc.name(), " ".join(proc.cmdline())))
return out
monkeypatch.setattr(
"hermes_cli.main._detect_venv_python_processes", fake_holders
)
# serve-shaped holder with a live parent (us) → owns
assert update_cmd._desktop_owns_gateway_lifecycle() is True
# Only the kanban lookalike left → classifier rejects → does not own
serve_like.kill()
serve_like.wait()
monkeypatch.setattr(
"hermes_cli.main._detect_venv_python_processes",
lambda: [fake_holders()[1]],
)
assert update_cmd._desktop_owns_gateway_lifecycle() is False

View File

@@ -41,6 +41,29 @@ def test_control_plane_argv_is_not_a_gateway():
assert looks_like_gateway_command_line(run) is True
def test_control_plane_classifier_is_token_based_not_substring():
"""#90778/#91869 class: flag values and lookalike tokens must not read
as a control plane. The salvage swapped the original substring check
for the parser-derived subcommand classifier."""
py = "C:\\Hermes\\.venv\\Scripts\\python.exe -m hermes_cli.main"
# "dashboard" as a FLAG VALUE, real subcommand is chat
assert update_cmd._looks_like_desktop_control_plane(f"{py} -m dashboard chat") is False
# "--preserve-cache" contains "serve"; real subcommand is kanban
assert (
update_cmd._looks_like_desktop_control_plane(f"{py} kanban --preserve-cache")
is False
)
# profile selector before the real subcommand still classifies correctly
assert (
update_cmd._looks_like_desktop_control_plane(f"{py} --profile serve dashboard")
is True
)
# dashboard as the real subcommand
assert update_cmd._looks_like_desktop_control_plane(f"{py} dashboard") is True
# undeterminable subcommand → NOT a control plane (never guess ownership)
assert update_cmd._looks_like_desktop_control_plane("python.exe -c import time") is False
def test_ledger_live_serve_with_live_spawner_owns_lifecycle(monkeypatch):
monkeypatch.setattr(
process_identity, "ledger_entries", lambda **_k: [_live_serve_ledger_entry()]