Files
hermes-agent/tests/pm/test_workspace_build_inputs.py
ethernet 8b7eae99ef fix(pm): own interpreter selection and dependency recovery
Pin uv and uvx to the PM interpreter instead of ambient Python discovery.
A matching dependency stamp cannot prove that installed files still exist.
Repair now rebuilds the recorded workspace and lock in a fresh generation,
checks startup imports, and publishes the selection only after success.

Run startup recovery before dependency activation. Keep manual PM repair
reachable when the selected environment is damaged. Preserve plugin
selection, retry ownership, and the previous generation on failure.
Remove the separate pip, ensurepip, per-extra, and install-time quarantine
ladders. Keep orphan launcher restoration.

Verification: 717 targeted tests passed on native Windows ARM64, with
56 skipped. Ruff, diff checks, and the source-scoped compat check passed.
A disposable real Hermes install recovered deleted YAML and dotenv files,
then printed CLI help with exit 0. Its lock and stamp stayed unchanged.
The full suite and a release build were not run for this change.
2026-09-08 23:39:55 -04:00

145 lines
7.1 KiB
Python

"""Workspace generation carries the source inputs of a buildable core."""
import importlib
import json
from pathlib import Path
import shutil
import subprocess
import sys
import pytest
from pm import workspace
from pm.packages import uv_env
def test_real_build_inputs_stay_in_generated_root(tmp_path, monkeypatch):
core = tmp_path / "core"
core.mkdir()
(core / "pyproject.toml").write_text(
'[project]\nname="buildable-core"\nversion="1.0"\nreadme="README.md"\n'
'requires-python=">=3.11"\nlicense="MIT"\nlicense-files=["LICENSE"]\n'
'[build-system]\nrequires=["setuptools==83.0.0","wheel==0.46.3"]\n'
'build-backend="setuptools.build_meta"\n'
'[tool.setuptools.packages.find]\ninclude=["buildable_core", "buildable_core.*"]\n', encoding="utf-8",
)
(core / "README.md").write_text("# Real core\n")
(core / "LICENSE").write_text("MIT\n")
package = core / "buildable_core"
package.mkdir()
(package / "__init__.py").write_text("VALUE = 'from actual source'\n")
(core / ".env").write_text("must not copy")
monkeypatch.setattr(workspace.paths, "repo_root", lambda: core)
root, venv = tmp_path / "staging", tmp_path / "venv"
uv = shutil.which("uv")
assert uv is not None
monkeypatch.setattr(importlib.import_module("pm.ensure"), "uv",
lambda **kwargs: (uv, {**uv_env(kwargs.get("base_env")), "UV_PYTHON": sys.executable}))
workspace.lock_and_sync([], [], root=root, venv_dir=venv)
python = venv / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python")
probe = subprocess.run([str(python), "-c", "import buildable_core; print(buildable_core.VALUE)"],
cwd=tmp_path, text=True, capture_output=True, check=True, timeout=30)
assert probe.stdout.strip() == "from actual source"
assert not (root / ".env").exists()
assert not list(core.glob("*.egg-info")), "build must not write into the original core"
assert not (core / "uv.lock").exists()
def test_source_refresh_does_not_need_metadata_change_and_refuses_live_root(tmp_path, monkeypatch):
core = tmp_path / "core"
core.mkdir()
(core / "pyproject.toml").write_text('[project]\nname="x"\nversion="1"\n')
(core / "code.py").write_text("VALUE = 1\n")
monkeypatch.setattr(workspace.paths, "repo_root", lambda: core)
staged = tmp_path / "staged"
workspace.build_root([], root=staged)
(core / "code.py").write_text("VALUE = 2\n")
workspace.build_root([], root=staged)
assert (staged / "code.py").read_text() == "VALUE = 2\n"
before = (core / "code.py").read_bytes()
with pytest.raises(workspace.InstallError, match="source"):
workspace.build_root([], root=core)
assert (core / "code.py").read_bytes() == before
def test_legacy_member_is_generated_only_inside_workspace(tmp_path, monkeypatch):
import tomllib
core, plugin = tmp_path / "core", tmp_path / "readonly-plugin"
core.mkdir(); plugin.mkdir()
(core / "pyproject.toml").write_text('[project]\nname="core"\nversion="1"\n')
manifest = plugin / "plugin.yaml"
manifest.write_text('name: legacy\npython_dependencies: ["example>=1,<2"]\n')
monkeypatch.setattr(workspace.paths, "repo_root", lambda: core)
stamp = workspace.members_stamp([plugin])
generated = workspace.build_root([plugin], tmp_path / "stage")
metadata = tomllib.loads((generated / "pyproject.toml").read_text())
member = (generated / metadata["tool"]["uv"]["workspace"]["members"][0]).resolve()
assert member.is_relative_to(generated)
assert not (plugin / "pyproject.toml").exists()
assert tomllib.loads((member / "pyproject.toml").read_text())["project"]["dependencies"] == ["example>=1,<2"]
manifest.write_text('name: legacy\npython_dependencies: ["example>=2,<3"]\n')
assert workspace.members_stamp([plugin]) != stamp
def _wheel(directory, name, version, requirements=()):
import zipfile
metadata = f"{name}-{version}.dist-info"
entries = {
f"{name}/__init__.py": f"__version__ = {version!r}\n",
f"{metadata}/METADATA": f"Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n"
+ "".join(f"Requires-Dist: {requirement}\n" for requirement in requirements),
f"{metadata}/WHEEL": "Wheel-Version: 1.0\nGenerator: fixture\nRoot-Is-Purelib: true\nTag: py3-none-any\n",
}
entries[f"{metadata}/RECORD"] = "".join(f"{path},,\n" for path in entries)
with zipfile.ZipFile(directory / f"{name}-{version}-py3-none-any.whl", "w") as archive:
for path, body in entries.items():
archive.writestr(path, body)
@pytest.mark.parametrize("exact", [False, True])
def test_plugin_can_move_compatible_transitive_but_not_exact_requirement(tmp_path, monkeypatch, exact):
import os
import tomllib
core = tmp_path / "core"
core.mkdir()
wheels = tmp_path / "wheels"
wheels.mkdir()
_wheel(wheels, "pkga", "1.0", ["pkgb>=1.2,<2"])
_wheel(wheels, "pkgb", "1.2")
core_requirement = '["pkga==1.0", "pkgb==1.2"]' if exact else '["pkga==1.0"]'
(core / "pyproject.toml").write_text(
'[project]\nname="core-proof"\nversion="1"\nrequires-python=">=3.11"\n'
f'dependencies={core_requirement}\n[tool.uv]\npackage=false\nno-index=true\n'
f'find-links=[{json.dumps(wheels.as_posix())}]\n', encoding="utf-8",
)
uv = shutil.which("uv")
assert uv
monkeypatch.setattr(workspace.paths, "repo_root", lambda: core)
monkeypatch.setattr(importlib.import_module("pm.ensure"), "uv",
lambda **kwargs: (uv, {**uv_env(kwargs.get("base_env")), "UV_PYTHON": sys.executable}))
baseline, first_env = tmp_path / "baseline", tmp_path / "first-env"
workspace.lock_and_sync([], [], root=baseline, venv_dir=first_env)
first_lock = (baseline / "uv.lock").read_bytes()
assert next(p["version"] for p in tomllib.loads(first_lock.decode())["package"] if p["name"] == "pkgb") == "1.2"
_wheel(wheels, "pkgb", "1.3")
plugin = tmp_path / "plugin"
plugin.mkdir()
(plugin / "pyproject.toml").write_text(
'[project]\nname="plugin-proof"\nversion="1"\nrequires-python=">=3.11"\ndependencies=["pkgb==1.3"]\n',
encoding="utf-8",
)
extended, candidate = tmp_path / "extended", tmp_path / "candidate"
if exact:
with pytest.raises(workspace.ResolutionConflict):
workspace.lock_and_sync([plugin], [], root=extended, venv_dir=candidate, seed_lock=baseline / "uv.lock")
else:
workspace.lock_and_sync([plugin], [], root=extended, venv_dir=candidate, seed_lock=baseline / "uv.lock")
installed = tomllib.loads((extended / "uv.lock").read_text(encoding="utf-8"))["package"]
assert next(p["version"] for p in installed if p["name"] == "pkgb") == "1.3"
python = candidate / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
result = subprocess.run([str(python), "-c", "import pkga, pkgb; print(pkga.__version__, pkgb.__version__)"],
capture_output=True, text=True, check=True, timeout=30)
assert result.stdout.strip() == "1.0 1.3"
assert (baseline / "uv.lock").read_bytes() == first_lock