Files
hermes-agent/tests/hermes_cli/test_local_component_jobs.py
ethernet 4e100d41d5 test(local-models): the component-job pause probe runs PM's engine in-process
A fixture package definition exists only in the test process; take the
resident-runtime seam so the router's ensure() reaches the same pm.install
pause contract without a worker re-import.
2026-09-19 04:28:01 -04:00

108 lines
4.7 KiB
Python

"""Desktop component jobs pause real PM archive transfers before activation."""
from __future__ import annotations
import hashlib
import threading
import pytest
import pm
from pm import paths, registry
from pm.lock import Facts, Lockfile
from pm.packages import LlamaCppCpu
from tests.hermes_cli.test_local_download_jobs import client, poll # noqa: F401
from tests.pm._range_server import RangeHandler, dl_server, url # noqa: F401
from tests.pm.test_install_download_control import archive
class ArchiveEngine(LlamaCppCpu):
probe_version = False
@pytest.mark.parametrize("endpoint", ["runtime/install", "quickstart"])
def test_component_job_pause_retains_pin_and_stops_sequence(client, monkeypatch, tmp_path, dl_server, endpoint):
from hermes_cli.web_routers import local_models as lm
package = ArchiveEngine()
monkeypatch.setitem(registry._packages, package.name, package)
# The router hands `ensure` to PM's client; a fixture package definition only exists in this
# process, so take the resident-runtime path and run the same engine in-process. The pause
# contract under test lives in pm.install/pm.downloader either way.
monkeypatch.setattr("pm.client.is_runtime", lambda: True)
root = tmp_path / "store"
lock_path = tmp_path / "lock.json"
monkeypatch.setattr(paths, "store_root", lambda: root)
monkeypatch.setattr(paths, "lockfile_path", lambda: lock_path)
monkeypatch.setattr(lm, "_runtime_section", lambda: {"backend": "cpu"})
target = pm.current_target()
binary_name = package.binary(root, target).name
files = {binary_name: b"archive engine fixture", "runtime.dll": bytes(range(256)) * (32 * 1024)}
pins = []
for index, (name, body) in enumerate(files.items()):
payload = archive({name: body})
path = f"/component-{index}.zip"
RangeHandler.payloads[path] = payload
pins.append({"url": url(dl_server, path), "sha256": hashlib.sha256(payload).hexdigest()})
lock = Lockfile(lock_path)
lock.set_pin(package.name, "10362", {target: pins})
lock.save()
entry = lm.catalog.CATALOG[0]
variant = entry.variants[0]
monkeypatch.setattr(lm, "_quickstart_target", lambda *args: (entry, variant))
model = lm.bootstrap.models_dir() / "test.gguf"
RangeHandler.payloads["/model"] = b"model fixture"
monkeypatch.setattr(lm, "_download_plan", lambda *args: [(url(dl_server, "/model"), model, 13)])
calls = []
monkeypatch.setattr(lm, "_ensure_server", lambda *args, **kwargs: calls.append("server"))
monkeypatch.setattr(lm, "_assign_default", lambda *args: calls.append("default"))
monkeypatch.setattr(lm.bootstrap, "get_supervisor", lambda: None)
flowing, release = threading.Event(), threading.Event()
real_hook = lm._runtime_progress_hook
def observed_hook(job):
hook = real_hook(job)
first_archive_bytes = 0
def tick(stage, done, total, label):
nonlocal first_archive_bytes
hook(stage, done, total, label)
if stage == "download" and label == "1/2":
first_archive_bytes = done
if stage == "download" and label == "2/2" and first_archive_bytes < done < total and not flowing.is_set():
flowing.set()
assert release.wait(15)
return tick
monkeypatch.setattr(lm, "_runtime_progress_hook", observed_hook)
response = client.post(f"/api/local-models/{endpoint}", json={"backend": "cpu"} if endpoint == "runtime/install" else {})
assert response.status_code == 200, response.text
job_id = response.json()["job_id"]
try:
assert flowing.wait(5), "runtime did not enter PM's pinned archive transfer"
paused = client.post("/api/local-models/download/pause", json={"job_id": job_id})
assert paused.json()["paused"], paused.text
finally:
release.set()
job = poll(client, job_id)
assert job["status"] == "paused", job
assert Facts(paths.facts_path()).get(package.name) is None
assert calls == [] and not model.exists()
assert job["can_resume"]
assert list(paths.partials_root().glob("*.ranges"))
assert client.post("/api/local-models/download/resume", json={"job_id": job_id}).json()["resumed"]
job = poll(client, job_id, until=("done", "error"))
assert job["status"] == "done", job
selected = pm.installed_package(package.name)
assert selected is not None
for name, body in files.items():
assert (selected.path / name).read_bytes() == body
assert Facts(paths.facts_path()).get(package.name)["artifacts"] == [p["sha256"] for p in pins]
if endpoint == "quickstart":
assert calls == ["server", "default"] and model.read_bytes() == RangeHandler.payloads["/model"]
assert job_id not in lm._RUNNING