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.
114 lines
5.0 KiB
Python
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()
|