"""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 RunContainer, 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) def test_container_secrets_use_env_file_not_argv() -> None: """Secrets go through --env-file; plain vars stay as -e; no secret in argv.""" run = RunContainer(runner="container", image="img:latest", env={"PLAIN": "1"}) env_file = Path("/home/u/.castle/secrets/env/castle-svc.service.env") with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"): cmd = _build_run_cmd("svc", run, {"PORT": "9001"}, [], secret_env_file=env_file) joined = " ".join(cmd) assert "--env-file" in cmd assert str(env_file) in cmd # plain (non-secret) vars are still inlined as -e assert "-e" in cmd and "PORT=9001" in joined and "PLAIN=1" in joined # no resolved secret value leaks into argv (only the file path is referenced) assert "SECRET=" not in joined def test_container_without_secrets_has_no_env_file() -> None: run = RunContainer(runner="container", image="img:latest") with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/docker"): cmd = _build_run_cmd("svc", run, {}, [], secret_env_file=None) assert "--env-file" not in cmd