Files
hermes-agent/tests/scripts/test_pm_runtime_bundle.py
ethernet bbec973514 refactor(pm): pm owns the dependency-environment layout and interpreter paths
hermes_cli.runtime_paths (venv generations, selection, activation) moves to
pm.environments, and gains venv_bin_dir / venv_python / project_python. Every
in-tree caller asks pm for an interpreter now; pm no longer reaches back into
hermes_cli for its own environment layout (pm.packages, pm.extras, pm.ensure,
pm.paths imported hermes_cli.runtime_paths). The three open-coded
"Scripts/python.exe or bin/python" ladders in pm collapse onto venv_python.

hermes_constants.venv_python_path / venv_bin_dir and hermes_cli.runtime_paths
stay as frozen-updater-surface shims only (tests/compat/old_updater_surface.json).

To keep the boot path light, pm/__init__ resolves its facade lazily (PEP 562)
and pm.registry loads the built-in package definitions on first read instead of
at import: `import hermes_bootstrap` now loads pm + pm.environments only (25ms,
was 37ms with the eager facade dragging in the downloader). The stripped-payload
fixtures that ship only pre-import files keep working for the same reason.

Also restores two frozen-surface re-exports the F401 sweep dropped
(banner._github_compare_behind, cua_backend.resolve_cua_driver_cmd).
2026-09-18 20:02:36 -04:00

114 lines
5.5 KiB
Python

"""A payload carries PM's independent dependency graph, not the app's imports."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import pytest
def _exercise_relocated_pm_runtime(tmp_path, monkeypatch):
from scripts.bundles import native
monkeypatch.setattr(Path, "home", lambda: tmp_path / "home")
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(tmp_path / "tools"))
uv = shutil.which("uv")
assert uv, "the packaging test requires uv"
root = tmp_path / "build"
repo = root / "hermes-agent"
source = Path(__file__).resolve().parents[2]
shutil.copytree(source / "pm", repo / "pm", ignore=shutil.ignore_patterns("__pycache__", ".hermes-tmp.*"))
(repo / "hermes_cli").mkdir()
for name in ("__init__.py", "runtime_state.py"):
shutil.copy2(source / "hermes_cli" / name, repo / "hermes_cli" / name)
shutil.copy2(source / "hermes_constants.py", repo / "hermes_constants.py")
# Copy the base executable, not a venv's launcher. The test host provides
# its stdlib; production's package stage provides the complete distribution.
python = root / "tools" / "python" / ("python.exe" if os.name == "nt" else "bin/python")
python.parent.mkdir(parents=True)
if os.name == "nt":
shutil.copytree(Path(sys.base_prefix), python.parent, dirs_exist_ok=True)
else:
shutil.copy2(Path(sys._base_executable).resolve(), python)
# Relocatable PBS does not retain the host's stdlib prefix. Supply the
# fixture's promised host library tree, including libpython on macOS.
(python.parent.parent / "lib").symlink_to(Path(sys.base_prefix) / "lib", target_is_directory=True)
stage = getattr(native, "stage_pm_runtime", None)
assert callable(stage), "native payload has no isolated PM runtime stage"
cache = tmp_path / "build-cache"
monkeypatch.setattr("pm.client._request", lambda *args, **kwargs: pytest.fail("runtime staging must not bootstrap a worker"))
monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: (Path(uv), Path(sys.executable)))
stage(root, python, repo, cache=cache)
assert cache.is_dir()
assert not (tmp_path / "home/cache/uv").exists()
stage(root, python, repo, offline=True, cache=cache)
(root / "manifest.json").write_text(json.dumps({"repo": "hermes-agent"}))
assert not (repo / ".venv").exists()
moved = tmp_path / "installed elsewhere"
root.rename(moved)
runtime = moved / "pm-runtime"
manifest = json.loads((runtime / "pm-runtime.json").read_text())
base = runtime / manifest["python"]
site = runtime / manifest["sitePackages"]
assert base.is_file() and site.is_dir()
assert not Path(manifest["python"]).is_absolute()
probe = """
import importlib.util, json, sys
sys.path.insert(0, sys.argv[1])
from ruamel.yaml import YAML
import packaging, tomli_w
assert importlib.util.find_spec('openai') is None
assert importlib.util.find_spec('yaml') is None
print(json.dumps(YAML(typ='safe').load('isolated: true')))
"""
checked = subprocess.run([str(base), "-I", "-S", "-B", "-c", probe, str(site)],
cwd=tmp_path, capture_output=True, text=True, timeout=30)
assert checked.returncode == 0, checked.stderr
assert json.loads(checked.stdout) == {"isolated": True}
from pm import runtime as runtime_api, paths
monkeypatch.setattr(paths, "repo_root", lambda: moved / "hermes-agent")
command = runtime_api.runtime_command(moved / "hermes-agent/pm/launch.py", ["status"])
checked = subprocess.run(command, cwd=tmp_path, env=runtime_api.runtime_environment(),
capture_output=True, text=True, timeout=30)
assert checked.returncode == 0, checked.stderr
assert "no pm sync receipt" in checked.stdout
assert str(root) not in (runtime / "pyvenv.cfg").read_text()
for link in (runtime / "bin").glob("python*"):
if link.is_symlink():
assert not os.path.isabs(os.readlink(link))
assert link.exists()
@pytest.mark.platforms("posix")
def test_native_pm_runtime_survives_payload_move(tmp_path, monkeypatch):
_exercise_relocated_pm_runtime(tmp_path, monkeypatch)
@pytest.mark.platforms("windows")
def test_resident_pm_bypasses_windows_redirector_after_move(tmp_path, monkeypatch):
_exercise_relocated_pm_runtime(tmp_path, monkeypatch)
@pytest.mark.parametrize("poison", ["cwd", "global"])
def test_pm_builder_ignores_ambient_uv_configuration(tmp_path, monkeypatch, poison):
from pm import stage_manager_runtime
monkeypatch.setattr(Path, "home", lambda: tmp_path / "home")
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(tmp_path / "tools"))
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path / "config"))
monkeypatch.setenv("APPDATA", str(tmp_path / "config"))
monkeypatch.chdir(tmp_path)
config = tmp_path / "uv.toml" if poison == "cwd" else tmp_path / "config/uv/uv.toml"
config.parent.mkdir(parents=True, exist_ok=True)
config.write_text('required-version = "<0.1"\n', encoding="utf-8")
uv = shutil.which("uv")
assert uv
monkeypatch.setattr("pm.client._request", lambda *args, **kwargs: pytest.fail("runtime staging must not bootstrap a worker"))
monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: (Path(uv), Path(sys.executable)))
executable = stage_manager_runtime(python=Path(sys.executable), destination=tmp_path / "runtime")
assert executable.is_file()