A python-runner service ran from a tool venv (uv tool install --editable), separate from the project venv that uv sync manages. The editable link kept source live, but dependencies were resolved independently in each venv, so a new dependency landed in the project venv and never reached the running service — leaving no clean rollout for it. Deploy now emits ExecStart=uv run --project <source> --no-dev <program>, running the service in place from its own project venv. uv run syncs the env to the lockfile before launching, so a single restart picks up both code and dependency changes; the ExecStart is deterministic from source and never goes stale. The tool-venv install is gated to the command runner only; uv tool install is reserved for tool-behavior programs, where PATH is the point. A python service with no resolvable source falls back to a PATH lookup.
51 lines
2.0 KiB
Python
51 lines
2.0 KiB
Python
"""Tests for `_build_run_cmd` — the python runner runs in place via `uv run`."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from castle_core.deploy import _build_run_cmd
|
|
from castle_core.manifest import RunPython
|
|
|
|
|
|
def test_python_runner_uses_uv_run_from_source(tmp_path: Path) -> None:
|
|
"""A python service with a real source dir launches via `uv run --project`."""
|
|
run = RunPython(runner="python", program="my-svc")
|
|
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/uv"):
|
|
cmd = _build_run_cmd("my-svc", run, {}, [], source_dir=tmp_path)
|
|
assert cmd == [
|
|
"/usr/bin/uv",
|
|
"run",
|
|
"--project",
|
|
str(tmp_path),
|
|
"--no-dev",
|
|
"my-svc",
|
|
]
|
|
|
|
|
|
def test_python_runner_appends_args(tmp_path: Path) -> None:
|
|
run = RunPython(runner="python", program="my-svc", args=["--flag", "x"])
|
|
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/uv"):
|
|
cmd = _build_run_cmd("my-svc", run, {}, [], source_dir=tmp_path)
|
|
assert cmd[-2:] == ["--flag", "x"]
|
|
assert cmd[:5] == ["/usr/bin/uv", "run", "--project", str(tmp_path), "--no-dev"]
|
|
|
|
|
|
def test_python_runner_falls_back_to_path_without_source() -> None:
|
|
"""No resolvable source → PATH lookup of the script (no uv run)."""
|
|
run = RunPython(runner="python", program="my-svc")
|
|
with patch("castle_core.deploy.shutil.which", return_value="/home/u/.local/bin/my-svc"):
|
|
cmd = _build_run_cmd("my-svc", run, {}, [], source_dir=None)
|
|
assert cmd == ["/home/u/.local/bin/my-svc"]
|
|
|
|
|
|
def test_python_runner_warns_when_unresolvable() -> None:
|
|
"""No source and not on PATH → a warning, bare program name as last resort."""
|
|
run = RunPython(runner="python", program="my-svc")
|
|
messages: list[str] = []
|
|
with patch("castle_core.deploy.shutil.which", return_value=None):
|
|
cmd = _build_run_cmd("my-svc", run, {}, messages, source_dir=None)
|
|
assert cmd == ["my-svc"]
|
|
assert any("my-svc" in m for m in messages)
|