feat: Enhance node runner to execute scripts in source directory and update tests

This commit is contained in:
2026-06-28 13:18:28 -07:00
parent 3f1b2e6f56
commit 48c3915e4d
2 changed files with 44 additions and 2 deletions

View File

@@ -465,7 +465,16 @@ def _build_run_cmd(
cmd.extend(run.args) # type: ignore[union-attr] cmd.extend(run.args) # type: ignore[union-attr]
return cmd return cmd
case "node": case "node":
cmd = [run.package_manager, "run", run.script] # type: ignore[union-attr] # Like the python runner bakes `--project <source>` into `uv run`, the
# node runner bakes `--dir <source>` so the package manager runs the
# script in the program's source dir — the systemd unit carries no
# WorkingDirectory, so a bare `pnpm run` would otherwise execute in the
# service's (wrong) cwd. Resolve the package manager to an absolute path.
pm = shutil.which(run.package_manager) or run.package_manager # type: ignore[union-attr]
cmd = [pm]
if source_dir and source_dir.is_dir():
cmd.extend(["--dir", str(source_dir)])
cmd.extend(["run", run.script]) # type: ignore[union-attr]
if run.args: # type: ignore[union-attr] if run.args: # type: ignore[union-attr]
cmd.extend(run.args) # type: ignore[union-attr] cmd.extend(run.args) # type: ignore[union-attr]
return cmd return cmd

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from unittest.mock import patch from unittest.mock import patch
from castle_core.deploy import _build_run_cmd from castle_core.deploy import _build_run_cmd
from castle_core.manifest import RunContainer, RunPython from castle_core.manifest import RunContainer, RunNode, RunPython
def test_python_runner_uses_uv_run_from_source(tmp_path: Path) -> None: def test_python_runner_uses_uv_run_from_source(tmp_path: Path) -> None:
@@ -52,6 +52,39 @@ def test_python_runner_warns_when_unresolvable() -> None:
assert any("my-svc" in m for m in messages) assert any("my-svc" in m for m in messages)
def test_node_runner_bakes_source_dir(tmp_path: Path) -> None:
"""A node service runs the script in its source dir via `--dir` (no unit cwd)."""
run = RunNode(runner="node", script="gateway:watch:raw", package_manager="pnpm")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
cmd = _build_run_cmd("oc", run, {}, [], source_dir=tmp_path)
assert cmd == ["/usr/bin/pnpm", "--dir", str(tmp_path), "run", "gateway:watch:raw"]
def test_node_runner_appends_args(tmp_path: Path) -> None:
run = RunNode(
runner="node", script="start", package_manager="pnpm", args=["--port", "18789"]
)
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
cmd = _build_run_cmd("oc", run, {}, [], source_dir=tmp_path)
assert cmd == [
"/usr/bin/pnpm",
"--dir",
str(tmp_path),
"run",
"start",
"--port",
"18789",
]
def test_node_runner_without_source_omits_dir() -> None:
"""No resolvable source → bare `pnpm run` (package manager still PATH-resolved)."""
run = RunNode(runner="node", script="start", package_manager="pnpm")
with patch("castle_core.deploy.shutil.which", return_value="/usr/bin/pnpm"):
cmd = _build_run_cmd("oc", run, {}, [], source_dir=None)
assert cmd == ["/usr/bin/pnpm", "run", "start"]
def test_container_secrets_use_env_file_not_argv() -> None: def test_container_secrets_use_env_file_not_argv() -> None:
"""Secrets go through --env-file; plain vars stay as -e; no secret in argv.""" """Secrets go through --env-file; plain vars stay as -e; no secret in argv."""
run = RunContainer(runner="container", image="img:latest", env={"PLAIN": "1"}) run = RunContainer(runner="container", image="img:latest", env={"PLAIN": "1"})