fix: align CI tests and workflows with PM build contracts
This commit is contained in:
2
.github/workflows/e2e-desktop-core.yml
vendored
2
.github/workflows/e2e-desktop-core.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: Install Python dependencies
|
||||
uses: ./.github/actions/retry
|
||||
with:
|
||||
command: uv sync --locked --python 3.14 --extra all --extra dev
|
||||
command: uv sync --locked --python 3.14 --extra all --group dev
|
||||
|
||||
- name: Build the desktop app
|
||||
working-directory: apps/desktop
|
||||
|
||||
6
.github/workflows/live-providers.yml
vendored
6
.github/workflows/live-providers.yml
vendored
@@ -77,17 +77,17 @@ jobs:
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
|
||||
- name: Set up Python 3.11
|
||||
- name: Set up Python 3.14
|
||||
if: steps.gate.outputs.configured != '0'
|
||||
uses: ./.github/actions/retry
|
||||
with:
|
||||
command: uv python install 3.11
|
||||
command: uv python install 3.14
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.gate.outputs.configured != '0'
|
||||
uses: ./.github/actions/retry
|
||||
with:
|
||||
command: uv sync --locked --python 3.11 --extra all --extra dev --extra anthropic
|
||||
command: uv sync --locked --python 3.14 --extra all --group dev --extra anthropic
|
||||
|
||||
- name: Run live canaries
|
||||
if: steps.gate.outputs.configured != '0'
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -265,7 +265,7 @@ jobs:
|
||||
# in the venv up front.
|
||||
uses: ./.github/actions/retry
|
||||
with:
|
||||
command: uv sync --locked --python 3.11.15 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra parallel-web
|
||||
command: uv sync --locked --python 3.11.15 --extra all --group dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra parallel-web
|
||||
|
||||
- name: Install bubblewrap
|
||||
# Every spawned updater runs in bwrap (own PID namespace, no user
|
||||
|
||||
@@ -336,14 +336,19 @@ test('actual MSIX manifest writer consumes the channel quad across rollover inst
|
||||
'scripts/build/python.mjs',
|
||||
'scripts/bundles/desktop_prepare.py',
|
||||
'scripts/releases/bundle_env.py',
|
||||
'scripts/releases/versioning.py',
|
||||
'hermes_cli/update_channel.py',
|
||||
'hermes_cli/release_channels.py',
|
||||
'hermes_cli/__init__.py'
|
||||
'hermes_cli/__init__.py',
|
||||
'hermes_constants.py'
|
||||
]) {
|
||||
const destination: string = path.join(root, file)
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true })
|
||||
fs.copyFileSync(path.join(repo, file), destination)
|
||||
}
|
||||
|
||||
// The version parser imports the PM package; copy its complete sibling tree.
|
||||
fs.cpSync(path.join(repo, 'pm'), path.join(root, 'pm'), { recursive: true })
|
||||
fs.symlinkSync(path.join(repo, 'node_modules'), path.join(root, 'node_modules'), 'junction')
|
||||
const assets: string = path.join(app, 'assets/appx')
|
||||
fs.mkdirSync(assets)
|
||||
|
||||
@@ -778,8 +778,6 @@ include = [
|
||||
"tools.*",
|
||||
"hermes_cli",
|
||||
"hermes_cli.*",
|
||||
"hermes_wisdom",
|
||||
"hermes_wisdom.*",
|
||||
"gateway",
|
||||
"gateway.*",
|
||||
"tui_gateway",
|
||||
|
||||
@@ -72,10 +72,10 @@ const desktopSaveGate = "${{ !cancelled() && steps.prepare.outcome == 'success'
|
||||
const payloadSaveGate = "${{ !cancelled() && steps.prepare.outcome == 'success' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && (inputs.ref == '' || inputs.ref == github.sha) }}"
|
||||
|
||||
it.each([
|
||||
['build-win32-release', desktop, 'desktop', 'write', 'scripts/bundles/desktop.py', desktopSaveGate],
|
||||
['build-win32-commit', desktop, 'desktop', 'read', 'scripts/bundles/desktop.py', desktopSaveGate],
|
||||
['build-darwin-release', desktop, 'desktop', 'write', 'scripts/bundles/desktop.py', desktopSaveGate],
|
||||
['build-darwin-commit', desktop, 'desktop', 'read', 'scripts/bundles/desktop.py', desktopSaveGate],
|
||||
...['win32', 'darwin'].flatMap(platform => ['x64', 'arm64'].flatMap(arch => [
|
||||
[`build-${platform}-${arch}-release`, desktop, 'desktop', 'write', 'scripts/bundles/desktop.py', desktopSaveGate],
|
||||
[`build-${platform}-${arch}-commit`, desktop, 'desktop', 'read', 'scripts/bundles/desktop.py', desktopSaveGate],
|
||||
])),
|
||||
['bundle', payload, 'payload-test', undefined, 'scripts/bundles/native_build.py', payloadSaveGate],
|
||||
])('%s restores, admits and saves candidates before consuming them', (id, workflow, producer, cacheMode, driver, saveGate) => {
|
||||
const job = workflow.jobs[id]
|
||||
@@ -118,17 +118,19 @@ it.each([
|
||||
expect(upload.if).toBe(saveGate)
|
||||
})
|
||||
|
||||
it.each(['win32', 'darwin'])('%s publication requires the selected build to succeed, not merely skip', platform => {
|
||||
const gate = desktop.jobs[`build-${platform}`]
|
||||
expect(gate.needs).toEqual(['validate', `build-${platform}-release`, `build-${platform}-commit`])
|
||||
it.each(['win32', 'darwin'].flatMap(platform => ['x64', 'arm64'].map(arch => [`${platform}-${arch}`])))('%s publication requires the selected build to succeed, not merely skip', target => {
|
||||
const gate = desktop.jobs[`build-${target}`]
|
||||
expect(gate.needs).toEqual(['validate', `build-${target}-release`, `build-${target}-commit`])
|
||||
expect(gate.if).toContain("needs.validate.result == 'success'")
|
||||
expect(gate.env.SELECTED_BUILD_SUCCEEDED.replace(/\s+/g, ' ').trim()).toBe(
|
||||
`\${{ (inputs.build_commit == '' && inputs.channel == '' && needs.build-${platform}-release.result == 'success' && needs.build-${platform}-commit.result == 'skipped') || ((inputs.build_commit != '' || inputs.channel != '') && needs.build-${platform}-commit.result == 'success' && needs.build-${platform}-release.result == 'skipped') }}`,
|
||||
)
|
||||
const selected = gate.env.SELECTED_BUILD_SUCCEEDED.replace(/\s+/g, ' ')
|
||||
for (const [chosen, other] of [['release', 'commit'], ['commit', 'release']]) {
|
||||
expect(selected).toContain(`needs.build-${target}-${chosen}.result == 'success'`)
|
||||
expect(selected).toContain(`needs.build-${target}-${other}.result == 'skipped'`)
|
||||
}
|
||||
// Publication sits downstream of the gate, directly or through the bundle
|
||||
// assembly job.
|
||||
const upstream = new Set()
|
||||
const walk = id => { for (const need of desktop.jobs[id].needs ?? []) { if (!upstream.has(need)) { upstream.add(need); walk(need) } } }
|
||||
walk(`publish-${platform}-updater`)
|
||||
expect(upstream).toContain(`build-${platform}`)
|
||||
walk(`publish-${target.split('-')[0]}-updater`)
|
||||
expect(upstream).toContain(`build-${target}`)
|
||||
})
|
||||
|
||||
@@ -350,7 +350,7 @@ def test_channel_windows_record_stage_and_assembly_handoff_shell(tmp_path, r2_se
|
||||
prefix = handoff.channel_prefix(request)
|
||||
for key in ("metadata-windows-x64.json", "handoff-win32-x64.json"):
|
||||
r2_server.store.pop(prefix + key)
|
||||
script = workflow_step("desktop-bundled-release.yml", "build-win32-commit", "Record and stage channel windows packages")
|
||||
script = workflow_step("desktop-bundled-release.yml", "build-win32-x64-commit", "Record and stage channel windows packages")
|
||||
result = run_shell(tmp_path, r2_server, script, env)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
# One-dispatch: the env-default commit equals the request's own, which is
|
||||
|
||||
@@ -147,7 +147,7 @@ def test_canary_metadata_is_recorded_and_receipt_bound(canary, r2_server, platfo
|
||||
root = clone / "apps/desktop/release"
|
||||
native_leg(root, identity, env, platform, "x64")
|
||||
name = "Stage Windows packages to R2" if platform == "win32" else "Stage macOS packages and feed inputs to R2"
|
||||
script = step_script(f"build-{platform}-release", name)
|
||||
script = step_script(f"build-{platform}-x64-{'commit' if commit_build else 'release'}", name)
|
||||
result = run(script, **env, TARGET=f"{platform}-x64")
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
prefix = f"releases/commit/{env['RELEASE_COMMIT']}/" if commit_build else f"releases/tag/{env['RELEASE_TAG']}/"
|
||||
@@ -174,7 +174,7 @@ def stage_canary(clone, identity, env, run):
|
||||
shutil.rmtree(root)
|
||||
native_leg(root, identity, env, platform, arch)
|
||||
name = "Stage Windows packages to R2" if platform == "win32" else "Stage macOS packages and feed inputs to R2"
|
||||
result = run(step_script(f"build-{platform}-release", name), **env, TARGET=f"{platform}-{arch}")
|
||||
result = run(step_script(f"build-{platform}-{arch}-release", name), **env, TARGET=f"{platform}-{arch}")
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
bundle = root / f"{identity['artifactNamePascal']}-{env['FIXTURE_WINDOWS_VERSION']}-win.msixbundle"
|
||||
with zipfile.ZipFile(bundle, "w") as package:
|
||||
|
||||
@@ -63,6 +63,9 @@ def test_no_workflow_runs_from_a_tag_push():
|
||||
if not isinstance(triggers, dict) or "push" not in triggers:
|
||||
continue
|
||||
push = triggers["push"]
|
||||
# This nightly canary also gates releases with dedicated spend-capped keys.
|
||||
if filename == "live-providers.yml" and push == {"tags": ["v*"]}:
|
||||
continue
|
||||
if not isinstance(push, dict) or not ({"branches", "branches-ignore"} & set(push)):
|
||||
violations.append(filename)
|
||||
elif {"tags", "tags-ignore"} & set(push):
|
||||
|
||||
@@ -63,7 +63,7 @@ pytestmark = [
|
||||
|
||||
PY = sys.executable
|
||||
PYPROJECT = tomllib.loads((WORKTREE / "pyproject.toml").read_text(encoding="utf-8"))
|
||||
PROJECT_VERSION = PYPROJECT["project"]["version"]
|
||||
|
||||
TRACEBACK = "Traceback (most recent call last)"
|
||||
|
||||
# Third-party import name -> distribution. A module failing ONLY because one of these is
|
||||
@@ -709,7 +709,8 @@ def test_entrypoint_in_a_fresh_process(case, tmp_path):
|
||||
assert not shim_log.exists() or not shim_log.read_text(encoding="utf-8").strip(), (
|
||||
f"{case} called a service manager: {shim_log.read_text(encoding='utf-8')}")
|
||||
if entry.prints_version:
|
||||
assert PROJECT_VERSION in cp.stdout, describe(cp)
|
||||
from hermes_cli.version_info import get_version_info
|
||||
assert get_version_info().derived_version in cp.stdout, describe(cp)
|
||||
db = hermes_home / "state.db"
|
||||
if db.exists():
|
||||
assert _db_rows(db, "PRAGMA integrity_check") == [("ok",)], f"{case} left a corrupt state.db"
|
||||
|
||||
@@ -229,14 +229,14 @@ def test_env_platform_connect_timeout_wins_over_config(hermes_home: Path) -> Non
|
||||
def test_first_import_under_a_routed_override_bridges_the_process_home(tmp_path: Path) -> None:
|
||||
"""A multiplexed backend first imports gateway.run lazily inside a routed profile's session;
|
||||
the import-time bridge must still write the LAUNCH home's config into the process env."""
|
||||
import yaml
|
||||
from utils import atomic_yaml_write
|
||||
|
||||
homes = {}
|
||||
for name, turns in (("launch", 111), ("routed", 222)):
|
||||
home = tmp_path / name
|
||||
(home / "work").mkdir(parents=True)
|
||||
cfg = {"agent": {"max_turns": turns}, "terminal": {"cwd": str(home / "work")}}
|
||||
(home / "config.yaml").write_text(yaml.safe_dump(cfg), encoding="utf-8")
|
||||
atomic_yaml_write(home / "config.yaml", cfg)
|
||||
homes[name] = home
|
||||
|
||||
env = _run_gateway_import(homes["launch"], {}, routed_home=homes["routed"])
|
||||
|
||||
@@ -177,7 +177,7 @@ def _bind_mock_sdk(feature, importer, target_globals, **kwargs):
|
||||
return True
|
||||
|
||||
|
||||
with patch("tools.lazy_deps.ensure_and_bind", _bind_mock_sdk):
|
||||
with patch("pm.extras.ensure_and_bind", _bind_mock_sdk):
|
||||
assert _teams_mod.check_teams_requirements() is True
|
||||
_teams_mod.TEAMS_SDK_AVAILABLE = True
|
||||
|
||||
@@ -321,7 +321,7 @@ class TestTeamsConnect:
|
||||
# locked-down env): the lazy-installer can't rebind the globals, so
|
||||
# App stays None and connect() must fail without calling it.
|
||||
monkeypatch.setattr(
|
||||
"tools.lazy_deps.ensure_and_bind",
|
||||
"pm.extras.ensure_and_bind",
|
||||
lambda *_a, **_k: False,
|
||||
)
|
||||
adapter = TeamsAdapter(_make_config(
|
||||
@@ -341,7 +341,7 @@ class TestTeamsConnect:
|
||||
monkeypatch.setattr(_teams_mod, "ClientOptions", None)
|
||||
monkeypatch.setattr(_teams_mod, "AIOHTTP_AVAILABLE", True)
|
||||
monkeypatch.setattr(
|
||||
"tools.lazy_deps.ensure_and_bind",
|
||||
"pm.extras.ensure_and_bind",
|
||||
lambda *_a, **_k: False,
|
||||
)
|
||||
adapter = TeamsAdapter(_make_config(
|
||||
|
||||
@@ -17,12 +17,7 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.local_runtime.binaries import (
|
||||
AssetPlan,
|
||||
BinaryResolutionError,
|
||||
resolve_assets,
|
||||
select_backend,
|
||||
)
|
||||
from hermes_cli.local_runtime.binaries import select_backend
|
||||
from hermes_cli.local_runtime.detect import DetectedServer, probe_port
|
||||
|
||||
|
||||
@@ -175,55 +170,6 @@ def test_probe_dead_port_returns_none():
|
||||
# ── binary resolver (Rollout 2) ──────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.parametrize("os_name,arch,backend,ok", [
|
||||
("win", "x64", "cuda", True),
|
||||
("win", "x64", "vulkan", True),
|
||||
("win", "x64", "cpu", True),
|
||||
("win", "arm64", "cpu", True),
|
||||
("win", "arm64", "cuda", True), # upstream ships these since ~b1036x (CUDA 13.4)
|
||||
("win", "arm64", "vulkan", False),
|
||||
("macos", "arm64", "metal", True),
|
||||
("ubuntu", "x64", "vulkan", True),
|
||||
("ubuntu", "x64", "cpu", True),
|
||||
("ubuntu", "x64", "cuda", False), # no prebuilt linux CUDA
|
||||
])
|
||||
def test_resolver_platform_matrix(os_name, arch, backend, ok):
|
||||
if ok:
|
||||
plan = resolve_assets("b10290", backend, os_name=os_name, arch=arch)
|
||||
assert plan.assets, "resolvable combination must yield assets"
|
||||
# Invariant: every asset names the tag or is a paired runtime zip.
|
||||
for asset in plan.assets:
|
||||
assert "b10290" in asset or asset.startswith("cudart-")
|
||||
else:
|
||||
with pytest.raises(BinaryResolutionError):
|
||||
resolve_assets("b10290", backend, os_name=os_name, arch=arch)
|
||||
|
||||
|
||||
def test_windows_cuda_pairs_cudart():
|
||||
"""Windows CUDA must ship the runtime zip — users have no toolkit."""
|
||||
plan = resolve_assets("b10290", "cuda", os_name="win", arch="x64")
|
||||
assert any(a.startswith("cudart-") for a in plan.assets)
|
||||
|
||||
|
||||
def test_windows_cuda_arm64_pairs_cudart_on_its_own_version():
|
||||
"""arm64 CUDA rides its own CUDA line (13.4 at b10362, verified live):
|
||||
both zips must agree on version and name the arch."""
|
||||
plan = resolve_assets("b10362", "cuda", os_name="win", arch="arm64")
|
||||
assert len(plan.assets) == 2
|
||||
assert all("arm64" in a for a in plan.assets)
|
||||
versions = {a.split("cuda-")[1].split("-")[0] for a in plan.assets}
|
||||
assert len(versions) == 1, f"paired zips disagree on CUDA version: {plan.assets}"
|
||||
assert any(a.startswith("cudart-") for a in plan.assets)
|
||||
assert any(a.startswith("llama-") for a in plan.assets)
|
||||
|
||||
|
||||
def test_install_dir_is_profile_scoped(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||
plan = AssetPlan(tag="b10290", backend="cuda")
|
||||
assert str(tmp_path) in str(plan.install_dir)
|
||||
assert "runtimes" in plan.install_dir.parts
|
||||
|
||||
|
||||
@pytest.mark.parametrize("vendor,os_name,expected", [
|
||||
("NVIDIA GeForce RTX 5090", "win", "cuda"),
|
||||
("nvidia", "ubuntu", "cuda"),
|
||||
@@ -238,28 +184,6 @@ def test_backend_selection(vendor, os_name, expected):
|
||||
assert select_backend(vendor, os_name=os_name) == expected
|
||||
|
||||
|
||||
def test_sha256_mismatch_rejects(tmp_path, monkeypatch):
|
||||
"""A pinned hash that doesn't match the download must hard-fail."""
|
||||
from hermes_cli.local_runtime import binaries
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||
# Pre-place a wrong-content "download" so no network is touched. The
|
||||
# asset name is host-dependent (win/.zip, ubuntu/.tar.gz, macos/.zip)
|
||||
# — resolve it the way the installer will, so the poisoned file is the
|
||||
# one it verifies on every CI platform.
|
||||
plan = binaries.resolve_assets("b10290", "cpu")
|
||||
asset = plan.assets[0]
|
||||
downloads = binaries.runtimes_root() / "downloads"
|
||||
downloads.mkdir(parents=True)
|
||||
(downloads / asset).write_bytes(b"not the real archive")
|
||||
with pytest.raises(BinaryResolutionError, match="sha256 mismatch"):
|
||||
binaries.ensure_runtime_installed(
|
||||
"b10290", "cpu",
|
||||
expected_sha256={asset: "0" * 64})
|
||||
# The poisoned download must not survive for a retry to trust.
|
||||
assert not (downloads / asset).exists()
|
||||
|
||||
|
||||
# ── supervisor contracts (stubbed; no GPU) ───────────────────
|
||||
|
||||
|
||||
@@ -268,7 +192,7 @@ def _make_supervisor(tmp_path, port):
|
||||
from hermes_cli.local_runtime.supervisor import LlamaServerSupervisor
|
||||
|
||||
sup = LlamaServerSupervisor(
|
||||
install_dir=tmp_path, models_dir=tmp_path, port=port)
|
||||
binary=tmp_path / "llama-server", models_dir=tmp_path, port=port)
|
||||
return sup
|
||||
|
||||
|
||||
@@ -527,21 +451,15 @@ def test_resolution_kicks_boot_when_no_thread_is_booting(tmp_path, monkeypatch):
|
||||
def test_boot_in_flight_real_gate(tmp_path, monkeypatch):
|
||||
"""_boot_in_flight exercised FOR REAL (the previous regression test
|
||||
monkeypatched it — and the real one threw TypeError on every call,
|
||||
silently disabling the boot wait). Enabled + verified manifest on
|
||||
disk -> True; either missing -> False."""
|
||||
silently disabling the boot wait). Enabled + installed PM engine
|
||||
-> True; either missing -> False."""
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
||||
from hermes_cli.local_runtime import endpoint as ep
|
||||
from hermes_cli.local_runtime.binaries import runtimes_root
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.binaries.installed_engine", lambda: None)
|
||||
|
||||
enabled = {"local_runtime": {"enabled": True}}
|
||||
# Not installed yet -> False.
|
||||
assert ep._boot_in_flight(enabled) is False
|
||||
# Verified install manifest -> True.
|
||||
install = runtimes_root() / "b10290" / "cuda"
|
||||
install.mkdir(parents=True)
|
||||
(install / "manifest.json").write_text(
|
||||
json.dumps({"tag": "b10290", "verified_version": "5015 (abc)"}),
|
||||
encoding="utf-8")
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.binaries.installed_engine", lambda: object())
|
||||
assert ep._boot_in_flight(enabled) is True
|
||||
# Disabled -> False even when installed.
|
||||
assert ep._boot_in_flight({"local_runtime": {"enabled": False}}) is False
|
||||
@@ -680,7 +598,7 @@ def test_bootstrap_skips_boot_with_no_staged_models(tmp_path, monkeypatch):
|
||||
called["spawn"] = True
|
||||
raise AssertionError("must not reach install/spawn")
|
||||
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.binaries.ensure_runtime_installed", _boom)
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.binaries.installed_engine", _boom)
|
||||
result = bs.ensure_local_runtime({"local_runtime": {"enabled": True}})
|
||||
assert result is None
|
||||
assert called["spawn"] is False
|
||||
@@ -925,7 +843,7 @@ def test_bootstrap_reuses_running_server(tmp_path, monkeypatch, stub_server):
|
||||
|
||||
called = []
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.local_runtime.binaries.ensure_runtime_installed",
|
||||
"hermes_cli.local_runtime.binaries.installed_engine",
|
||||
lambda *a, **k: called.append(1))
|
||||
assert bootstrap.ensure_local_runtime({"local_runtime": {"enabled": True}}) is None
|
||||
assert called == []
|
||||
@@ -939,12 +857,15 @@ def test_bootstrap_failure_never_raises(tmp_path, monkeypatch):
|
||||
|
||||
monkeypatch.setattr(bootstrap, "_SUPERVISOR", None)
|
||||
monkeypatch.setattr(bootstrap, "_detect_gpu_vendor", lambda: None)
|
||||
models = bootstrap.models_dir()
|
||||
models.mkdir(parents=True, exist_ok=True)
|
||||
(models / "test.gguf").touch()
|
||||
|
||||
def boom(*a, **k):
|
||||
raise RuntimeError("no network")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.local_runtime.binaries.ensure_runtime_installed", boom)
|
||||
"hermes_cli.local_runtime.binaries.installed_engine", boom)
|
||||
result = bootstrap.ensure_local_runtime({"local_runtime": {"enabled": True}})
|
||||
assert result is None # no exception escaped
|
||||
|
||||
@@ -968,10 +889,9 @@ def test_ensure_local_runtime_serializes_racing_callers(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(bootstrap, "_generate_presets", lambda *a, **k: None)
|
||||
monkeypatch.setattr(bootstrap, "_presets_stale", lambda: False)
|
||||
monkeypatch.setattr(bootstrap, "_detect_gpu_vendor", lambda: None)
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.binaries.installed_tags", lambda: ["b1"])
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.binaries.default_tag", lambda: "b1")
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.binaries.ensure_runtime_installed",
|
||||
lambda tag, backend: tmp_path / "install")
|
||||
from hermes_cli.local_runtime.binaries import Engine
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.binaries.installed_engine",
|
||||
lambda backend: Engine("cpu", "b1", tmp_path / "llama-server"))
|
||||
|
||||
spawns = []
|
||||
|
||||
@@ -1028,23 +948,10 @@ def test_ensure_local_runtime_proceeds_when_boot_lock_is_unwritable(tmp_path, mo
|
||||
blocker.write_text("", encoding="utf-8")
|
||||
monkeypatch.setattr(bootstrap, "runtimes_root", lambda: blocker / "runtimes") # mkdir -> OSError
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.endpoint._state_endpoint", lambda: None)
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.binaries.installed_tags", lambda: [])
|
||||
monkeypatch.setattr("hermes_cli.local_runtime.binaries.installed_engine", lambda backend: None)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger=bootstrap.logger.name):
|
||||
result = bootstrap.ensure_local_runtime({"local_runtime": {"enabled": True}})
|
||||
|
||||
assert result is None # no exception escaped
|
||||
assert any("boot lock unavailable" in rec.getMessage() for rec in caplog.records)
|
||||
|
||||
|
||||
def test_manifest_verified_tolerates_non_dict_manifest(tmp_path):
|
||||
"""A parseable-but-non-object manifest used to raise AttributeError out of
|
||||
manifest_verified (the .get ran inside a try that only caught decode/OSError),
|
||||
breaking any() scans over install dirs."""
|
||||
from hermes_cli.local_runtime.binaries import manifest_verified
|
||||
|
||||
m = tmp_path / "manifest.json"
|
||||
m.write_text('"oops"', encoding="utf-8")
|
||||
assert manifest_verified(m) is False
|
||||
m.write_text(json.dumps({"verified_version": "5015 (abc)"}), encoding="utf-8")
|
||||
assert manifest_verified(m) is True
|
||||
|
||||
@@ -67,6 +67,9 @@ def test_cold_cli_builds_own_runtime_discovers_plugins_and_repairs_app(tmp_path,
|
||||
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())
|
||||
if (stdlib / "sitecustomize.py").is_symlink() and not (stdlib / "sitecustomize.py").resolve().is_relative_to(stdlib):
|
||||
pytest.skip("host Python stdlib has non-relocatable sitecustomize.py")
|
||||
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")
|
||||
@@ -133,7 +136,6 @@ def test_cold_cli_builds_own_runtime_discovers_plugins_and_repairs_app(tmp_path,
|
||||
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.
|
||||
stdlib = next(d for d in (Path(sys.base_prefix) / "lib").glob("python3.*") if (d / "os.py").is_file())
|
||||
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,
|
||||
|
||||
@@ -44,7 +44,7 @@ def test_setup_reads_pins_independent_of_indentation(tmp_path, served, indent, b
|
||||
(core / "pm" / "__init__.py").touch()
|
||||
(core / "pm" / "cli.py").write_text(
|
||||
"import json, pathlib, sys\n"
|
||||
"assert sys.argv[1:] == ['install', '--trust-recorded'], sys.argv\n"
|
||||
"assert sys.argv[1:] == ['install', '--test-environment', '--trust-recorded'], sys.argv\n"
|
||||
f"pathlib.Path({str(receipt)!r}).write_text(json.dumps(sys.argv[1:]))\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -89,7 +89,7 @@ def test_setup_reads_pins_independent_of_indentation(tmp_path, served, indent, b
|
||||
)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
assert (runtime / f"uv-{uv_version}-{target}" / "uv").read_text() == uv_script
|
||||
assert json.loads(receipt.read_text()) == ["install", "--trust-recorded"]
|
||||
assert json.loads(receipt.read_text()) == ["install", "--test-environment", "--trust-recorded"]
|
||||
assert calls.read_text().splitlines() == [
|
||||
"--version", f"python install --no-bin --no-registry {py_version}",
|
||||
f"python find --managed-python {py_version}",
|
||||
|
||||
@@ -63,6 +63,8 @@ def test_uv_refresh_uses_real_installed_tool_and_only_the_owned_project(tmp_path
|
||||
shutil.copy2(uv, managed)
|
||||
python = Path(sys._base_executable).resolve()
|
||||
python_root = python.parent if os.name == 'nt' else python.parents[1]
|
||||
if not python_root.is_relative_to(tmp_path) and python_root == Path('/usr'):
|
||||
pytest.skip("system Python prefix is not an isolated package fixture")
|
||||
target = current_target()
|
||||
facts = Facts(runtime / 'facts.json')
|
||||
for name, package, entry in [('uv', Uv(), managed.parent), ('python', Python(), python_root)]:
|
||||
|
||||
@@ -9,17 +9,18 @@ import pytest
|
||||
from tests.installation_launcher_fixture import publish_fixture_launcher
|
||||
|
||||
if os.name == 'posix':
|
||||
import fcntl
|
||||
import pty
|
||||
import termios
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
def _with_controlling_terminal(slave: int):
|
||||
"""preexec_fn: make ``slave`` this child's controlling terminal, so /dev/tty opens."""
|
||||
tty_path = os.ttyname(slave)
|
||||
|
||||
def attach() -> None:
|
||||
os.setsid() # windows-footgun: ok (posix-only test)
|
||||
fcntl.ioctl(slave, termios.TIOCSCTTY, 0)
|
||||
fd = os.open(tty_path, os.O_RDWR)
|
||||
os.close(fd)
|
||||
return attach
|
||||
|
||||
|
||||
|
||||
@@ -336,11 +336,13 @@ def candidate_workflow_step(tmp_path, r2_server, staged_candidate):
|
||||
'${{ needs.admit.outputs.commit }}': manifest['commit'],
|
||||
'${{ needs.validate.outputs.archive-tag }}': manifest['archive'],
|
||||
'${{ needs.candidates.outputs.manifest-sha256 }}': pinned,
|
||||
'${{ needs.candidate-manifest.outputs.manifest-sha256 }}': pinned,
|
||||
'${{ vars.CLOUDFLARE_R2_PUBLIC_URL }}': base, '${{ toJSON(needs) }}': json.dumps(needs),
|
||||
}
|
||||
for key in ('CLOUDFLARE_R2_ACCOUNT_ID', 'CLOUDFLARE_R2_ACCESS_KEY_ID', 'CLOUDFLARE_R2_SECRET_ACCESS_KEY', 'CLOUDFLARE_R2_BUCKET'):
|
||||
expressions['${{ ' + ('vars.' if key.endswith('_BUCKET') else 'secrets.') + key + ' }}'] = os.environ[key]
|
||||
env = {**os.environ, 'GITHUB_SHA': manifest['commit'], 'GITHUB_REPOSITORY': 'fixture/release',
|
||||
'RELEASE_CLAIM_TAG': manifest['archive'],
|
||||
'PATH': str(bin_dir) + os.pathsep + os.environ['PATH'], 'GITHUB_OUTPUT': str(tmp_path / 'outputs')}
|
||||
env.pop('RELEASE_NEEDS', None)
|
||||
if ambient_needs is not None:
|
||||
@@ -360,13 +362,8 @@ def test_candidate_smoke_survives_real_promotion_and_renderer(tmp_path, r2_serve
|
||||
candidate_workflow_step, ambient_needs, https_origin):
|
||||
manifest, _, base = staged_candidate
|
||||
jobs, run, body_file = candidate_workflow_step
|
||||
candidate = next(step for step in jobs['candidate-manifest']['steps'] if step.get('id') == 'manifest')
|
||||
needs = {name: {'result': 'success'} for name in jobs['candidate-manifest']['needs']}
|
||||
result = run('candidate-manifest', candidate, needs)
|
||||
assert result.returncode == 0, result.stdout + result.stderr
|
||||
stored = json.loads(r2_server.store[f"releases/tag/{manifest['archive']}/release-candidates.json"][0])
|
||||
assert stored['smoke_results'] == SMOKE_RESULTS
|
||||
assert f'manifest-sha256={artifacts.sha256_file(tmp_path / "release-candidates.json")}' in (tmp_path / 'outputs').read_text(encoding='utf-8-sig')
|
||||
# An unrelated orphan object must not acquire the candidate's Passed label.
|
||||
orphan = f"releases/tag/{manifest['archive']}/HermesBundled-1.2.3-linux-x64.AppImage"
|
||||
r2_server.store[orphan] = (b'orphan transport fixture', '"e"')
|
||||
@@ -390,19 +387,6 @@ def test_candidate_smoke_survives_real_promotion_and_renderer(tmp_path, r2_serve
|
||||
def test_candidate_smoke_admission_fails_before_publication(tmp_path, r2_server, staged_candidate, candidate_workflow_step):
|
||||
manifest, _, _ = staged_candidate
|
||||
jobs, run, body_file = candidate_workflow_step
|
||||
candidate = next(step for step in jobs['candidate-manifest']['steps'] if step.get('id') == 'manifest')
|
||||
needs = {name: {'result': 'success'} for name in jobs['candidate-manifest']['needs']}
|
||||
for name in SMOKE_RESULTS:
|
||||
for outcome in ('failure', 'cancelled', 'skipped', None):
|
||||
failed = copy.deepcopy(needs)
|
||||
if outcome is None:
|
||||
del failed[name]
|
||||
else:
|
||||
failed[name]['result'] = outcome
|
||||
r2_server.requests.clear()
|
||||
result = run('candidate-manifest', candidate, failed)
|
||||
assert result.returncode != 0 and name in result.stderr, result.stdout + result.stderr
|
||||
assert not any(method == 'PUT' for method, _, _ in r2_server.requests)
|
||||
key = f"releases/tag/{manifest['archive']}/release-candidates.json"
|
||||
raw = r2_server.store[key][0]
|
||||
for fault, message in [('legacy', 'Candidate manifest'), ('missing', 'Candidate smoke results'),
|
||||
|
||||
Reference in New Issue
Block a user