test: capture PM desktop launches under isolated interpreter

This commit is contained in:
ethernet
2026-09-24 04:35:14 -04:00
parent 46e9f8746f
commit f74ae0d862
5 changed files with 121 additions and 8 deletions

View File

@@ -147,7 +147,7 @@ A leg can install a release from months back. The driver must not assume that th
The desktop app has two launch paths, so the matrix has two app-update methods. Both click "Update now" in the running app. They differ in how the app starts:
- `open-app-update`: the app starts from the installed app entry point. On Windows, both the desktop installer and `installer-script+desktop` create shortcuts, so both support this route. On Linux and macOS, the script's opt-in desktop stage builds inside the checkout without registering an OS entry point. The macOS route therefore requires a desktop-installer install; Linux has no open-app-update leg.
- `hermes-desktop-app-update`: the app starts with the `hermes desktop` command. Every install method provides this command, on each OS that ships the desktop app. On linux this is the only app surface: no desktop installer and no packaged desktop artifact exist for linux. The driver captures the product's own launch call (argv, cwd, environment) with `e2e-assets/launch-capture/sitecustomize.py` and re-executes it under Playwright, which owns the app and clicks the update flow.
- `hermes-desktop-app-update`: the app starts with the `hermes desktop` command. Every install method provides this command, on each OS that ships the desktop app. On linux this is the only app surface: no desktop installer and no packaged desktop artifact exist for linux. The driver captures the product's own launch call (argv, cwd, environment) with `e2e-assets/launch-capture/sitecustomize.py` and re-executes it under Playwright, which owns the app and clicks the update flow. Pre-PM console scripts load the capture hook via `PYTHONPATH`; PM launchers use `-I`, so `launch-capture/pm-launch.py` obtains the installed launcher's own isolated runtime command and loads the driver hook before its bootstrap.
## Skips

View File

