Files
hermes-agent/tests/scripts/test_agent_build.py
ethernet 0b8aa8bf83 fix: align Python CI contracts with current PM and checkout behavior
Allow read-only merged-tag queries through the live-system guard, route checkpoint rekeying to its real ref-deletion owner, and update stale fixtures to exercise current update and PM boundaries. Fix the shutdown test wait by patching the bound server global.
2026-09-24 03:42:38 -04:00

355 lines
18 KiB
Python

"""Prepared inputs become runnable products without acquiring dependencies."""
from __future__ import annotations
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import pytest
from tests.pm._fixtures import stage_host_python
ROOT = Path(__file__).resolve().parents[2]
def inputs_fixture(tmp_path):
source = tmp_path / "prepared source"
source.mkdir()
(source / "pyproject.toml").write_text(
'[project]\nname="assembly-fixture"\nversion="1.2.3"\n'
'[project.scripts]\nprobe="entry:main"\n', encoding="utf-8")
(source / "entry.py").write_text(
"import json, os, sys\nfrom pathlib import Path\nimport dependency\n"
"from importlib.metadata import version\n"
"def main():\n print(json.dumps([sys.argv[1:], dependency.VALUE, "
"version('assembly-fixture'), (Path(__file__).parent/'skills/data').read_text()]))\n"
" return 7\n", encoding="utf-8")
(source / "skills").mkdir()
(source / "skills/data").write_text("resource", encoding="utf-8")
out = tmp_path / "payload"
python = stage_host_python(out / "tools/python/bin/python3")
site = out / "venv/lib/python3.14/site-packages"
site.mkdir(parents=True)
(site / "dependency.py").write_text("VALUE = 'prepared'\n", encoding="utf-8")
(site / "entry.py").write_text("raise RuntimeError('dependency shadowed source')\n", encoding="utf-8")
pm = out / "pm-runtime"
(pm / "lib/site-packages").mkdir(parents=True)
(pm / "pm-runtime.json").write_text(json.dumps({
"python": "../tools/python/bin/python3", "sitePackages": "lib/site-packages"}), encoding="utf-8")
tui, web = tmp_path / "tui", tmp_path / "web"
(tui / "dist").mkdir(parents=True)
web.mkdir()
(tui / "dist/entry.js").write_text("export const built = true;", encoding="utf-8")
(tui / "package.json").write_text('{"type":"module"}', encoding="utf-8")
(web / "index.html").write_text("built web", encoding="utf-8")
return out, {
"project": str(source / "pyproject.toml"), "code": str(source),
"repo": "app", "target": "linux-x64", "placement": "contained",
"python": str(python), "site_packages": str(site),
"environment": str(out / "venv"), "tools": str(out / "tools"),
"pm_runtime": str(pm), "resources": {"skills": str(source / "skills")},
"frontends": {"tui": str(tui), "web": str(web)}, "ref": "fixture-revision",
}
def build_cli(data, out, tmp_path):
document = tmp_path / "inputs.json"
document.write_text(json.dumps(data), encoding="utf-8")
home = tmp_path / "home"
home.mkdir(exist_ok=True)
return subprocess.run(
[sys.executable, "-B", "-m", "scripts.build.agent", "--inputs", str(document), "--out", str(out)],
cwd=tmp_path, env={"PATH": os.environ["PATH"], "HOME": str(home),
"HERMES_HOME": str(home / ".hermes"), "PYTHONPATH": str(ROOT)},
capture_output=True, text=True, timeout=30,
)
@pytest.mark.platforms("posix")
def test_contained_cli_assembly_runs_after_move_and_preserves_prepared_state(tmp_path):
out, data = inputs_fixture(tmp_path)
(out / "provider-state").write_text("retain", encoding="utf-8")
before = {p.relative_to(data["code"]): p.read_bytes() for p in Path(data["code"]).rglob("*") if p.is_file()}
result = build_cli(data, out, tmp_path)
assert result.returncode == 0, result.stderr
assert (out / "provider-state").read_text() == "retain"
manifest = json.loads((out / "manifest.json").read_text())
assert manifest["ref"] == data["ref"]
assert manifest["runtime"] == {
"repoDir": "app", "toolsDir": "tools", "storePython": "tools/python/bin/python3",
"sitePackages": "venv/lib/python3.14/site-packages", "commands": {"probe": "bin/probe"}}
assert (out / "app/hermes_cli/tui_dist/entry.js").is_file()
assert json.loads((out / "app/hermes_cli/tui_dist/package.json").read_text())["type"] == "module"
assert (out / "app/hermes_cli/web_dist/index.html").is_file()
assert not (tmp_path / "home/.hermes").exists()
moved = tmp_path / "relocated payload"
out.rename(moved)
(tmp_path / "entry.py").write_text("raise RuntimeError('cwd shadowed source')", encoding="utf-8")
run = subprocess.run([str(moved / manifest["runtime"]["commands"]["probe"]), "two words", "$(no)", ""],
cwd=tmp_path, env={"PATH": os.environ["PATH"], "HOME": str(tmp_path / "home"),
"PYTHONPATH": "/foreign", "PYTHONHOME": "/foreign"},
text=True, capture_output=True, timeout=30)
assert run.returncode == 7, run.stderr
assert json.loads(run.stdout) == [["two words", "$(no)", ""], "prepared", "1.2.3", "resource"]
assert before == {p.relative_to(data["code"]): p.read_bytes() for p in Path(data["code"]).rglob("*") if p.is_file()}
assert not list(moved.rglob("__pycache__"))
assert (moved / "pm-runtime/pm-runtime.json").is_file()
@pytest.mark.platforms("posix")
def test_referenced_installed_code_uses_derived_command_map_without_copying(tmp_path):
_, data = inputs_fixture(tmp_path)
out = tmp_path / "nix output"
source = Path(data["code"])
(source / "entry.py").write_text(
"import json, os\nfrom pathlib import Path\n"
"def main():\n print(json.dumps([Path(os.environ['HERMES_BUNDLED_SKILLS'],'data').read_text(),"
"os.environ['HERMES_NODE']]))\n", encoding="utf-8")
bindir = tmp_path / "installed env/bin"
bindir.mkdir(parents=True)
command = bindir / "probe"
command.write_text(f"#!{sys.executable}\nimport sys\nsys.path.insert(0, {str(source)!r})\nfrom entry import main\nmain()\n", encoding="utf-8")
command.chmod(0o755)
data.update(placement="references", repo="share/hermes-agent", command_dir=str(bindir),
env={"HERMES_NODE": "/prepared/node"})
before = {p: p.read_bytes() for p in source.rglob("*") if p.is_file()}
result = build_cli(data, out, tmp_path)
assert result.returncode == 0, result.stderr
mapping = json.loads((out / "command-map.json").read_text())
assert mapping["commands"] == {"probe": {
"source": str(command), "destination": "bin/probe", "entry": "entry:main"}}
assert Path(mapping["env"]["HERMES_TUI_DIR"]).resolve() == Path(data["frontends"]["tui"])
assert Path(mapping["env"]["HERMES_WEB_DIST"]).resolve() == Path(data["frontends"]["web"])
assert mapping["env"]["HERMES_INSTALL_ROOT"] == str(out / "share/hermes-agent")
assert not list(out.rglob("entry.py"))
assert (out / "share/hermes-agent/skills").is_symlink()
assert (out / "ui-tui").is_symlink()
assert (out / "share/hermes-agent/web_dist").is_symlink()
assert not (out / "bin/probe").exists(), "Nix owns native makeWrapper"
run = subprocess.run([mapping["commands"]["probe"]["source"]], cwd=tmp_path,
env={"PATH": os.environ["PATH"], "HOME": str(tmp_path / "home"),
"PYTHONDONTWRITEBYTECODE": "1", **mapping["env"]},
text=True, capture_output=True, check=True)
assert json.loads(run.stdout) == ["resource", "/prepared/node"]
assert before == {p: p.read_bytes() for p in source.rglob("*") if p.is_file()}
@pytest.mark.platforms("posix")
def test_fixed_root_keeps_privilege_shim_and_resolves_venv_command_symlink(tmp_path):
out, data = inputs_fixture(tmp_path)
# Docker source already occupies /opt/hermes; its bin/hermes belongs to s6.
shutil.copytree(data["code"], out, dirs_exist_ok=True)
data.update(placement="fixed", repo=".", code=str(out), project=str(out / "pyproject.toml"), bin_dir="libexec", python=sys.executable)
bindir = out / "bin"
bindir.mkdir()
(bindir / "probe").write_text("privilege shim", encoding="utf-8")
stale = out / "hermes_cli/web_dist/stale"
stale.parent.mkdir(parents=True)
stale.write_bytes(b"old surface")
result = build_cli(data, out, tmp_path)
assert result.returncode == 0, result.stderr
assert (bindir / "probe").read_text() == "privilege shim"
assert not stale.exists()
link = out / "venv/bin/probe"
link.parent.mkdir()
link.symlink_to("../../libexec/probe")
run = subprocess.run([str(link), "ok"], cwd=tmp_path,
env={"PATH": os.environ["PATH"], "HOME": str(tmp_path / "home")},
text=True, capture_output=True)
assert run.returncode == 7, run.stderr
assert json.loads(run.stdout)[0] == ["ok"]
@pytest.mark.platforms("posix")
def test_source_metadata_keeps_declared_requirements_extras_and_entrypoints(tmp_path):
from importlib.metadata import distributions
from packaging.requirements import Requirement
out, data = inputs_fixture(tmp_path)
project = Path(data["project"])
project.write_text(
'[project]\nname="assembly-fixture"\nversion="1.2.3"\nrequires-python=">=3.11"\n'
'dependencies=["dependency>=1,<2; python_version >= \'3.11\'"]\n'
'[project.optional-dependencies]\nextra=["optional>=2; sys_platform == \'linux\'"]\n'
'[project.scripts]\nprobe="entry:main"\n', encoding="utf-8")
result = build_cli(data, out, tmp_path)
assert result.returncode == 0, result.stderr
metadata = next(distributions(path=[str(out / "app")]))
assert metadata.metadata["Requires-Python"] == ">=3.11"
reqs = {Requirement(r).name: Requirement(r) for r in metadata.requires}
assert str(reqs["dependency"].specifier) == "<2,>=1"
optional = reqs["optional"]
assert optional.marker.evaluate({"extra": "extra", "sys_platform": "linux"})
assert not optional.marker.evaluate({"extra": "", "sys_platform": "linux"})
assert not optional.marker.evaluate({"extra": "extra", "sys_platform": "win32"})
assert [(e.name, e.value) for e in metadata.entry_points] == [("probe", "entry:main")]
@pytest.mark.platforms("posix")
@pytest.mark.parametrize("fault", ["missing-pm", "missing-resource", "missing-tui", "missing-web", "unknown-field", "bad-repo", "bad-target"])
def test_failed_inputs_cannot_leave_a_completion_claim(tmp_path, fault):
out, data = inputs_fixture(tmp_path)
(out / "manifest.json").write_text('{"runtime": {"commands": {}}}', encoding="utf-8")
if fault == "missing-pm":
(Path(data["pm_runtime"]) / "pm-runtime.json").unlink()
elif fault == "missing-resource":
data["resources"]["skills"] = str(tmp_path / "missing skills")
elif fault == "missing-tui":
(Path(data["frontends"]["tui"]) / "dist/entry.js").unlink()
elif fault == "missing-web":
(Path(data["frontends"]["web"]) / "index.html").unlink()
elif fault == "unknown-field":
data["typo"] = True
elif fault == "bad-repo":
data["repo"] = "../escape"
else:
data["target"] = "linux-typo"
result = build_cli(data, out, tmp_path)
assert result.returncode != 0
assert not (out / "manifest.json").exists()
assert not (out / "bin/probe").exists()
@pytest.mark.platforms("posix")
def test_incremental_copy_drops_removed_source_without_deleting_provider_files(tmp_path):
out, data = inputs_fixture(tmp_path)
source = Path(data["code"])
(source / "obsolete.py").write_text("old", encoding="utf-8")
assert build_cli(data, out, tmp_path).returncode == 0
sentinel = out / "provider-file"
sentinel.write_text("retain", encoding="utf-8")
(source / "obsolete.py").unlink()
(out / "app/hermes_cli/web_dist/stale").write_text("old", encoding="utf-8")
assert build_cli(data, out, tmp_path).returncode == 0
assert not (out / "app/obsolete.py").exists()
assert not (out / "app/hermes_cli/web_dist/stale").exists()
assert (out / "app/hermes_cli/tui_dist/entry.js").read_bytes() == (Path(data["frontends"]["tui"]) / "dist/entry.js").read_bytes()
assert sentinel.read_text() == "retain"
@pytest.mark.platforms("posix")
def test_payload_smoke_uses_relocated_manifest_commands(tmp_path):
out, data = inputs_fixture(tmp_path)
source = Path(data["code"])
(source / "pyproject.toml").write_text(
'[project]\nname="smoke-fixture"\nversion="1"\n'
'[project.scripts]\nhermes="entry:main"\n', encoding="utf-8")
(source / "entry.py").write_text(
"import json, os, sys\nfrom pathlib import Path\n"
"def main():\n"
f" assert not Path({str(out)!r}).exists(), 'payload was not moved'\n"
" assert 'HERMES_PYTHON' not in os.environ\n"
" assert not Path.cwd().is_relative_to(Path(__file__).parent)\n"
" if sys.argv[1:] == ['tools', 'list']:\n"
" import dependency\n"
" print(json.dumps([sys.argv[1:], dependency.VALUE]))\n"
" else: print('fast path')\n", encoding="utf-8")
data["bin_dir"] = "libexec"
assert build_cli(data, out, tmp_path).returncode == 0
shutil.rmtree(source)
env = dict(os.environ, PYTHONPATH="/foreign", PYTHONHOME="/foreign", HERMES_PYTHON="/foreign")
command = ["bash", str(ROOT / "scripts/smoke-payload.sh"), str(out)]
result = subprocess.run(command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30)
assert result.returncode == 0, result.stdout + result.stderr
assert '"prepared"' in result.stdout
assert out.is_dir(), "smoke must restore the artifact for packaging"
dependency = Path(data['site_packages']) / 'dependency.py'
dependency.unlink()
missing_dep = subprocess.run(command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30)
assert missing_dep.returncode != 0
assert 'SMOKE OK' not in missing_dep.stdout
# A broken published command cannot be rescued by importing raw Python.
(out / "libexec/hermes").unlink()
failed = subprocess.run(command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30)
assert failed.returncode != 0
assert "SMOKE OK" not in failed.stdout
assert out.is_dir(), "failed smoke must also restore the artifact"
# Even an executable reporting success is not a payload command if it escapes.
manifest = json.loads((out / 'manifest.json').read_text())
external = shutil.which('true')
assert external
(out / 'libexec/hermes').symlink_to(external)
for path in (external, 'libexec/hermes'):
manifest['runtime']['commands']['hermes'] = path
(out / 'manifest.json').write_text(json.dumps(manifest), encoding='utf-8')
escaped = subprocess.run(command, cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30)
assert escaped.returncode != 0, escaped.stdout
assert 'SMOKE OK' not in escaped.stdout
@pytest.mark.platforms("posix")
def test_stage_passes_explicit_products_to_the_single_assembler(tmp_path, monkeypatch):
from scripts.bundles import stage
from scripts.build.agent import assemble
from scripts.build.inputs import AgentInputs
out, data = inputs_fixture(tmp_path)
calls = []
def native(args):
calls.append(args.frontends)
assemble(AgentInputs.from_dict({**data, "frontends": args.frontends}), out)
return 0
monkeypatch.setattr(stage, "stage_native", native)
assert stage.main(["--out", str(out), "--ref", "fixture", "--tui", data["frontends"]["tui"],
"--web", data["frontends"]["web"]]) == 0
assert calls == [{k: Path(v) for k, v in data["frontends"].items()}]
assert json.loads((out / "manifest.json").read_text())["runtime"]["commands"] == {"probe": "bin/probe"}
@pytest.mark.platforms("posix")
def test_prepared_environment_python_imports_app_from_unrelated_script(tmp_path):
out, data = inputs_fixture(tmp_path)
environment = out / ".venv"
subprocess.run([sys.executable, "-m", "venv", "--without-pip", str(environment)], check=True)
python = environment / "bin/python"
site = subprocess.check_output([str(python), "-I", "-c", "import sysconfig; print(sysconfig.get_paths()['purelib'])"], text=True).strip()
Path(site, "dependency.py").write_text("VALUE='prepared'", encoding="utf-8")
data.update(placement="fixed", environment=str(environment), python=str(python), site_packages=site)
result = build_cli(data, out, tmp_path)
assert result.returncode == 0, result.stderr
script = tmp_path / "external-probe.py"
script.write_text("import entry\nfrom importlib.metadata import version\nprint(version('assembly-fixture'))\n", encoding="utf-8")
run = subprocess.run([str(python), "-I", str(script)], cwd=tmp_path,
env={"PATH": os.environ["PATH"], "HOME": str(tmp_path / "home"), "PYTHONDONTWRITEBYTECODE": "1"},
capture_output=True, text=True)
assert run.returncode == 0, run.stderr
assert run.stdout.strip() == "1.2.3"
def test_site_layout_uses_explicit_target_version_not_builder_python(tmp_path):
from scripts.build.inputs import dependency_site
assert dependency_site(tmp_path, "3.99.1+vendor", "linux-x64") == tmp_path / "lib/python3.99/site-packages"
assert dependency_site(tmp_path, "3.99.1", "win32-arm64") == tmp_path / "Lib/site-packages"
def test_automatic_frontends_use_requested_snapshot_not_current_checkout(tmp_path, monkeypatch):
from scripts.bundles import stage
repo = tmp_path / "checkout"
repo.mkdir()
subprocess.run(["git", "init", str(repo)], check=True, capture_output=True)
project = repo / "pyproject.toml"
project.write_text('[project]\nversion="1"\n', encoding="utf-8")
subprocess.run(["git", "add", "."], cwd=repo, check=True)
subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=f@example.test", "commit", "-m", "selected"], cwd=repo, check=True, capture_output=True)
revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=repo, text=True).strip()
project.write_text('[project]\nversion="dirty-current"\n', encoding="utf-8")
monkeypatch.setattr(stage, "ROOT", repo)
monkeypatch.setattr("scripts.build.icon_environment.prepare_icon_environment",
lambda source, out, cache: sys.executable)
monkeypatch.setattr(stage.shutil, "which", lambda name: "/prepared/node")
real_run = subprocess.run
built_sources = []
def run(command, **kwargs):
if command[0] != "/prepared/node":
return real_run(command, **kwargs)
source = Path(command[command.index("--source") + 1])
built_sources.append(source)
assert source != repo
assert 'version="1"' in (source / "pyproject.toml").read_text()
monkeypatch.setattr(stage.subprocess, "run", run)
monkeypatch.setattr(stage, "stage_native", lambda args: 0)
assert stage.main(["--out", str(tmp_path / "out"), "--ref", revision]) == 0
assert len(built_sources) == 4