Files
hermes-agent/nix/tests/desktop-backend.py
ethernet a41aabff6c fix: platform legs — docker arm64 bootstrap, win32-arm64 native builds, nix desktop-backend cleanup
- pm.environment owns _RESOLVER_MARKERS: the streaming uv runner imported
  pm.workspace, whose tomllib import fails on the 3.10 system python that
  bootstraps the Docker arm64 image (No module named 'tomllib'). Invariant test
  proves runtime staging needs neither tomllib nor pm.workspace.
- run_tests.sh forwards the MSVC/SDK/Rust/OpenSSL toolchain variables through
  its env -i scrub so PM tests that compile ruamel-yaml-clib on Windows arm64
  find cl.exe (previously 'Visual C++ 14.0 or greater is required').
- nix desktop-backend check: the spawned backend outlives cage's process group
  and kept writing under the temp HERMES_HOME during rmtree; stop every
  process bound to the throwaway HOME before cleanup.
- windows: test_launcher_runtime_selection imports runtime_state from
  hermes_cli (moved in bbec973514); the ' spaced ' suffix row loses its
  trailing space on win32 (the filesystem strips it).
- macOS: test_sealed_worker_command copies the interpreter into the payload
  (the escape guard resolves symlinks) and links the host lib tree.
2026-09-20 10:42:30 -04:00

111 lines
4.5 KiB
Python

"""Launch the packaged desktop with a competing mutable install."""
import contextlib
import os
from pathlib import Path
import signal
import subprocess
import sys
import tempfile
import time
def _processes_under(home: Path) -> list[int]:
"""PIDs whose environment or cwd binds them to this throwaway HOME (Linux /proc)."""
needle = str(home).encode()
found = []
for entry in os.listdir("/proc"):
if not entry.isdigit() or int(entry) == os.getpid():
continue
try:
environ = Path("/proc", entry, "environ").read_bytes()
cwd = os.readlink(f"/proc/{entry}/cwd").encode()
except OSError:
continue
if needle in environ or cwd.startswith(needle):
found.append(int(entry))
return found
desktop, expected = sys.argv[1:]
with tempfile.TemporaryDirectory(prefix="hermes-desktop-backend-") as temporary:
home = Path(temporary)
hermes_home = home / ".hermes"
legacy = hermes_home / "hermes-agent"
(legacy / "hermes_cli").mkdir(parents=True)
(legacy / "hermes_cli" / "main.py").touch()
launcher = legacy / "venv" / "bin" / "hermes"
launcher.parent.mkdir(parents=True)
# The old runtime passes discovery but cannot initialize a session. It
# must not win merely because it was installed before the Nix desktop.
launcher.write_text(
f"#!{sys.executable}\n"
"import sys\n"
"if '--version' in sys.argv:\n"
" print('Hermes legacy fixture')\n"
" sys.exit(0)\n"
"print('WRONG_BACKEND_SELECTED', flush=True)\n"
"sys.exit(73)\n"
)
launcher.chmod(0o755)
runtime = home / "runtime"
runtime.mkdir(mode=0o700)
env = {
"PATH": os.environ["PATH"],
"HOME": str(home),
"HERMES_HOME": str(hermes_home),
"HERMES_DESKTOP_USER_DATA_DIR": str(home / "electron"),
"XDG_RUNTIME_DIR": str(runtime),
"XDG_CONFIG_HOME": str(home / ".config"),
"XDG_CACHE_HOME": str(home / ".cache"),
"WLR_BACKENDS": "headless",
"WLR_RENDERER": "pixman",
"WLR_NO_HARDWARE_CURSORS": "1",
"LANG": "C.UTF-8",
"TZ": "UTC",
}
log_path = home / "desktop-output.log"
with log_path.open("w") as log:
child = subprocess.Popen(
["cage", "--", desktop, "--no-sandbox", "--disable-gpu", "--ozone-platform=wayland"],
cwd=home, env=env, stdout=log, stderr=subprocess.STDOUT,
start_new_session=True,
)
try:
deadline = time.monotonic() + 90
while time.monotonic() < deadline:
output = log_path.read_text()
desktop_log = hermes_home / "logs" / "desktop.log"
if desktop_log.exists():
output += desktop_log.read_text()
assert "WRONG_BACKEND_SELECTED" not in output, output
assert child.poll() is None, output
if "HERMES_BACKEND_READY port=" in output:
assert f"existing Hermes CLI at {expected}" in output, output
print("PASS: packaged desktop selected its pinned backend over the mutable install")
break
time.sleep(0.1)
else:
raise AssertionError(f"Desktop did not start its backend:\n{log_path.read_text()}")
finally:
os.killpg(child.pid, signal.SIGTERM)
try:
child.wait(timeout=15)
except subprocess.TimeoutExpired:
os.killpg(child.pid, signal.SIGKILL)
child.wait(timeout=5)
# The desktop spawns its backend in its own session (hermes serve outlives a
# window close on purpose), so killing cage's group leaves that gateway writing
# under HERMES_HOME while the tempdir is removed. Stop everything still rooted
# in this home before cleanup; the sandbox has no other processes to confuse.
survivors = _processes_under(home)
for pid in survivors:
with contextlib.suppress(ProcessLookupError, PermissionError):
os.kill(pid, signal.SIGTERM)
deadline = time.monotonic() + 15
while survivors and time.monotonic() < deadline:
time.sleep(0.2)
survivors = _processes_under(home)
for pid in survivors:
with contextlib.suppress(ProcessLookupError, PermissionError):
os.kill(pid, signal.SIGKILL)