Files
hermes-agent/tests/pm/test_cold_runtime_e2e.py
ethernet ed76cbf9bb test(pm): skip host-Python fixtures by what the prefix contains
The cold-runtime e2e and the uv dependency-leg test ship or digest the host
interpreter as if it were a standalone Python. The earlier skips matched one
file (sitecustomize.py) and one path (/usr), so /usr/local, Homebrew or a
distro stdlib with other absolute links still failed.

Skip instead when the shipped stdlib holds a link tarfile's data filter
refuses, or when the prefix has unreadable files or is far larger than a
single interpreter.
2026-09-24 14:08:21 -04:00

307 lines
14 KiB
Python

"""Cold CLI provisioning and repair, with real tools and no mocked child code.
Only uv/Python are offered by the fixture's loopback archive server. The copied
PM recipe is unchanged; its small locked runtime is fetched from PyPI. The app
recipe is deliberately tiny so this cannot install the production dependency
set. Run through scripts/run_tests.sh with uv/uvx available on PATH.
"""
from __future__ import annotations
from functools import partial
import hashlib
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tarfile
import threading
import tomllib
import pytest
# Spawns children with a home it builds itself; the parent's must stay real.
pytestmark = pytest.mark.real_machine_home
class _ArchiveHandler(SimpleHTTPRequestHandler):
def copyfile(self, source, outputfile):
# The downloader closes its size probe without consuming the archive.
try:
super().copyfile(source, outputfile)
except (BrokenPipeError, ConnectionResetError):
pass
def _run(command, *, cwd, env, expected=0, timeout=240):
result = subprocess.run(command, cwd=cwd, env=env, capture_output=True,
text=True, timeout=timeout)
assert result.returncode == expected, (
f"{command!r}\nexit={result.returncode}\n{result.stdout}\n{result.stderr}"
)
return result
def _bare(python, repo, code, *, env, expected=0):
return _run(
[str(python), "-I", "-S", "-B", "-c",
f"import sys; sys.path.insert(0, {str(repo)!r});\n" + code],
cwd=repo.parent, env=env, expected=expected,
)
_UNSHIPPED = {"site-packages", "test", "__pycache__"}
def _foreign_link(stdlib: Path) -> Path | None:
"""First link in the shipped stdlib that the store's extractor refuses.
PM extracts archives with tarfile's data filter, which rejects absolute or
escaping links. Distro Pythons link stdlib files into /etc (Debian's
sitecustomize.py), so their stdlib cannot stand in for a standalone one.
"""
for directory, dirnames, filenames in os.walk(stdlib):
dirnames[:] = [name for name in dirnames if name not in _UNSHIPPED]
for name in (*dirnames, *filenames):
path = Path(directory) / name
if path.is_symlink() and (os.path.isabs(os.readlink(path))
or not path.resolve().is_relative_to(stdlib)):
return path
return None
@pytest.mark.platforms("linux")
@pytest.mark.parametrize("bootstrap_name", [None, "python3.11"])
def test_cold_cli_builds_own_runtime_discovers_plugins_and_repairs_app(tmp_path, bootstrap_name):
from pm.packages import Python, Uv
from pm.store import current_target, tree_digest
uv = shutil.which("uv")
if uv is None:
pytest.skip("real uv and uvx must be on PATH")
uv = Path(uv).resolve()
uvx = uv.with_name("uvx")
assert uvx.is_file(), "the real uv distribution must include uvx"
python = Path(sys._base_executable).resolve()
if sys.version_info[:2] != (3, 14):
pytest.skip("the checked-in PM runtime currently requires Python 3.14")
stdlib = next(d for d in (Path(sys.base_prefix) / "lib").glob("python3.*") if (d / "os.py").is_file())
foreign = _foreign_link(stdlib)
if foreign is not None:
pytest.skip(f"host Python is not self-contained: {foreign} links outside its stdlib")
bootstrap_python = shutil.which(bootstrap_name) if bootstrap_name else python
if bootstrap_python is None:
pytest.skip(f"{bootstrap_name} must be on PATH for the legacy bootstrap test")
source = Path(__file__).resolve().parents[2]
repo = tmp_path / "source"
repo.mkdir()
for name in ("pm", "hermes_cli"):
shutil.copytree(source / name, repo / name,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
for name in ("utils.py", "hermes_constants.py", "hermes_yaml.py",
"hermes_bootstrap.py"):
shutil.copy2(source / name, repo / name)
# No production application lock or metadata enters this source snapshot.
recipe = tomllib.loads((repo / "pm" / "pyproject.toml").read_text())
yaml_requirement = next(dep for dep in recipe["project"]["dependencies"]
if dep.startswith("ruamel.yaml"))
(repo / "pyproject.toml").write_text(
'[project]\nname="cold-pm-app"\nversion="0.0.0"\n'
'requires-python=">=3.14,<3.15"\n'
f'dependencies=[{json.dumps(yaml_requirement)}]\n'
'[project.optional-dependencies]\nall=[]\n'
'[tool.uv]\npackage=false\n', encoding="utf-8",
)
home = tmp_path / "home"
hermes_home = home / ".hermes"
plugin = hermes_home / "plugins" / "cold-proof"
plugin.mkdir(parents=True)
config = hermes_home / "config.yaml"
config.write_text("plugins:\n enabled: [cold-proof]\n", encoding="utf-8")
(plugin / "plugin.yaml").write_text(
"name: cold-proof\nversion: 1.0.0\npip_dependencies: [idna==3.10]\n",
encoding="utf-8",
)
store = hermes_home / "tools"
scratch = tmp_path / "scratch"
scratch.mkdir()
env = {
"HOME": str(home), "HERMES_HOME": str(hermes_home),
"HERMES_RUNTIME_DIR": str(store), "PATH": os.defpath,
"TMPDIR": str(scratch), "LANG": "C.UTF-8", "LC_ALL": "C.UTF-8",
"UV_PYTHON_DOWNLOADS": "never", "UV_NO_CONFIG": "1",
"UV_CACHE_DIR": str(tmp_path / "seed-cache"),
}
# Keep TLS functional on Nix without inheriting any application settings.
for key in ("SSL_CERT_FILE", "SSL_CERT_DIR", "NIX_SSL_CERT_FILE"):
if key in os.environ:
env[key] = os.environ[key]
_run([str(uv), "lock", "--project", str(repo), "--python", str(python)],
cwd=tmp_path, env=env)
archives = tmp_path / "archives"
archives.mkdir()
target = current_target()
rows = {}
for package, files in (
(Python(), [(python, "python/bin/python3")]),
(Uv(), [(uv, "uv-dist/uv"), (uvx, "uv-dist/uvx")]),
):
archive = archives / f"{package.name}.tar.gz"
with tarfile.open(archive, "w:gz", compresslevel=1) as tar:
for binary, name in files:
tar.add(binary, arcname=name)
if package.name == "python":
# A relocatable python-build-standalone finds its stdlib beside the binary, not at
# the host's prefix: ship the host's stdlib the way the real archive does.
tar.add(stdlib, arcname=f"python/lib/{stdlib.name}",
filter=lambda info: None if any(part in info.name.split("/") for part in ("site-packages", "test", "__pycache__")) else info)
version = _run([str(files[0][0]), "--version"], cwd=tmp_path,
env=env).stdout.split()[1]
rows[package.name] = {
"version": version,
"artifacts": {target: {"sha256": hashlib.sha256(archive.read_bytes()).hexdigest()}},
}
server = ThreadingHTTPServer(("127.0.0.1", 0),
partial(_ArchiveHandler, directory=str(archives)))
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
for name, row in rows.items():
row["artifacts"][target]["url"] = f"http://127.0.0.1:{server.server_port}/{name}.tar.gz"
(repo / "pm" / "lock.json").write_text(
json.dumps({"schema": 1, "packages": rows}), encoding="utf-8",
)
bootstrap = """
import importlib.util
assert importlib.util.find_spec('ruamel') is None
assert importlib.util.find_spec('yaml') is None
assert importlib.util.find_spec('packaging') is None
assert importlib.util.find_spec('idna') is None
"""
cli = "\nsys.argv = ['hermes', 'pm', {action!r}]; import hermes_cli.main\n"
try:
assert not store.exists()
assert not (hermes_home / "installs").exists()
result = _bare(bootstrap_python, repo, bootstrap + cli.format(action="install"), env=env)
assert "✓ venv" in result.stdout
assert "Preparing the isolated Hermes runtime" in result.stderr
if bootstrap_name:
# The installed launcher still starts on the old interpreter after
# a source swap. Completion must re-exec before importing the app.
(repo / ".git").mkdir()
(repo / "install-stamp.json").write_text(
json.dumps({"updateMechanism": "self"}), encoding="utf-8",
)
lock = repo / "uv.lock"
lock.write_bytes(lock.read_bytes() + b"\n# source update\n")
entry = repo / "launch_probe.py"
entry.write_text(
"import hermes_bootstrap\n"
"import idna, json, sys\n"
"print(json.dumps({'python': sys.executable, 'version': list(sys.version_info[:2]), "
"'idna': idna.__file__}))\n", encoding="utf-8",
)
launched = _run([str(bootstrap_python), "-B", str(entry)], cwd=repo, env=env)
launch_report = json.loads(launched.stdout)
assert Path(launch_report["python"]).is_relative_to(store)
assert launch_report["version"] == list(sys.version_info[:2])
assert Path(launch_report["idna"]).is_relative_to(hermes_home / "installs")
assert launched.stderr.count("completing source-update dependencies") == 1
# The launch finished the update's shared tail, whose maintenance migrates
# config.yaml; `pm repair` below must then leave that migrated file alone.
assert "_config_version" in config.read_text()
finally:
server.shutdown()
server.server_close()
thread.join(timeout=10)
expected_config = config.read_text()
report_code = """
import json
from pathlib import Path
from pm.environments import install_state_dir, selected_venv, runtime_facts_path
root = Path(sys.path[0])
state = install_state_dir(root)
print(json.dumps({'state': str(state), 'app': str(selected_venv(root)),
'facts': json.loads(runtime_facts_path(root).read_text())}))
"""
report = json.loads(_bare(python, repo, report_code, env=env).stdout)
state, app = Path(report["state"]), Path(report["app"])
assert state.is_relative_to(hermes_home)
pm_root = state / "pm-runtime"
selection = (pm_root / "selected.json").read_bytes()
runtime = pm_root / json.loads(selection)["generation"]
pm_python = runtime / "bin/python"
assert runtime != app
facts = json.loads((store / "facts.json").read_text())["packages"]
assert set(facts) == {"python", "uv"}
for name, fact in facts.items():
assert fact["version"] == rows[name]["version"]
assert fact["artifacts"] == [rows[name]["artifacts"][target]["sha256"]]
assert tree_digest(store / fact["entry"]) == fact["digest"]
probe = _run([str(pm_python), "-I", "-B", "-c", f"""
import importlib.util, json, sys
from pathlib import Path
sys.path.insert(0, {str(repo)!r})
from pm.workspace import enabled_member_dirs
from ruamel.yaml import YAML
import ruamel.yaml
assert enabled_member_dirs() == [Path({str(plugin)!r})]
assert importlib.util.find_spec('idna') is None
assert importlib.util.find_spec('openai') is None
print(json.dumps({{'yaml': ruamel.yaml.__file__, 'prefix': sys.prefix}}))
"""], cwd=tmp_path, env=env)
pm_report = json.loads(probe.stdout)
assert Path(pm_report["yaml"]).is_relative_to(runtime)
app_code = """
import json
from pathlib import Path
from pm.environments import activate_dependencies, selected_venv
root = Path(sys.path[0])
activate_dependencies(root)
import ruamel.yaml, idna
from ruamel.yaml import YAML
assert idna.encode('bücher.example') == b'xn--bcher-kva.example'
assert YAML(typ='safe').load('proof: true')['proof'] is True
print(json.dumps({'yaml': ruamel.yaml.__file__, 'idna': idna.__file__,
'app': str(selected_venv(root))}))
"""
app_report = json.loads(_bare(python, repo, app_code, env=env).stdout)
assert Path(app_report["yaml"]).is_relative_to(app)
assert Path(app_report["idna"]).is_relative_to(app)
initial_fact = report["facts"]["packages"]["venv"]
assert initial_fact["extras"] == ["all"]
lock_before = Path(initial_fact["resolved_lock"]).read_bytes()
assert b'idna' in lock_before
# Real damage: the app no longer imports, while PM retains its independent YAML.
shutil.rmtree(Path(app_report["yaml"]).parent)
broken = _bare(python, repo, app_code, env=env, expected=1)
assert "ModuleNotFoundError" in broken.stderr
assert "ruamel.yaml" in broken.stderr
repaired = _bare(python, repo, bootstrap + cli.format(action="repair"), env=env)
assert "Restart Hermes" in repaired.stdout
restored = json.loads(_bare(python, repo, app_code, env=env).stdout)
repaired_app = Path(restored["app"])
assert repaired_app != app
assert Path(restored["yaml"]).is_relative_to(repaired_app)
assert Path(restored["idna"]).is_relative_to(repaired_app)
assert (pm_root / "selected.json").read_bytes() == selection
after = json.loads(_bare(python, repo, report_code, env=env).stdout)
repaired_fact = after["facts"]["packages"]["venv"]
assert repaired_fact["extras"] == initial_fact["extras"]
assert repaired_fact["stamp"] == initial_fact["stamp"]
assert Path(repaired_fact["resolved_lock"]).read_bytes() == lock_before
assert config.read_text() == expected_config
if not bootstrap_name:
assert expected_config == "plugins:\n enabled: [cold-proof]\n"
assert not (state / ".repair-incomplete").exists()
assert not (repo / "venv").exists()
assert not (repo / ".venv").exists()
print(f"cold PM proof: runtime={runtime}; first_app={app}; repaired_app={repaired_app}")