"""Real isolated workers: no parent imports or callables cross the wire.""" from __future__ import annotations import importlib from pathlib import Path import subprocess import shutil import sys import pytest from pm import paths from pm.package import InstallError from pm.plugin_inputs import Members, Selection from pm.runtime import runtime_python from tests.pm._range_server import RangeHandler, dl_server, url # noqa: F401 from tests.pm.test_runtime_wheelhouse import locked_wheelhouse # noqa: F401 from tests.pm._fixtures import ( _wheel, build_worker as build_worker, client as client, isolated_python as isolated_python, ) # Spawns children with a home it builds itself; the parent's must stay real. pytestmark = pytest.mark.real_machine_home def test_isolated_worker_preserves_install_error(client, monkeypatch): engine = importlib.import_module("pm.install") monkeypatch.setattr(engine, "ensure", lambda *a, **kw: pytest.fail("engine ran in caller")) with pytest.raises(InstallError) as caught: client.ensure("node", explicit=True) assert caught.value.package == "node" assert caught.value.cause == "not in the lockfile" assert caught.value.remedy == "add it with `hermes pm lock --bump`" assert not paths.facts_path().exists() def test_refused_or_already_paused_install_does_not_acquire_runtime(client, monkeypatch): import threading from pm.downloader import DownloadPaused monkeypatch.setattr(client, "runtime_command", lambda path, **kwargs: pytest.fail("refusal acquired PM runtime")) monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1") with pytest.raises(InstallError, match="lazy installs are disabled"): client.ensure("node") paused = threading.Event() paused.set() with pytest.raises(DownloadPaused): client.ensure("node", explicit=True, pause_event=paused) def _current_environment(tmp_path, monkeypatch, members): from pm.environments import install_state_dir from pm.lock import Facts from pm.packages import Venv repo = tmp_path / "project" repo.mkdir() (repo / "uv.lock").write_text("version = 1\n") monkeypatch.setattr(paths, "repo_root", lambda: repo) environment = install_state_dir(repo) / "environments" / "existing" / "venv" environment.mkdir(parents=True) (environment / "pyvenv.cfg").write_text("home = test\n") Facts(paths.runtime_facts_path()).record_state( "venv", Venv().expected_stamp([], plugin_dirs=members), [], environment=environment, ) return repo @pytest.mark.parametrize("member_shape", ["paths", "sources"]) @pytest.mark.parametrize("route", ["worker", "direct", "foreign-runtime"]) def test_currency_probe_preserves_union_and_candidate_inputs(client, tmp_path, monkeypatch, isolated_python, member_shape, route): import json from pm.environments import runtime_facts_path, selected_venv from pm.lock import Facts from pm.packages import Venv candidate = tmp_path / "candidate" candidate.mkdir() manifest = candidate / "plugin.yaml" manifest.write_text('name: candidate\npython_dependencies: ["candidate-dep==1"]\n') members = [candidate] if member_shape == "paths" else {tmp_path / "installed": candidate} repo = _current_environment(tmp_path, monkeypatch, members) environment = selected_venv(repo) recorded = ["incumbent-extra", "provider-extra"] facts_path = runtime_facts_path(repo) Facts(facts_path).record_state( "venv", Venv(repo).expected_stamp(recorded, plugin_dirs=members), recorded, environment=environment, ) monkeypatch.setattr(client, "is_runtime", lambda: route != "worker") if route == "foreign-runtime": monkeypatch.setattr(paths, "repo_root", lambda: tmp_path / "other-project") root_args = {"project_root": repo} if route != "worker" else {} acquisitions = [] def ready_runtime(*, bootstrap, cache): assert bootstrap is False, "currency probe attempted to bootstrap PM" acquisitions.append(bootstrap) return isolated_python monkeypatch.setattr("pm.runtime.runtime_python", ready_runtime) if route != "direct": engine = importlib.import_module("pm.install") monkeypatch.setattr(engine, "venv_is_current", lambda **kw: pytest.fail("probe ran in caller")) def snapshot(): return {path.relative_to(tmp_path): (path.read_bytes() if path.is_file() else None) for path in tmp_path.rglob("*")} from pm.lock import Lockfile pins = Lockfile(paths.lockfile_path()) pins.set_pin("python", "test.1", {"any": {"url": "https://example.invalid/python", "sha256": "1" * 64}}) pins.save() Facts(facts_path).record_state("venv", Venv(repo).expected_stamp(recorded, plugin_dirs=members), recorded, environment=environment) before = snapshot() assert client.venv_is_current(extras=["provider-extra"], plugins=Members(members), **root_args) assert client.venv_is_current(extras=[], plugins=Members(members), **root_args) assert not client.venv_is_current(extras=["new-extra"], plugins=Members(members), **root_args) assert not client.venv_is_current(extras=recorded, plugins=Members([]), **root_args) assert snapshot() == before, "currency queries changed dependency state" assert bool(acquisitions) is (route != "direct") pins.set_pin("uv", "unrelated", {"any": {"url": "https://example.invalid/uv", "sha256": "3" * 64}}) pins.save() unchanged = snapshot() assert client.venv_is_current(extras=[], plugins=Members(members), **root_args) assert snapshot() == unchanged for version, digest in [("test.1", "2" * 64), ("test.2", "1" * 64)]: pins.set_pin("python", version, {"any": {"url": "https://example.invalid/python", "sha256": digest}}) pins.save() changed = snapshot() assert not client.venv_is_current(plugins=Members(members), **root_args) assert snapshot() == changed pins.set_pin("python", "test.1", {"any": {"url": "https://example.invalid/python", "sha256": "1" * 64}}) pins.save() marker = environment / "pyvenv.cfg" contents = marker.read_bytes() marker.unlink() changed = snapshot() assert not client.venv_is_current(plugins=Members(members), **root_args) assert snapshot() == changed marker.write_bytes(contents) assert client.venv_is_current(plugins=Members(members), **root_args) manifest.write_text('name: candidate\npython_dependencies: ["candidate-dep==2"]\n') changed = snapshot() assert not client.venv_is_current(extras=["provider-extra"], plugins=Members(members), **root_args) assert snapshot() == changed assert selected_venv(repo) == environment # Corruption must not be mistaken for a missing or current environment. data = json.loads(facts_path.read_text()) data["packages"]["venv"]["extras"] = "provider-extra" facts_path.write_text(json.dumps(data)) malformed = snapshot() with pytest.raises(ValueError, match="invalid recorded dependency state"): client.venv_is_current(extras=[], plugins=Members(members), **root_args) assert snapshot() == malformed def _assert_worker_holds_lock(repo): from pm.environments import install_state_dir from pm.filesystem import lock_fd with (install_state_dir(repo) / ".install.lock").open("a+b") as lock: assert not lock_fd(lock.fileno(), wait=False), "callback escaped the worker's runtime lock" @pytest.mark.parametrize("explicit", [True, False]) def test_sync_discovers_profile_members_after_worker_acquires_lock(client, tmp_path, monkeypatch, isolated_python, explicit): from concurrent.futures import ThreadPoolExecutor import time from hermes_cli.runtime_state import runtime_lock from tests.pm._fixtures import worker_toolchain sibling = tmp_path / "home/profiles/sibling/plugins/dependency" sibling.mkdir(parents=True) (sibling / "plugin.yaml").write_text("name: dependency\npython_dependencies: [fixture-dep==1]\n") repo = _current_environment(tmp_path, monkeypatch, [sibling]) ready = tmp_path / "waiting-for-lock" worker_toolchain(client, monkeypatch, isolated_python, "from contextlib import contextmanager\nimport hermes_cli.runtime_state as state\n" "original = state.runtime_lock\n@contextmanager\ndef lock(project, **kwargs):\n" f" Path({str(ready)!r}).touch()\n" " with original(project, **kwargs) as held:\n yield held\nstate.runtime_lock = lock\n") with ThreadPoolExecutor() as executor: with runtime_lock(repo): future = executor.submit(client.sync_venv, explicit=explicit, plugins=Selection({ "home": str(tmp_path / "home"), "enabled": ["plain"], "disabled": [], })) deadline = time.monotonic() + 15 while not ready.exists() and time.monotonic() < deadline: time.sleep(0.01) assert ready.exists(), "worker never reached the install lock" assert not future.done(), "worker ignored the install lock" (sibling.parent.parent / "config.yaml").write_text("plugins:\n enabled: [dependency]\n") future.result(timeout=30) assert client.venv_is_current() @pytest.mark.parametrize("current", [True, False]) @pytest.mark.parametrize("tools_present", [False, True], ids=["cold-tools", "ready-tools"]) def test_lazy_disabled_sync_does_not_bootstrap_tools(client, tmp_path, monkeypatch, isolated_python, current, tools_present): import json from pm import receipt repo = _current_environment(tmp_path, monkeypatch, []) if not current: (repo / "uv.lock").write_text("version = 2\n") monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1") # Exercise runtime acquisition too: the other worker tests supply a ready # interpreter, which hides an explicit tool install before worker refusal. monkeypatch.setattr("pm.runtime.runtime_python", runtime_python) from pm import _uv original_toolchain = _uv._toolchain def toolchain(**kwargs): assert kwargs.get("realize") is False, "lazy-disabled sync bootstrapped tools" if tools_present: return tmp_path / "uv", isolated_python return original_toolchain(**kwargs) monkeypatch.setattr(_uv, "_toolchain", toolchain) monkeypatch.setattr("pm.runtime_stage.stage_runtime", lambda *a, **kw: pytest.fail("lazy-disabled sync prepared PM runtime")) with receipt.worker_context("lazy-disabled-sync"): with pytest.raises(InstallError, match="lazy installs are disabled") as caught: client.sync_venv([], plugins=Members([])) result = receipt.last_for_update("lazy-disabled-sync", consume=True) assert caught.value.package == "pm-runtime" assert result is not None assert result["outcome"] == "failed" receipts = list((tmp_path / "home" / "logs" / "update_receipts").glob("pm_*.json")) assert len(receipts) == 1 assert json.loads(receipts[0].read_text()) == result assert not paths.facts_path().exists() def test_invalid_selection_waits_for_failed_receipt_and_lock_release(client, tmp_path, monkeypatch): import json from pm.environments import install_state_dir from pm.filesystem import lock_fd repo = _current_environment(tmp_path, monkeypatch, []) home = tmp_path / "home" (home / "config.yaml").write_text("plugins: []\n") with pytest.raises(ValueError, match="plugins must be a mapping"): client.sync_venv([], explicit=True, plugins=Selection({"home": str(home), "enabled": [], "disabled": []})) receipts = list((home / "logs" / "update_receipts").glob("pm_*.json")) assert len(receipts) == 1 assert json.loads(receipts[0].read_text())["outcome"] == "failed" with (install_state_dir(repo) / ".install.lock").open("a+b") as lock: assert lock_fd(lock.fileno(), wait=False) def _node_archive(server, body=b"#!/bin/sh\nexit 0\n"): import hashlib import io import zipfile from pm.lock import Lockfile from pm.registry import get_package from pm.store import current_target target = current_target() package = get_package("node") relative = package.binary(Path("."), target) assert relative is not None stream = io.BytesIO() with zipfile.ZipFile(stream, "w") as archive: info = zipfile.ZipInfo((Path("node-package") / relative).as_posix()) info.external_attr = 0o100755 << 16 archive.writestr(info, body) payload = stream.getvalue() RangeHandler.payloads["/node.zip"] = payload lock = Lockfile(paths.lockfile_path()) lock.set_pin("node", "1", {target: {"url": url(server, "/node.zip"), "sha256": hashlib.sha256(payload).hexdigest()}}) lock.save() return target, relative, body @pytest.mark.platforms("posix") @pytest.mark.parametrize("operation", ["ensure", "stage_only"]) def test_worker_artifact_lifecycle_keeps_identity_and_relays_progress(client, dl_server, operation): import hashlib from pm.lock import Facts, Lockfile from pm.store import tree_digest target, relative, body = _node_archive(dl_server) stages, downloads = [], [] def progress(*args): stages.append(args) return object() # Notifications never serialize caller-owned return values. if operation == "stage_only": entry = client.stage_only("node", target, progress=progress) assert isinstance(entry, Path) assert not paths.facts_path().exists() else: base = {"PATH": "/caller/bin", "CALLER": "kept", "PYTHONPATH": "/caller/dependencies"} runner = client.ensure("node", explicit=True, base_env=base, progress=lambda *args: stages.append(args), download_progress=lambda *args: downloads.append(args)) fact = Facts(paths.facts_path()).get("node") entry = paths.store_root() / fact["entry"] assert runner.env["CALLER"] == "kept" assert runner.env["PYTHONPATH"] == base["PYTHONPATH"] assert runner.env["PATH"].endswith(base["PATH"]) assert downloads and downloads[-1][0] == downloads[-1][1] assert all(isinstance(row, tuple) for rows in downloads[-1][2].values() for row in rows) assert (entry / relative).read_bytes() == body assert stages and stages[0][0] == "download" def install(): if operation == "stage_only": return client.stage_only("node", target) client.ensure("node", explicit=True) return entry first_tree = tree_digest(entry) RangeHandler.payloads.clear() assert install() == entry assert tree_digest(entry) == first_tree _, _, replacement = _node_archive(dl_server, b"#!/bin/sh\nexit 0\n# repinned\n") assert install() == entry assert (entry / relative).read_bytes() == replacement assert not list(paths.store_root().glob("fetch-*")) good_tree = tree_digest(entry) previous_facts = paths.facts_path().read_bytes() if paths.facts_path().exists() else None lock = Lockfile(paths.lockfile_path()) RangeHandler.payloads["/node.zip"] = b"not an archive" lock.set_pin("node", "1", {target: {"url": url(dl_server, "/node.zip"), "sha256": hashlib.sha256(b"not an archive").hexdigest()}}) lock.save() with pytest.raises(RuntimeError if operation == "stage_only" else InstallError, match="zip"): install() assert tree_digest(entry) == good_tree if operation == "stage_only": assert not paths.facts_path().exists(), "foreign staging cannot publish host identity" else: assert paths.facts_path().read_bytes() == previous_facts @pytest.mark.platforms("posix") @pytest.mark.parametrize("from_progress", [False, True]) def test_pause_event_reaches_running_worker_without_hanging(client, dl_server, monkeypatch, from_progress): import threading from pm.downloader import DownloadPaused from pm.lock import Facts _node_archive(dl_server, b"#!/bin/sh\nexit 0\n#" + b"x" * (8 << 20)) RangeHandler.slow_per_chunk = 0.03 pause, transferring = threading.Event(), threading.Event() original = RangeHandler.do_GET def get(handler): if handler.headers.get("Range") not in (None, "bytes=0-0"): transferring.set() original(handler) monkeypatch.setattr(RangeHandler, "do_GET", get) def cancel(): assert transferring.wait(10), "worker never began transferring" pause.set() thread = None progress = None if from_progress: def progress(stage, done, total, label): if stage == "download" and done: pause.set() else: thread = threading.Thread(target=cancel) thread.start() try: with pytest.raises(DownloadPaused): client.ensure("node", explicit=True, progress=progress, pause_event=pause) finally: if thread is not None: thread.join(timeout=15) assert not thread.is_alive() assert Facts(paths.facts_path()).get("node") is None @pytest.mark.platforms("posix") def test_installed_tool_needs_no_pm_runtime(client, dl_server, monkeypatch): _node_archive(dl_server) client.ensure("node", explicit=True) monkeypatch.setattr("pm.runtime.runtime_python", lambda: pytest.fail("hot path bootstrapped PM")) assert client.ensure("node", base_env={"PATH": "caller"}).env["PATH"].endswith("caller") def test_environment_probe_needs_no_pm_runtime(client, monkeypatch, tmp_path): from pm import environment_python, python_tool monkeypatch.setattr("pm.runtime.runtime_python", lambda: pytest.fail("probe bootstrapped PM")) root = tmp_path / "not-created" assert environment_python("test", root=root) is None assert python_tool("test", "test", root=root) is None assert not root.exists() def test_worker_receipt_is_exact_even_if_latest_is_replaced(client, tmp_path, monkeypatch): import json from pm import receipt _current_environment(tmp_path, monkeypatch, []) original_accept = receipt.accept_worker_receipt received = [] def accept(data, update_id): # Simulate a different process publishing after this worker completes. point = tmp_path / "home" / "logs" / "update_receipts" / "latest.json" point.write_text(json.dumps({"update_id": "unrelated", "outcome": "failed"})) received.append(data) original_accept(data, update_id) monkeypatch.setattr(receipt, "accept_worker_receipt", accept) with receipt.worker_context("my-update"): client.sync_venv([], explicit=True, plugins=Members([])) result = receipt.last_for_update("my-update", consume=True) assert received and result == received[0] assert result["update_id"] == "my-update" and result["outcome"] == "ok" def _patch_worker_apply(client, monkeypatch, isolated_python, body): """Fault injection at the build boundary, not an alternate transport/engine.""" import textwrap worker = Path(client.__file__).with_name("worker.py") script = ( "import os, runpy, sys\n" f"sys.path.insert(0, {str(worker.parent.parent)!r})\n" "from pm.packages import Venv\n" "from pm.workspace import ResolutionConflict\n" "def apply(self, *args, **kwargs):\n" + textwrap.indent(body, " ") + "\n" "Venv.apply = apply\n" f"runpy.run_path({str(worker)!r}, run_name='__main__')\n" ) monkeypatch.setattr(client, "runtime_command", lambda path, **kwargs: [str(isolated_python), "-I", "-B", "-c", script]) def test_resolution_conflict_survives_worker_and_receipt(client, tmp_path, monkeypatch, isolated_python, capfd): from pm import receipt from pm.workspace import ResolutionConflict repo = _current_environment(tmp_path, monkeypatch, []) (repo / "uv.lock").write_text("version = 2\n") _patch_worker_apply(client, monkeypatch, isolated_python, "print('engine stdout', flush=True)\n" "os.write(1, b'native stdout\\n')\n" "raise ResolutionConflict('venv', 'impossible union', 'change member')") with receipt.worker_context("conflict-update"): with pytest.raises(ResolutionConflict) as caught: client.sync_venv([], explicit=True, plugins=Members([])) result = receipt.last_for_update("conflict-update", consume=True) assert (caught.value.package, caught.value.cause, caught.value.remedy) == ( "venv", "impossible union", "change member") assert result["outcome"] == "failed" assert "engine stdout" in capfd.readouterr().err def test_failed_facts_write_restores_exact_config_before_reporting(client, tmp_path, monkeypatch, isolated_python): from tests.pm._fixtures import worker_toolchain from pm.environments import install_state_dir repo = _current_environment(tmp_path, monkeypatch, []) home = tmp_path / "home" config = home / "config.yaml" config.write_text("# preserve me\nplugins: {enabled: [old]}\n") previous = config.read_bytes() facts = (install_state_dir(repo) / "facts.json").read_bytes() (repo / "uv.lock").write_text("version = 2\n") worker_toolchain(client, monkeypatch, isolated_python, "from pm.packages import Venv\nfrom pm.lock import Facts\n" "Venv.apply = lambda *args, **kwargs: {}\n" "def fail(*args, **kwargs):\n" f" assert b'new' in Path({str(config)!r}).read_bytes()\n" " raise OSError('facts disk full')\nFacts.record_state = fail\n") with pytest.raises(OSError, match="facts disk full"): client.sync_venv(explicit=True, plugins=Selection({"home": str(home), "enabled": ["new"], "disabled": []})) assert config.read_bytes() == previous assert (install_state_dir(repo) / "facts.json").read_bytes() == facts assert not (install_state_dir(repo) / "publication.json").exists() def test_invalid_arguments_keep_the_engine_exception_type(client): with pytest.raises(ValueError, match="repair restores"): client.sync_venv([], repair=True) with pytest.raises(ValueError, match="environment name"): client.ensure_environment("../escape", ["example==1"], explicit=True) with pytest.raises(KeyError): client.ensure("no-such-package", explicit=True) def test_worker_death_reports_transport_failure(client, monkeypatch, isolated_python): monkeypatch.setattr(client, "runtime_command", lambda path, **kwargs: [str(isolated_python), "-I", "-c", "import os; os._exit(7)"]) with pytest.raises(InstallError, match="worker.*result"): client.ensure("node", explicit=True) def test_foreign_checkout_sync_uses_its_own_pm_generation(client, tmp_path, monkeypatch, isolated_python): from pm import venv_is_current from pm.environments import selected_venv, runtime_facts_path from tests.pm._fixtures import worker_toolchain worker_toolchain(client, monkeypatch, isolated_python) foreign = tmp_path / "other checkout" foreign.mkdir() (foreign / "pyproject.toml").write_text( '[project]\nname="foreign-proof"\nversion="1"\nrequires-python=">=3.11"\n' '[tool.uv]\npackage=false\n', encoding="utf-8") client.lock_project(foreign, offline=True, explicit=True) assert not venv_is_current(project_root=foreign) original_root = paths.repo_root() client.sync_venv([], project_root=foreign, plugins=Members([]), explicit=True) selected = selected_venv(foreign) assert selected != foreign / "venv" assert selected.is_relative_to(runtime_facts_path(foreign).parent) assert runtime_facts_path(foreign).is_file() assert venv_is_current(project_root=foreign) assert paths.repo_root() == original_root assert not runtime_facts_path(original_root).exists() def test_cold_manager_build_does_not_bootstrap_a_worker(client, tmp_path, monkeypatch, locked_wheelhouse): import pm wheels, _ = locked_wheelhouse uv = shutil.which("uv") assert uv monkeypatch.setattr(client, "runtime_command", lambda *a, **kw: pytest.fail("manager build requested itself")) monkeypatch.setattr("pm._uv._toolchain", lambda **kwargs: (Path(uv), Path(sys.executable)) if kwargs == {"realize": False} else pytest.fail("bootstrap tried to install tools")) project = Path(__file__).resolve().parents[2] / "pm" python = pm.stage_manager_runtime(python=Path(sys.executable), destination=tmp_path / "manager", project=project, wheelhouse=wheels, offline=True) result = subprocess.run([str(python), "-I", "-c", "import packaging, tomli_w, truststore; from ruamel.yaml import YAML"], capture_output=True, text=True, timeout=30) assert result.returncode == 0, result.stderr with pytest.raises(FileExistsError): pm.stage_manager_runtime(python=Path(sys.executable), destination=python.parent.parent) def test_worker_side_environment_reuses_and_keeps_selection_on_failed_tool(client, tmp_path, monkeypatch, isolated_python): import zipfile from pm import environment_python, python_tool from tests.pm._fixtures import worker_toolchain worker_toolchain(client, monkeypatch, isolated_python) wheel = _wheel(tmp_path, "side_dep") with zipfile.ZipFile(wheel, "a") as archive: archive.writestr("side_dep/cli.py", "def main():\n print('real side dependency')\n") archive.writestr("side_dep-1.0.dist-info/entry_points.txt", "[console_scripts]\nside-proof = side_dep.cli:main\n") requirements = [f"side-dep @ {wheel.as_uri()}"] root = tmp_path / "side environment" assert environment_python("proof", root=root) is None executable = client.ensure_python_tool("proof", requirements, "side-proof", root=root, explicit=True) assert python_tool("proof", "side-proof", root=root) == executable result = subprocess.run([str(executable)], capture_output=True, text=True, timeout=30) assert result.returncode == 0, result.stderr assert result.stdout.strip() == "real side dependency" selected = environment_python("proof", root=root) selection = (root / "active.json").read_bytes() generations = set(root.glob("gen-*")) assert client.ensure_environment("proof", requirements, root=root) == selected assert set(root.glob("gen-*")) == generations with pytest.raises(InstallError, match="do not provide"): client.ensure_python_tool("proof", requirements, "missing-command", root=root, explicit=True) assert (root / "active.json").read_bytes() == selection assert set(root.glob("gen-*")) == generations assert python_tool("proof", "side-proof", root=root) == executable monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1") with pytest.raises(InstallError, match="lazy installs are disabled"): client.ensure_environment("proof", ["absent-dependency==0"], root=root) assert (root / "active.json").read_bytes() == selection @pytest.mark.parametrize("operation", ["activate", "stage_manager_runtime"]) @pytest.mark.parametrize("route", ["client", "wire"]) def test_unknown_worker_operation_is_not_dispatched(client, monkeypatch, tmp_path, isolated_python, operation, route): import json arguments = {"destination": str(tmp_path / "unused")} if route == "client": monkeypatch.setattr(client, "runtime_command", lambda *a, **kw: pytest.fail("unsupported operation acquired PM")) with pytest.raises(KeyError, match=operation): client._request(operation, arguments) else: request = {"id": "unsupported", "operation": operation, "arguments": arguments, "callbacks": [], "packages": [], "context": {"repo": str(paths.repo_root()), "lockfile": str(paths.lockfile_path())}} worker = Path(client.__file__).with_name("worker.py") result = subprocess.run(client.runtime_command(worker), input=json.dumps(request) + "\n", capture_output=True, text=True, encoding="utf-8", timeout=30, env=client.runtime_environment()) assert result.returncode == 0, result.stderr response = json.loads(result.stdout) assert response["error"]["type"] == "KeyError", response assert operation in response["error"]["message"] assert not paths.facts_path().exists()