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.
97 lines
4.2 KiB
Python
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")
|