@@ -0,0 +1,35 @@
"""Run an installed PM CLI with driver-side launch capture under its own -I interpreter.
The published launcher exposes its installation-bound command. Inject only the
capture module ahead of that command's bootstrap; keep its selected interpreter,
isolated flag, bootstrap, and argv instead of constructing a new product launch.
"""
import json
import os
from pathlib import Path
import subprocess
import sys
def main() -> int:
launcher = sys.argv[1]
spec = sys.argv[2]
query = subprocess.run(
[launcher, "--print-runtime-command", "--", "desktop"],
check=True, capture_output=True, text=True,
)
command = json.loads(query.stdout)
if (not isinstance(command, list) or len(command) != 5
or command[1:3] != ["-I", "-c"] or command[4] != "desktop"
or not all(isinstance(part, str) for part in command)):
raise ValueError("installed PM launcher did not provide an isolated desktop command")
capture = Path(__file__).with_name("sitecustomize.py")
command[3] = f"import runpy; runpy.run_path({str(capture)!r}); " + command[3]
env = os.environ.copy()
env["HERMES_E2E_CAPTURE_LAUNCH"] = spec
return subprocess.run(command, env=env).returncode
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1,8 +1,9 @@
"""Driver-side spawn interception for hermes desktop E2E legs.
The installed ``hermes`` is a venv console script, so its interpreter
imports ``sitecustomize`` at startup when this directory is on
``PYTHONPATH``. Behind an explicit env-var opt-in the module wraps
Pre-PM ``hermes`` is a venv console script that imports this module at
startup via ``PYTHONPATH``. PM launchers use -I and instead load it via
the driver-side pm-launch.py before the installed bootstrap. With an
explicit env-var opt-in the module wraps
``subprocess.run`` so the FINAL electron launch call of ``hermes
desktop`` is captured -- argv, cwd, and the fully-constructed ``env``
kwarg written to a JSON spec -- and replaced with a fake success instead

View File

@@ -473,10 +473,19 @@ case "$UPDATE_METHOD" in
step "capturing the hermes desktop launch spec (build runs for real)"
rc=0
(cd "$INSTALL_DIR" && \
PYTHONPATH="$ASSETS/launch-capture${PYTHONPATH:+:$PYTHONPATH}" \
HERMES_E2E_CAPTURE_LAUNCH="$SPEC" \
source_build_env "$HERMES" desktop < /dev/null 2>&1 | ts_prefix > "$LOG_DIR/desktop-launch-capture.log") || rc=$?
if [ "$HERMES" = "$INSTALL_DIR/.hermes/bin/hermes" ]; then
# The PM launcher uses -I: PYTHONPATH/sitecustomize cannot reach it.
# Ask the installed launcher for its own isolated command, then inject
# the driver hook into that command without changing product code.
(cd "$INSTALL_DIR" && source_build_env python3 -I "$ASSETS/launch-capture/pm-launch.py" \
"$HERMES" "$SPEC" < /dev/null 2>&1 | ts_prefix > "$LOG_DIR/desktop-launch-capture.log") || rc=$?
else
# Pre-PM console scripts load sitecustomize from PYTHONPATH.
(cd "$INSTALL_DIR" && \
PYTHONPATH="$ASSETS/launch-capture${PYTHONPATH:+:$PYTHONPATH}" \
HERMES_E2E_CAPTURE_LAUNCH="$SPEC" \
source_build_env "$HERMES" desktop < /dev/null 2>&1 | ts_prefix > "$LOG_DIR/desktop-launch-capture.log") || rc=$?
fi
log_group "hermes desktop (launch capture) transcript" "$LOG_DIR/desktop-launch-capture.log"
[ "$rc" -eq 0 ] || fail "hermes desktop exited $rc during launch capture; transcript above"
# Exit 0 without a capture means a version that never reached its

View File

@@ -0,0 +1,68 @@
"""The installer E2E's capture must survive the installed PM launcher's -I."""
import json
import os
from pathlib import Path
import shlex
import subprocess
import sys
import pytest
@pytest.mark.platforms("posix")
def test_isolated_pm_launch_captures_only_final_electron_spawn(tmp_path):
root = Path(__file__).resolve().parents[3]
helper = root / "tests/install/e2e-assets/launch-capture/pm-launch.py"
launcher = tmp_path / "hermes"
app = tmp_path / "app.py"
app.write_text(
"import os, subprocess, sys\n"
"assert sys.flags.isolated == 1\n"
"assert os.environ.get('PYTHONPATH') is None\n"
"assert sys.argv[1:] == ['desktop']\n"
"subprocess.run(['npm', 'run', 'build'], check=True)\n"
"subprocess.run(['npm', 'exec', '--', 'electron', '.'], "
"cwd=os.getcwd(), env={**os.environ, 'PRODUCT_SENTINEL': 'present'}, check=True)\n",
encoding="utf-8",
)
# Query emulates the published --print-runtime-command interface. The
# returned command executes under a REAL isolated Python, not a patched
# subprocess; the fake npm command proves builds still pass through.
code = ("import os, runpy; os.environ.pop('PYTHONPATH', None); "
f"runpy.run_path({str(app)!r}, run_name='__main__')")
command = [sys.executable, "-I", "-c", code, "desktop"]
launcher.write_text(
"#!/bin/sh\n"
"[ \"$1\" = --print-runtime-command ] || exit 90\n"
f"printf '%s\\n' {shlex.quote(json.dumps(command))}\n",
encoding="utf-8",
)
launcher.chmod(0o755)
fake_npm = tmp_path / "npm"
fake_npm.write_text(
"#!/bin/sh\n"
"[ \"$1 $2 $3\" = 'run build ' ] || exit 89\n"
f"touch {shlex.quote(str(tmp_path / 'build-ran'))}\n",
encoding="utf-8",
)
fake_npm.chmod(0o755)
evil = tmp_path / "evil"
evil.mkdir()
(evil / "sitecustomize.py").write_text(
f"open({str(tmp_path / 'ambient-hook-ran')!r}, 'w').close()\n", encoding="utf-8",
)
spec = tmp_path / "launch.json"
env = {**os.environ, "PATH": str(tmp_path) + os.pathsep + os.environ["PATH"],
"PYTHONPATH": str(evil)}
result = subprocess.run([sys.executable, "-I", str(helper), str(launcher), str(spec)],
cwd=tmp_path, env=env, capture_output=True, text=True)
assert result.returncode == 0, result.stderr
assert (tmp_path / "build-ran").is_file()
assert not (tmp_path / "ambient-hook-ran").exists()
assert spec.with_name(spec.name + ".captured").read_text() == "source"
captured = json.loads(spec.read_text())
assert captured["argv"] == ["npm", "exec", "--", "electron", "."]
assert captured["cwd"] == str(tmp_path)
assert captured["env"]["PRODUCT_SENTINEL"] == "present"
assert captured["matchedShape"] == "source"