From 16da7f1b381b067a5851fb1ef7776eaca4f7abc2 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 26 Sep 2026 18:37:15 -0700 Subject: [PATCH] test(e2e/windows): real C:\Users profiles, serialized gateway phases, Git-for-Windows machines --- .../workflows/windows-install-update-e2e.yml | 12 +- scripts/run_tests.sh | 7 +- tests/e2e/core/windows_update/_machine.py | 114 ++++++++++++++---- .../test_fresh_install_update.py | 17 +-- .../test_gateway_across_update.py | 48 ++++---- .../windows_update/test_git_and_holders.py | 9 +- .../core/windows_update/test_interpreter.py | 14 ++- .../windows_update/test_paths_and_acls.py | 4 +- 8 files changed, 157 insertions(+), 68 deletions(-) diff --git a/.github/workflows/windows-install-update-e2e.yml b/.github/workflows/windows-install-update-e2e.yml index 0f78b97c61..eb16d35db4 100644 --- a/.github/workflows/windows-install-update-e2e.yml +++ b/.github/workflows/windows-install-update-e2e.yml @@ -50,9 +50,12 @@ jobs: -m "platforms and integration" -v --tb=short -rA env: HERMES_E2E_WINDOWS_INSTALL: '1' - # Short machine roots: a real install lives at C:\Users\\AppData\Local\hermes; - # pytest's tmp dir would add ~100 characters before node_modules. + # Machine work dirs (serve.git, transcripts) stay short under runner.temp. The fake + # user profiles live in C:\Users itself, where a real one does: the checkout carries + # 159-character paths, so a deeper profile hits MAX_PATH where no user would (the + # runner is disposable, so writing C:\Users is fine). HERMES_E2E_MACHINE_ROOT: ${{ runner.temp }}\wm + HERMES_E2E_PROFILES_ROOT: C:\Users HERMES_E2E_ARTIFACTS: ${{ runner.temp }}\win-update-e2e # One journey per file, all in parallel (each is mostly network + subprocess wait). HERMES_TEST_WORKERS: '6' @@ -77,11 +80,14 @@ jobs: shell: pwsh run: | $names = 'python.exe', 'pythonw.exe', 'hermes.exe', 'node.exe' + # Work dirs live under runner.temp\wm; profiles are C:\Users\*hermes-e2e-. $root = "${{ runner.temp }}\wm".ToLower() + $profileMark = 'hermes-e2e-' $deadline = (Get-Date).AddSeconds(20) do { $left = @(Get-CimInstance Win32_Process | Where-Object { - ($names -contains $_.Name.ToLower()) -and ("$($_.CommandLine) $($_.ExecutablePath)".ToLower().Contains($root)) + $blob = "$($_.CommandLine) $($_.ExecutablePath)".ToLower() + ($names -contains $_.Name.ToLower()) -and ($blob.Contains($root) -or $blob.Contains($profileMark)) }) if ($left.Count -eq 0) { 'no leftover processes from the E2E machines'; exit 0 } Start-Sleep -Milliseconds 500 diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 24f4422302..8fcc4294f4 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -145,8 +145,9 @@ _pf86="$(env | sed -n 's/^ProgramFiles(x86)=//p' | head -n1)" # * CI / GITHUB_ACTIONS tell suites they run on a disposable runner (e.g. # tests/e2e/core/upgrade runs the real updater unsandboxed only there). # * HERMES_E2E_WINDOWS_INSTALL opts tests/e2e/core/windows_update into running -# the real install.ps1 (it writes HKCU PATH); HERMES_E2E_MACHINE_ROOT and -# HERMES_E2E_ARTIFACTS place its fake machines and the transcripts CI uploads. +# the real install.ps1 (it writes HKCU PATH); HERMES_E2E_MACHINE_ROOT, +# HERMES_E2E_PROFILES_ROOT and HERMES_E2E_ARTIFACTS place its fake machines, +# their user profiles and the transcripts CI uploads. # # These are test-infrastructure knobs, not credentials — same class as the # HERMES_RUN_SLOW_PET_TESTS / HERMES_E2E_BROWSER / HERMES_RUN_E2E opt-ins @@ -160,7 +161,7 @@ TEST_ENV=() for _test_var in HERMES_TEST_IMAGE HERMES_TEST_WORKERS HERMES_TEST_PATHS \ HERMES_TEST_FILE_TIMEOUT HERMES_TEST_FILE_RETRIES HERMES_TEST_SLICE \ SSL_CERT_FILE SSL_CERT_DIR HERMES_GATEWAY_LOCK_DIR HERMES_E2E_REQUIRE_TUI CI GITHUB_ACTIONS \ - HERMES_E2E_WINDOWS_INSTALL HERMES_E2E_MACHINE_ROOT HERMES_E2E_ARTIFACTS; do + HERMES_E2E_WINDOWS_INSTALL HERMES_E2E_MACHINE_ROOT HERMES_E2E_PROFILES_ROOT HERMES_E2E_ARTIFACTS; do if [ -n "${!_test_var:-}" ]; then TEST_ENV+=("$_test_var=${!_test_var}") fi diff --git a/tests/e2e/core/windows_update/_machine.py b/tests/e2e/core/windows_update/_machine.py index 1658561edc..c76e7a4db5 100644 --- a/tests/e2e/core/windows_update/_machine.py +++ b/tests/e2e/core/windows_update/_machine.py @@ -20,11 +20,17 @@ Only external edges are replaced (tests/install/README.md, "The isolation trick" Tool and dependency downloads (uv, the managed Python, wheels, Node) use the network, exactly like the real installer. -Each machine is a fake user profile under pytest's tmp dir: ``USERPROFILE``/``HOME``/ -``LOCALAPPDATA``/``APPDATA`` point inside it and ``HERMES_HOME`` is NOT set, so the -installer and every ``hermes`` command resolve the default ``%LOCALAPPDATA%\\hermes`` -the way a real user's do. The installer also prepends its bin dir to the user PATH in -HKCU; the machine restores that value on teardown. +Each machine is a fake user profile: ``USERPROFILE``/``HOME``/``LOCALAPPDATA``/``APPDATA`` +point inside it and ``HERMES_HOME`` is NOT set, so the installer and every ``hermes`` +command resolve the default ``%LOCALAPPDATA%\\hermes`` the way a real user's do. CI +puts the profiles in ``C:\\Users`` itself (``HERMES_E2E_PROFILES_ROOT``): the checkout +carries 159-character paths, so a profile any deeper than a real one would hit MAX_PATH +where no user does. The installer also prepends its bin dir to the user PATH in HKCU; +the machine restores that value on teardown. + +Machines run in parallel, but ``hermes update`` on Windows pauses every gateway on the +host, including other installs' (#124659). Updates and gateway lifetimes therefore take +a job-wide lock (``gateway_phase``); installs, turns and everything else stay parallel. The suite mutates HKCU and downloads a toolchain per machine, so it only runs where ``HERMES_E2E_WINDOWS_INSTALL=1`` (the CI job sets it). @@ -32,11 +38,12 @@ The suite mutates HKCU and downloads a toolchain per machine, so it only runs wh from __future__ import annotations +import contextlib import json import os import shutil import subprocess -import sys +import tempfile import time import uuid from dataclasses import dataclass, field @@ -135,6 +142,10 @@ class Machine: root: Path profile_name: str base_url: str + profiles_root: Path | None = None + # Git for Windows on PATH (a typical developer box). False: a clean machine, where the + # installer must stage its own pinned Git. + system_git: bool = False path_prepend: list[str] = field(default_factory=list) head: str = "" next: str = "" @@ -142,12 +153,14 @@ class Machine: _hkcu: Any = None _seq: int = 0 _spawned: list[subprocess.Popen] = field(default_factory=list) + _lock_depth: int = 0 + timings: list[tuple[str, float]] = field(default_factory=list) # -- layout --------------------------------------------------------------- @property def profile(self) -> Path: - return self.root / "Users" / self.profile_name + return (self.profiles_root or self.root / "Users") / self.profile_name @property def local(self) -> Path: @@ -183,7 +196,8 @@ class Machine: roaming = self.profile / "AppData" / "Roaming" self.local.mkdir(parents=True, exist_ok=True) roaming.mkdir(parents=True, exist_ok=True) - path = _strip_path(os.environ.get("PATH", ""), ("git.exe", "python.exe", "python3.exe", "uv.exe")) + drop = ("python.exe", "python3.exe", "uv.exe") + (() if self.system_git else ("git.exe",)) + path = _strip_path(os.environ.get("PATH", ""), drop) env.update({ "PATH": os.pathsep.join([*self.path_prepend, path]), "USERPROFILE": str(self.profile), @@ -253,11 +267,13 @@ class Machine: """Run to completion with the full transcript on disk (survives a timeout).""" self._seq += 1 log = self.logs / f"{self._seq:02d}-{label}.log" + started = time.monotonic() with log.open("wb") as fh: proc = subprocess.Popen(argv, cwd=cwd or self.profile, env=self.env(env_extra), stdin=subprocess.DEVNULL, stdout=fh, stderr=subprocess.STDOUT) try: code = proc.wait(timeout=timeout) + self.timings.append((label, round(time.monotonic() - started, 1))) except subprocess.TimeoutExpired: subprocess.run(["taskkill", "/PID", str(proc.pid), "/T", "/F"], capture_output=True, timeout=60) proc.wait(timeout=60) @@ -290,7 +306,46 @@ class Machine: return self._run_logged([str(self.hermes_exe), *args], name, timeout=timeout, env_extra=env_extra) def update(self, *extra: str, label: str = "update") -> Run: - return self.hermes("update", "--yes", *extra, label=label, timeout=UPDATE_TIMEOUT) + with self.gateway_phase(): + return self.hermes("update", "--yes", *extra, label=label, timeout=UPDATE_TIMEOUT) + + @contextlib.contextmanager + def gateway_phase(self): + """Job-wide mutex for updates and gateway lifetimes (reentrant within a machine). + + ``hermes update`` on Windows discovers gateways host-wide and stops ones it cannot map + to its own profiles (#124659), so one machine's update would kill another machine's + gateway. Real users rarely run two installs side by side; this suite always does.""" + if self._lock_depth: + self._lock_depth += 1 + try: + yield + finally: + self._lock_depth -= 1 + return + import msvcrt + + lock_dir = Path(os.environ.get("HERMES_E2E_MACHINE_ROOT") or tempfile.gettempdir()) + lock_dir.mkdir(parents=True, exist_ok=True) + waited = time.monotonic() + with (lock_dir / "gateway-phase.lock").open("a+b") as fh: + while True: + try: + fh.seek(0) + msvcrt.locking(fh.fileno(), msvcrt.LK_NBLCK, 1) + break + except OSError: + if time.monotonic() - waited > 1800: + raise RuntimeError("harness: gateway-phase lock not acquired within 30 min") from None + time.sleep(0.5) + self.timings.append(("(waited for gateway phase)", round(time.monotonic() - waited, 1))) + self._lock_depth = 1 + try: + yield + finally: + self._lock_depth = 0 + fh.seek(0) + msvcrt.locking(fh.fileno(), msvcrt.LK_UNLCK, 1) def installed_head(self) -> str: try: @@ -348,7 +403,7 @@ class Machine: """Live processes of this machine: exe/cwd/argv under its root, or its HERMES_HOME.""" import psutil - root = os.path.normcase(os.path.normpath(str(self.root))) + roots = {os.path.normcase(os.path.normpath(str(p))) for p in (self.root, self.profile)} home = os.path.normcase(os.path.normpath(str(self.hermes_home))) me = os.getpid() owned = [] @@ -361,14 +416,14 @@ class Machine: blob = os.path.normcase(" ".join([proc.exe() or "", proc.cwd() or "", *proc.cmdline()])) except (psutil.Error, OSError): continue - if hh == home or root in blob: + if hh == home or any(root in blob for root in roots): owned.append(proc) return owned def evidence(self) -> str: """Receipts, logs and the process table: what a failure message must carry.""" - parts = [f"machine root: {self.root}", f"HEAD={self.head} NEXT={self.next}", - f"installed checkout: {self.installed_head()}"] + parts = [f"machine root: {self.root}", f"profile: {self.profile}", f"HEAD={self.head} NEXT={self.next}", + f"installed checkout: {self.installed_head()}", f"timings (s): {self.timings}"] receipt = self.hermes_home / "logs" / "update_receipts" / "latest.json" if receipt.is_file(): parts.append(self._tail(receipt, 4000)) @@ -385,11 +440,15 @@ class Machine: parts.append(f"process table unavailable: {exc}") return "\n".join(parts) - def teardown(self) -> None: + def kill_owned(self) -> None: + """Hard-stop every live process of this machine (end of a gateway phase, teardown).""" kill_tree(self.owned_processes()) for proc in self._spawned: if proc.poll() is None: subprocess.run(["taskkill", "/PID", str(proc.pid), "/T", "/F"], capture_output=True, timeout=60) + + def teardown(self) -> None: + self.kill_owned() try: _restore_hkcu_path(self._hkcu) except OSError: @@ -404,17 +463,20 @@ class Machine: shutil.copytree(src, dest / sub, dirs_exist_ok=True, ignore=shutil.ignore_patterns("*.db", "*.db-*")) (dest / "evidence.txt").write_text(self.evidence(), encoding="utf-8", errors="replace") + (dest / "timings.json").write_text(json.dumps(self.timings, indent=1), encoding="utf-8") -def new_machine(tmp_root: Path, base_url: str, *, label: str, profile_name: str = "e2e", - path_prepend: list[str] | None = None) -> Machine: - """A staged machine. ``HERMES_E2E_MACHINE_ROOT`` (CI: a short runner.temp dir) keeps the - install tree near the depth of a real ``C:\\Users\\\\AppData\\Local\\hermes``; - pytest's own tmp dir is already ~100 characters deep on Windows, and node_modules does - the rest of the way to MAX_PATH.""" +def new_machine(tmp_root: Path, base_url: str, *, label: str, person: str = "", + system_git: bool = False) -> Machine: + """A staged machine. Its work dir (serve.git, transcripts) lives under + ``HERMES_E2E_MACHINE_ROOT`` (else ``tmp_root``); its user profile under + ``HERMES_E2E_PROFILES_ROOT`` (CI: ``C:\\Users``), named ``[ ]hermes-e2e-``.""" + sfx = uuid.uuid4().hex[:4] base = Path(os.environ.get("HERMES_E2E_MACHINE_ROOT") or tmp_root) - machine = Machine(root=base / f"{label}-{uuid.uuid4().hex[:4]}", profile_name=profile_name, - base_url=base_url, path_prepend=list(path_prepend or [])) + profiles = os.environ.get("HERMES_E2E_PROFILES_ROOT") + name = f"{person} hermes-e2e-{sfx}" if person else f"hermes-e2e-{sfx}" + machine = Machine(root=base / f"{label}-{sfx}", profile_name=name, base_url=base_url, + profiles_root=Path(profiles) if profiles else None, system_git=system_git) machine.stage() return machine @@ -454,6 +516,14 @@ class Journey: raise RuntimeError(fail_with(self.machine, f"{name}: {message}", run)) +def failure_line(run: Run) -> str: + """The first line a ``hermes`` command printed as its failure (``✗ ...``), else ``""``.""" + for line in run.stdout.splitlines(): + if line.strip().startswith("✗"): + return line.strip() + return "" + + def fail_with(machine: Machine, message: str, run: Run | None = None) -> str: """Assertion text: the claim first (gates match on it), then the transcript and evidence.""" tail = f"\n--- transcript rc={run.returncode} ---\n{run.stdout[-6000:]}" if run is not None else "" diff --git a/tests/e2e/core/windows_update/test_fresh_install_update.py b/tests/e2e/core/windows_update/test_fresh_install_update.py index a3652e1a7d..dd5565db5c 100644 --- a/tests/e2e/core/windows_update/test_fresh_install_update.py +++ b/tests/e2e/core/windows_update/test_fresh_install_update.py @@ -19,6 +19,7 @@ from tests.e2e.core.windows_update._machine import ( REQUIRES_OPT_IN, Journey, fail_with, + failure_line, new_machine, one_shot_turn, source_completion_detour, @@ -29,10 +30,10 @@ pytestmark = [pytest.mark.platforms("windows"), pytest.mark.integration, pytest.mark.live_system_guard_bypass, REQUIRES_OPT_IN] KNOWN = { - "first_launch": ( - r"^first agent launch after a pristine install detoured through source-update completion", - "gated on #123314: a fresh Windows source install enters source-update completion on the " - "first agent launch", + "update": ( + r"^hermes update exited 1: ✗ Update failed: \[WinError 2\]", + "gated on #124634: on a machine without system Git, the pinned Git that install.ps1 " + "staged never reaches `hermes update`'s PATH", ), } @@ -71,10 +72,9 @@ def test_install_lands_on_head_and_publishes_hermes(journey: Journey) -> None: def test_first_agent_launch_runs_the_turn(journey: Journey) -> None: m, turn = journey.machine, journey["first_turn"] detour = source_completion_detour(turn.run) - with known_gate(KNOWN, "first_launch"): - assert detour is None, fail_with( + assert detour is None, fail_with( m, f"first agent launch after a pristine install detoured through source-update completion " - f"(printed {detour!r})", turn.run) + f"(printed {detour!r})", turn.run) assert turn.ok, fail_with( m, f"first agent launch did not complete a turn (reply printed={turn.reply_id in turn.run.stdout}, " f"prompt reached provider={turn.reached_wire})", turn.run) @@ -82,7 +82,8 @@ def test_first_agent_launch_runs_the_turn(journey: Journey) -> None: def test_update_moves_checkout_to_next(journey: Journey) -> None: m, run = journey.machine, journey["update"] - assert run.returncode == 0, fail_with(m, f"hermes update exited {run.returncode}", run) + with known_gate(KNOWN, "update"): + assert run.returncode == 0, fail_with(m, f"hermes update exited {run.returncode}: {failure_line(run)}", run) head = m.installed_head() assert head == m.next, fail_with(m, f"after hermes update the checkout is at {head}, expected NEXT {m.next}", run) assert (m.install_dir / NEXT_MARKER).is_file(), fail_with(m, "NEXT's marker file is missing from the checkout", run) diff --git a/tests/e2e/core/windows_update/test_gateway_across_update.py b/tests/e2e/core/windows_update/test_gateway_across_update.py index f0fd9e15df..d86503669b 100644 --- a/tests/e2e/core/windows_update/test_gateway_across_update.py +++ b/tests/e2e/core/windows_update/test_gateway_across_update.py @@ -53,10 +53,18 @@ def _alive(pid: int) -> bool: return False +def _exits(pid: int, timeout: float) -> bool: + try: + wait_until(lambda: not _alive(pid), timeout, f"gateway pid {pid} to exit", interval=0.5) + except AssertionError: + return False + return True + + @pytest.fixture(scope="module") def journey(tmp_path_factory): with FakeLLMServer() as srv: - machine = new_machine(tmp_path_factory.mktemp("gw"), srv.base_url, label="gw") + machine = new_machine(tmp_path_factory.mktemp("gw"), srv.base_url, label="gw", system_git=True) j = Journey(machine) try: install = j.step("install", machine.install) @@ -64,18 +72,22 @@ def journey(tmp_path_factory): j.step("install_ok", lambda: j.require("install", install.returncode == 0, f"install.ps1 exited {install.returncode}", install)) if j.ok("install_ok"): - j.step("spawn", machine.spawn_gateway) - before = j.step("state_before", machine.wait_gateway_running) - j.step("status_before", lambda: machine.hermes("gateway", "status", label="status-before")) - machine.advance() - j.step("update", machine.update) - old_pid = int(before.get("pid") or 0) if isinstance(before, dict) else None - j.step("state_after", lambda: machine.wait_gateway_running(not_pid=old_pid)) - j.step("status_after", lambda: machine.hermes("gateway", "status", label="status-after")) - j.step("pidfile_after", lambda: (machine.hermes_home / "gateway.pid").exists()) - j.step("update_again", lambda: machine.update(label="update-again")) - j.step("state_before_stop", machine.gateway_state) - j.step("stop", lambda: machine.hermes("gateway", "stop", label="stop")) + with machine.gateway_phase(): + j.step("spawn", machine.spawn_gateway) + before = j.step("state_before", machine.wait_gateway_running) + j.step("status_before", lambda: machine.hermes("gateway", "status", label="status-before")) + machine.advance() + j.step("update", machine.update) + old_pid = int(before.get("pid") or 0) if isinstance(before, dict) else None + j.step("state_after", lambda: machine.wait_gateway_running(not_pid=old_pid)) + j.step("status_after", lambda: machine.hermes("gateway", "status", label="status-after")) + j.step("pidfile_after", lambda: (machine.hermes_home / "gateway.pid").exists()) + j.step("update_again", lambda: machine.update(label="update-again")) + last = j.step("state_before_stop", machine.gateway_state) + j.step("stop", lambda: machine.hermes("gateway", "stop", label="stop")) + last_pid = int(last.get("pid") or 0) if isinstance(last, dict) else 0 + j.step("stopped", lambda: _exits(last_pid, 90)) + machine.kill_owned() # nothing of this machine outlives its gateway phase yield j finally: machine.teardown() @@ -100,8 +112,7 @@ def test_update_with_running_gateway_succeeds(journey: Journey) -> None: def test_gateway_serves_next_after_update(journey: Journey) -> None: m, state = journey.machine, journey["state_after"] - pid, sha = int(state.get("pid") or 0), str(state.get("code_sha") or "") - assert _alive(pid), fail_with(m, f"the relaunched gateway pid {pid} is not alive") + sha = str(state.get("code_sha") or "") # state_after only resolves for a live, running pid assert len(sha) >= 7 and m.next.startswith(sha), fail_with( m, f"the relaunched gateway serves code_sha={sha!r}, expected NEXT {m.next}") @@ -130,12 +141,7 @@ def test_gateway_stop_after_update(journey: Journey) -> None: m, stop, state = journey.machine, journey["stop"], journey["state_before_stop"] pid = int(state.get("pid") or 0) assert pid, fail_with(m, f"no gateway recorded before stop: {state}") - try: - wait_until(lambda: not _alive(pid), 90, f"gateway pid {pid} to exit after stop", interval=0.5) - stopped = True - except AssertionError: - stopped = False with known_gate(KNOWN, "stop"): - assert stopped, fail_with( + assert journey["stopped"], fail_with( m, f"after update `hermes gateway stop` left the serving gateway (pid {pid}) running", stop) assert stop.returncode == 0, fail_with(m, f"hermes gateway stop exited {stop.returncode}", stop) diff --git a/tests/e2e/core/windows_update/test_git_and_holders.py b/tests/e2e/core/windows_update/test_git_and_holders.py index fe7430d955..ec4d57775d 100644 --- a/tests/e2e/core/windows_update/test_git_and_holders.py +++ b/tests/e2e/core/windows_update/test_git_and_holders.py @@ -26,6 +26,7 @@ from tests.e2e.core.windows_update._machine import ( Journey, Machine, fail_with, + failure_line, new_machine, ) from tests.fakes.fake_llm_provider import FakeLLMServer @@ -34,8 +35,9 @@ pytestmark = [pytest.mark.platforms("windows"), pytest.mark.integration, pytest.mark.live_system_guard_bypass, REQUIRES_OPT_IN] KNOWN = { - "partial_fetch": (r"^hermes update over the installer's partial clone failed: .*BUG: builtin/pack-objects", - "gated on #124323: the pinned Git's pack-objects BUG on the --filter=tree:0 clone"), + "partial_fetch": (r"^hermes update over the installer's partial clone failed: rc=1, .*" + r"✗ Update failed: \[WinError 2\]", + "gated on #124634: the pinned Git install.ps1 staged never reaches `hermes update`'s PATH"), "holders": (r"^`hermes update --list-venv-holders` did not report the live venv holders .*" r"\(rc=0, reported pids \[\]\)", "gated on #123050: --list-venv-holders reads the retired hermes_cli.main stub and always " @@ -108,7 +110,8 @@ def test_update_fetches_into_the_installers_partial_clone(journey: Journey) -> N m, run, flt = journey.machine, journey["update"], journey["partial_filter"] assert flt, fail_with(m, "harness: the installer's clone is not partial (no remote.origin.partialclonefilter); " "serve.git should allow filters") - fetch_bug = next((ln.strip() for ln in run.stdout.splitlines() if "BUG:" in ln or "fatal:" in ln), "") + fetch_bug = next((ln.strip() for ln in run.stdout.splitlines() if "BUG:" in ln or "fatal:" in ln), + failure_line(run)) with known_gate(KNOWN, "partial_fetch"): assert run.returncode == 0 and m.installed_head() == m.next, fail_with( m, f"hermes update over the installer's partial clone failed: rc={run.returncode}, checkout at " diff --git a/tests/e2e/core/windows_update/test_interpreter.py b/tests/e2e/core/windows_update/test_interpreter.py index 3cdb7b00f3..b36864c9b0 100644 --- a/tests/e2e/core/windows_update/test_interpreter.py +++ b/tests/e2e/core/windows_update/test_interpreter.py @@ -84,7 +84,7 @@ def _under(path: str, root: Path) -> bool: def journey(tmp_path_factory): system_python = _system_python_dir() with FakeLLMServer() as srv: - machine = new_machine(tmp_path_factory.mktemp("interp"), srv.base_url, label="interp") + machine = new_machine(tmp_path_factory.mktemp("interp"), srv.base_url, label="interp", system_git=True) j = Journey(machine) try: install = j.step("install", machine.install) @@ -103,11 +103,13 @@ def journey(tmp_path_factory): j.step("update_ok", lambda: j.require("update", update.returncode == 0, f"hermes update exited {update.returncode}", update)) if j.ok("update_ok"): - j.step("spawn", machine.spawn_gateway) - state = j.step("state", machine.wait_gateway_running) - if j.ok("state"): - time.sleep(15) # supervised workers start ~2 s after boot; give them room to die - j.step("gateway_proc", lambda: _inspect(int(state["pid"]))) + with machine.gateway_phase(): + j.step("spawn", machine.spawn_gateway) + state = j.step("state", machine.wait_gateway_running) + if j.ok("state"): + time.sleep(15) # supervised workers start ~2 s after boot; give them room to die + j.step("gateway_proc", lambda: _inspect(int(state["pid"]))) + machine.kill_owned() # nothing of this machine outlives its gateway phase j.step("turn", lambda: one_shot_turn(machine, srv, "turn-stale-venv")) yield j finally: diff --git a/tests/e2e/core/windows_update/test_paths_and_acls.py b/tests/e2e/core/windows_update/test_paths_and_acls.py index 2b2daeb430..60831275fa 100644 --- a/tests/e2e/core/windows_update/test_paths_and_acls.py +++ b/tests/e2e/core/windows_update/test_paths_and_acls.py @@ -32,7 +32,7 @@ from tests.fakes.fake_llm_provider import FakeLLMServer pytestmark = [pytest.mark.platforms("windows"), pytest.mark.integration, pytest.mark.live_system_guard_bypass, REQUIRES_OPT_IN] -PROFILE_NAME = "Jörg Ñúñez e2e" +PERSON = "Jörg Ñúñez" # the profile is "Jörg Ñúñez hermes-e2e-" KNOWN = { "install": (r"^install\.ps1 failed for a profile path with non-ASCII characters and spaces: .*" r"is not recognized as the name of a cmdlet", @@ -138,7 +138,7 @@ def _tool_runs(hermes_home: Path) -> list[tuple[Path, int | None, str]]: def journey(tmp_path_factory): with FakeLLMServer() as srv: machine = new_machine(tmp_path_factory.mktemp("paths"), srv.base_url, label="paths", - profile_name=PROFILE_NAME) + person=PERSON, system_git=True) j = Journey(machine) try: install = j.step("install", machine.install)