Files
hermes-agent/tests/e2e/core/_pm_dependencies.py
ethernet fcd4e40dfc ci(e2e): provision node through PM so /api/pty can start the TUI
The dashboard PTY test failed on this branch with "node: not installed and
lazy installs are disabled". On this branch _make_tui_argv asks PM for node
(hermes_cli/main_tui_launch.py _tui_node_bin). The E2E sandbox forbids lazy
installs (HERMES_DISABLE_LAZY_INSTALLS=1 in tests/e2e/core/dashboard
hermetic_env). It gets node only when the runner's PM store has it
(tests/e2e/core/_pm_dependencies.py). The e2e job got node from
actions/setup-node, so node was on PATH but not in the PM store. main passes
the test because main's TUI launch takes node from PATH.

Install the locked toolchain (setup-pm toolchain: all) before npm ci, in
place of setup-node. The locked Node 26 also satisfies the boot-contract
suite. When HERMES_E2E_REQUIRE_TUI=1, the fixture now fails at once if the
store has no node, not later with an opaque 1011 close.
2026-09-24 16:49:58 -04:00

97 lines
4.2 KiB
Python

"""Select prepared test dependencies for subprocesses with isolated E2E homes."""
from __future__ import annotations
import atexit
import json
import os
import shutil
import sys
import tempfile
from pathlib import Path
from pm.build_operations import verified_tools
from pm.environments import install_key, site_packages
from pm.lock import Facts, Lockfile
from pm.paths import lockfile_path
from pm.store import current_target
def _prepare_test_tools() -> tuple[Path, dict] | None:
"""Verify the PM store before per-test home-I/O guards, then isolate its contents."""
test_venv = Path(sys.prefix)
if not (test_venv / "pyvenv.cfg").is_file():
return None
source = Path(sys.base_prefix).resolve().parent
if not (source / "facts.json").is_file():
return None # A system/Nix test interpreter has no selected PM tool store.
facts = Facts(source / "facts.json", strict=True)
# node runs the TUI behind the dashboard's /api/pty; carry it when the
# runner's store has it, or PM in the sandbox reports it not installed.
names = ("uv", "python") + (("node",) if facts.get("node") else ())
if "node" not in names and os.environ.get("HERMES_E2E_REQUIRE_TUI") == "1":
# A node on PATH alone (actions/setup-node) is invisible to PM, so
# every /api/pty chat would fail later with an opaque close 1011.
raise AssertionError(
f"HERMES_E2E_REQUIRE_TUI=1 but the PM store has no node: {source} "
"(install it with setup-pm `toolchain: all`)"
)
records = {name: facts.get(name) for name in names}
python = records["python"]
if not python or (source / python["entry"]).resolve() != Path(sys.base_prefix).resolve():
raise AssertionError(f"test interpreter is not from the selected PM store: {source}")
target = current_target()
lock = Lockfile(lockfile_path())
verified_tools(names, source_store=source, target=target, lock=lock)
# The source may live under the real ~/.hermes. Per-test guards must never
# read it, nor follow a sandbox tool symlink back into it. Copy the verified
# entries once per pytest process into the runner's throwaway scratch root.
prepared = Path(tempfile.mkdtemp(prefix="pm-e2e-tools-"))
try:
for record in records.values():
assert record is not None
shutil.copytree(source / record["entry"], prepared / record["entry"], symlinks=True)
(prepared / "facts.json").write_text(
json.dumps({"schema": 1, "packages": records}), encoding="utf-8"
)
verified_tools(names, source_store=prepared, target=target, lock=lock)
except BaseException:
shutil.rmtree(prepared, ignore_errors=True)
raise
atexit.register(shutil.rmtree, prepared, ignore_errors=True)
return prepared, records
_PREPARED_TOOLS = _prepare_test_tools()
def select_test_dependencies(hermes_home: Path, checkout: Path) -> None:
"""Point sandbox PM facts at the real test venv instead of bootstrapping a second one."""
test_venv = Path(sys.prefix)
if not (test_venv / "pyvenv.cfg").is_file():
return # Nix/system Python has no prepared venv to select.
selected = site_packages(test_venv)
assert selected.is_dir(), f"test interpreter lacks site-packages: {test_venv}"
state = hermes_home / "installs" / install_key(checkout)
environment = state / "environments" / "e2e-test" / "venv"
environment.mkdir(parents=True)
shutil.copyfile(test_venv / "pyvenv.cfg", environment / "pyvenv.cfg")
target = environment / selected.relative_to(test_venv)
target.parent.mkdir(parents=True)
target.symlink_to(selected, target_is_directory=True)
(state / "facts.json").write_text(json.dumps({"schema": 1, "packages": {"venv": {
"environment": str(environment),
}}}), encoding="utf-8")
if _PREPARED_TOOLS is None:
return
source, records = _PREPARED_TOOLS
tools = hermes_home / "tools"
tools.mkdir()
for record in records.values():
assert record is not None
entry = record["entry"]
(tools / entry).symlink_to(source / entry, target_is_directory=True)
(tools / "facts.json").write_text(json.dumps({"schema": 1, "packages": records}), encoding="utf-8")