Files
hermes-agent/tests/scripts/test_setup_toolchain.py
ethernet 53e6f001c7 refactor(pm): consolidate runtime ownership and updater completion
Run historical updater completion in a fresh interpreter so cached imports
cannot revive retired dependency installers. Share Git and ZIP completion,
carry receipt and recovery state, and preserve child exit status.

Route plugin admission, binary acquisition, desktop launch and build paths
through PM. Replace redundant helpers and tests with real worker, package,
publication and launch checks. Keep the shipped compatibility surface fixed.

Targeted Python and desktop checks pass. Native update journeys and fresh
production image qualification remain pending. This is a checkpoint before
those acceptance runs.
2026-09-12 16:30:35 -04:00

114 lines
5.0 KiB
Python

"""The CI bootstrap reads PM's pins without importing installed dependencies."""
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import sys
import pytest
from pm.lock import Lockfile
from pm.paths import lockfile_path
from pm.store import current_target
@pytest.mark.parametrize("extras", [[], ["dev"]], ids=["runtime", "tests"])
def test_development_setup_keeps_test_groups_out_of_the_runtime(tmp_path, monkeypatch, extras):
from types import SimpleNamespace
import shutil
from scripts.ci import setup_toolchain
from pm import lock_project
from tests.pm._fixtures import _wheel
monkeypatch.setattr(Path, "home", lambda: tmp_path / "home")
core = tmp_path / "core"
core.mkdir()
wheels = tmp_path / "wheels"
wheels.mkdir()
_wheel(wheels, "test_only_dep", "1.0")
(core / "pyproject.toml").write_text(
'[project]\nname="ci-test-environment"\nversion="1"\nrequires-python=">=3.11"\n'
'[project.optional-dependencies]\ndev=[]\n'
'[dependency-groups]\ntest=["test-only-dep==1.0"]\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("pm.client.is_runtime", lambda: True)
monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: (Path(uv), Path(sys.executable)))
lock_project(core, python=Path(sys.executable), offline=True, explicit=True)
home = tmp_path / "ci-home"
monkeypatch.setenv("HERMES_HOME", str(home))
monkeypatch.setattr("pm.paths.repo_root", lambda: core)
# This test exercises the worker-side CI environment split with offline uv.
# Dispatch into that worker is covered by test_runtime_entrypoints.
monkeypatch.setattr("pm.runtime.is_runtime", lambda: True)
files = {name: tmp_path / name for name in ("GITHUB_ENV", "GITHUB_OUTPUT", "GITHUB_PATH")}
for name, file in files.items():
monkeypatch.setenv(name, str(file))
setup_toolchain.dependencies(SimpleNamespace(extras=extras, home=home))
outputs = dict(line.split("=", 1) for line in files["GITHUB_OUTPUT"].read_text(encoding="utf-8").splitlines())
result = subprocess.run(
[outputs["python-path"], "-I", "-c", "import importlib.util; print(importlib.util.find_spec('test_only_dep') is not None)"],
cwd=tmp_path, capture_output=True, text=True, timeout=30,
)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == str("dev" in extras)
assert Path(outputs["venv"]).is_relative_to(home)
assert not (core / ".venv").exists()
from hermes_cli.runtime_paths import runtime_facts_path
assert runtime_facts_path(core).exists() == ("dev" not in extras)
@pytest.mark.parametrize("toolchain,names", [
("python", {"python", "uv"}),
("node", {"node", "npm"}),
("all", {"python", "uv", "node", "npm"}),
])
def test_stdlib_bootstrap_exports_the_pm_lock(toolchain, names, tmp_path):
root = Path(__file__).resolve().parents[2]
output = tmp_path / "output"
envfile = tmp_path / "environment"
home = tmp_path / "runner state"
env = {**os.environ, "GITHUB_OUTPUT": str(output), "GITHUB_ENV": str(envfile)}
result = subprocess.run(
[sys.executable, "-S", str(root / "scripts/ci/setup_toolchain.py"),
"prepare", "--toolchain", toolchain, "--home", str(home)],
cwd=tmp_path, env=env, capture_output=True, text=True, encoding="utf-8", timeout=30,
)
assert result.returncode == 0, result.stdout + result.stderr
values = dict(line.split("=", 1) for line in output.read_text(encoding="utf-8-sig").splitlines())
lock = Lockfile(lockfile_path())
assert json.loads(values["packages"]) == sorted(names)
assert values["target"] == current_target()
for name in names:
expected = lock.version(name)
assert values[f"{name}-version"] == (expected.partition("+")[0] if name == "python" else expected)
exported = dict(line.split("=", 1) for line in envfile.read_text(encoding="utf-8-sig").splitlines())
assert Path(exported["HERMES_HOME"]) == home
assert Path(exported["HERMES_RUNTIME_DIR"]).is_relative_to(home)
assert not Path(exported["HERMES_RUNTIME_DIR"]).exists(), "prepare must not provision before cache restore"
@pytest.mark.parametrize("extras", ['"dev"', '{}', '[1]', '["dev\\nHERMES_HOME=bad"]', '["--all"]'])
def test_invalid_extras_do_not_export_or_install(extras, tmp_path):
root = Path(__file__).resolve().parents[2]
output = tmp_path / "output"
home = tmp_path / "state"
result = subprocess.run(
[sys.executable, "-S", str(root / "scripts/ci/setup_toolchain.py"), "prepare",
"--home", str(home), "--extras", extras],
cwd=tmp_path, env={**os.environ, "GITHUB_OUTPUT": str(output)},
capture_output=True, text=True, encoding="utf-8", timeout=30,
)
assert result.returncode != 0
assert "extras must be a JSON array of extra names" in result.stderr
assert not output.exists()
assert not home.exists()