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).
114 lines
5.5 KiB
Python
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()
|