- pm.environment owns _RESOLVER_MARKERS: the streaming uv runner imported
pm.workspace, whose tomllib import fails on the 3.10 system python that
bootstraps the Docker arm64 image (No module named 'tomllib'). Invariant test
proves runtime staging needs neither tomllib nor pm.workspace.
- run_tests.sh forwards the MSVC/SDK/Rust/OpenSSL toolchain variables through
its env -i scrub so PM tests that compile ruamel-yaml-clib on Windows arm64
find cl.exe (previously 'Visual C++ 14.0 or greater is required').
- nix desktop-backend check: the spawned backend outlives cage's process group
and kept writing under the temp HERMES_HOME during rmtree; stop every
process bound to the throwaway HOME before cleanup.
- windows: test_launcher_runtime_selection imports runtime_state from
hermes_cli (moved in bbec973514); the ' spaced ' suffix row loses its
trailing space on win32 (the filesystem strips it).
- macOS: test_sealed_worker_command copies the interpreter into the payload
(the escape guard resolves symlinks) and links the host lib tree.
138 lines
6.3 KiB
Python
138 lines
6.3 KiB
Python
"""PM's resolver must not depend on the application it is repairing."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
|
|
def test_pm_runtime_discovers_plugins_without_application_dependencies(tmp_path, monkeypatch):
|
|
from pm.runtime import prepare_runtime
|
|
|
|
uv = shutil.which("uv")
|
|
if uv is None:
|
|
pytest.skip("uv is required for the real dependency-runtime test")
|
|
home = tmp_path / "home"
|
|
home.mkdir()
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
monkeypatch.setenv("HERMES_HOME", str(home))
|
|
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(home / "tools"))
|
|
(home / "config.yaml").write_text("plugins:\n enabled: []\n", encoding="utf-8")
|
|
repo = Path(__file__).resolve().parents[2]
|
|
python = prepare_runtime(Path(uv), Path(sys.executable), tmp_path / "runtime")
|
|
env = {k: v for k, v in os.environ.items() if not k.startswith(("PYTHON", "UV_"))}
|
|
env.update(HERMES_HOME=str(home), HERMES_RUNTIME_DIR=str(home / "tools"))
|
|
# Import real PM, including its production plugin-discovery chain.
|
|
code = f"""
|
|
import importlib.util, json, sys
|
|
sys.path.insert(0, {str(repo)!r})
|
|
from pm.workspace import enabled_member_dirs
|
|
from pm.plugins_state import read_home_selection
|
|
from pathlib import Path
|
|
assert read_home_selection(Path({str(home)!r}))["plugins"]["enabled"] == []
|
|
assert enabled_member_dirs() == []
|
|
assert importlib.util.find_spec("openai") is None
|
|
assert importlib.util.find_spec("yaml") is None
|
|
print(json.dumps({{"prefix": sys.prefix, "yaml": importlib.util.find_spec("ruamel.yaml").origin}}))
|
|
"""
|
|
result = subprocess.run([str(python), "-I", "-B", "-c", code], env=env,
|
|
capture_output=True, text=True, timeout=30)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
report = json.loads(result.stdout)
|
|
assert Path(report["yaml"]).is_relative_to(Path(report["prefix"]))
|
|
assert prepare_runtime(Path(uv), Path(sys.executable), tmp_path / "runtime", offline=True) == python
|
|
# Repair PM itself from its own lock, without trusting an existing marker.
|
|
(Path(report["yaml"]).parent / "main.py").unlink()
|
|
repaired = prepare_runtime(Path(uv), Path(sys.executable), tmp_path / "runtime", offline=True)
|
|
assert repaired != python
|
|
checked = subprocess.run([str(repaired), "-I", "-B", "-c", code], env=env,
|
|
capture_output=True, text=True, timeout=30)
|
|
assert checked.returncode == 0, checked.stdout + checked.stderr
|
|
|
|
|
|
def test_cold_worker_bootstrap_reuses_the_requests_cache(tmp_path, monkeypatch):
|
|
import pm
|
|
from hermes_constants import get_default_hermes_root
|
|
from pm import client, runtime
|
|
from pm.runtime_stage import stage_runtime
|
|
|
|
uv = shutil.which("uv")
|
|
assert uv, "the bootstrap cache contract requires real uv"
|
|
tools = Path(uv), Path(sys.executable)
|
|
cache = tmp_path / "shared-cache"
|
|
monkeypatch.setenv("HOME", str(tmp_path / "home"))
|
|
monkeypatch.setenv("USERPROFILE", str(tmp_path / "home"))
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home/.hermes"))
|
|
monkeypatch.setenv("HERMES_RUNTIME_DIR", str(tmp_path / "tools"))
|
|
monkeypatch.setattr("pm.paths.repo_root", lambda: tmp_path / "project")
|
|
monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: tools)
|
|
monkeypatch.setattr(client, "is_runtime", lambda: False)
|
|
stage_runtime(*tools, tmp_path / "warmup", cache=cache)
|
|
shutil.rmtree(tmp_path / "warmup")
|
|
|
|
def offline_stage(*args, **kwargs):
|
|
# A fresh manager must use the populated explicit cache, not download
|
|
# its dependencies again under the bundle's isolated HOME.
|
|
kwargs["offline"] = True
|
|
return stage_runtime(*args, **kwargs)
|
|
|
|
monkeypatch.setattr("pm.runtime_stage.stage_runtime", offline_stage)
|
|
worker = Path(client.__file__).with_name("worker.py")
|
|
script = (
|
|
"import runpy, sys; from pathlib import Path; "
|
|
f"sys.path.insert(0, {str(worker.parent.parent)!r}); import pm._uv; "
|
|
f"pm._uv._toolchain = lambda **kwargs: (Path({uv!r}), Path({sys.executable!r})); "
|
|
f"runpy.run_path({str(worker)!r}, run_name='__main__')"
|
|
)
|
|
|
|
def command(*args, **kwargs):
|
|
prepared = runtime.runtime_command(*args, **kwargs)
|
|
return [*prepared[:3], "-c", script]
|
|
|
|
monkeypatch.setattr(client, "runtime_command", command)
|
|
before = dict(os.environ)
|
|
pm.prune_cache(cache)
|
|
assert cache.is_dir()
|
|
assert not (get_default_hermes_root() / "cache/uv").exists(), "bootstrap created an unshared private cache"
|
|
assert dict(os.environ) == before
|
|
|
|
|
|
@pytest.mark.platforms("macos", "windows")
|
|
def test_sealed_worker_command_uses_only_its_recorded_site(tmp_path, monkeypatch):
|
|
from pm import paths
|
|
from pm.runtime import runtime_command
|
|
|
|
repo = tmp_path / "payload" / "hermes-agent"
|
|
repo.mkdir(parents=True)
|
|
(repo.parent / "manifest.json").write_text('{"repo":"hermes-agent"}')
|
|
runtime = repo.parent / "pm-runtime"
|
|
site = runtime / "site"
|
|
site.mkdir(parents=True)
|
|
base = repo.parent / "python"
|
|
if os.name == "nt":
|
|
shutil.copytree(Path(sys.base_prefix), base)
|
|
python = base / "python.exe"
|
|
else:
|
|
# The guard resolves symlinks (a python linked outside the payload IS an escape), so the
|
|
# interpreter is a real copy inside the payload. A relocatable/framework build locates its
|
|
# stdlib beside the executable: supply the host's library tree the way the bundle test does.
|
|
(base / "bin").mkdir(parents=True)
|
|
python = base / "bin" / "python"
|
|
shutil.copy2(Path(sys._base_executable).resolve(), python)
|
|
(base / "lib").symlink_to(Path(sys.base_prefix) / "lib", target_is_directory=True)
|
|
(runtime / "pm-runtime.json").write_text(json.dumps({
|
|
"python": os.path.relpath(python, runtime), "sitePackages": "site",
|
|
}))
|
|
script = repo / "probe.py"
|
|
script.write_text("import sys,json; print(json.dumps(sys.path))")
|
|
monkeypatch.setattr(paths, "repo_root", lambda: repo)
|
|
command = runtime_command(script)
|
|
result = subprocess.run(command, cwd=tmp_path, capture_output=True, text=True, timeout=30)
|
|
assert result.returncode == 0, result.stderr
|
|
entries = json.loads(result.stdout)
|
|
assert str(site) in entries
|
|
assert not any(Path(entry).name in {"site-packages", "dist-packages"} for entry in entries)
|