"""Unit tests for the foundry run CLI command."""
import json
from unittest.mock import patch
import pytest
from foundry_mcp.cli.main import cli
class TestRunClaudeMissing:
"""Tests for error when claude CLI is not installed."""
@patch("foundry_mcp.cli.commands.run.shutil.which", return_value=None)
def test_claude_not_found_shows_error(self, mock_which, cli_runner, temp_specs_dir):
"""run returns clear error when claude CLI is not installed."""
result = cli_runner.invoke(
cli,
["--specs-dir", str(temp_specs_dir), "run", "test-spec-001"],
)
assert result.exit_code == 1
data = json.loads(result.output)
assert data["success"] is False
assert data["data"]["error_code"] == "DEPENDENCY_MISSING"
assert "claude" in data["error"]
@patch("foundry_mcp.cli.commands.run.shutil.which", return_value=None)
def test_claude_not_found_provides_remediation(self, mock_which, cli_runner, temp_specs_dir):
"""Error includes install instructions."""
result = cli_runner.invoke(
cli,
["--specs-dir", str(temp_specs_dir), "run", "test-spec-001"],
)
data = json.loads(result.output)
assert "claude" in data["data"].get("remediation", "").lower()
class TestRunSpecNotFound:
"""Tests for error when spec does not exist."""
@patch("foundry_mcp.cli.commands.run.shutil.which", return_value="/usr/bin/claude")
def test_error_when_spec_not_found(self, mock_which, cli_runner, temp_specs_dir):
"""run returns error for non-existent spec."""
result = cli_runner.invoke(
cli,
["--specs-dir", str(temp_specs_dir), "run", "nonexistent-spec"],
)
assert result.exit_code == 1
data = json.loads(result.output)
assert data["success"] is False
assert data["data"]["error_code"] == "NOT_FOUND"
class TestRunExecvpe:
"""Tests for correct command construction when launching claude."""
@patch("foundry_mcp.cli.commands.run.os.execvpe")
@patch("foundry_mcp.cli.commands.run.signal.alarm")
@patch("foundry_mcp.cli.commands.run.shutil.which", return_value="/usr/bin/claude")
def test_launches_claude_with_correct_args(self, mock_which, mock_alarm, mock_execvpe, cli_runner, temp_specs_dir):
"""run launches claude with the foundry-implement-auto skill."""
result = cli_runner.invoke(
cli,
["--specs-dir", str(temp_specs_dir), "run", "test-spec-001"],
)
mock_execvpe.assert_called_once()
args = mock_execvpe.call_args
cmd = args[0][1] # The command list
assert cmd[0] == "/usr/bin/claude"
assert "/foundry-implement-auto test-spec-001" in cmd[1]
@patch("foundry_mcp.cli.commands.run.os.execvpe")
@patch("foundry_mcp.cli.commands.run.signal.alarm")
@patch("foundry_mcp.cli.commands.run.shutil.which", return_value="/usr/bin/claude")
def test_unattended_includes_skip_permissions(
self, mock_which, mock_alarm, mock_execvpe, cli_runner, temp_specs_dir
):
"""Default unattended posture includes --dangerously-skip-permissions."""
result = cli_runner.invoke(
cli,
["--specs-dir", str(temp_specs_dir), "run", "test-spec-001"],
)
mock_execvpe.assert_called_once()
cmd = mock_execvpe.call_args[0][1]
assert "--dangerously-skip-permissions" in cmd
@patch("foundry_mcp.cli.commands.run.os.execvpe")
@patch("foundry_mcp.cli.commands.run.signal.alarm")
@patch("foundry_mcp.cli.commands.run.shutil.which", return_value="/usr/bin/claude")
def test_supervised_omits_skip_permissions(self, mock_which, mock_alarm, mock_execvpe, cli_runner, temp_specs_dir):
"""--posture supervised does NOT include --dangerously-skip-permissions."""
result = cli_runner.invoke(
cli,
["--specs-dir", str(temp_specs_dir), "run", "--posture", "supervised", "test-spec-001"],
)
mock_execvpe.assert_called_once()
cmd = mock_execvpe.call_args[0][1]
assert "--dangerously-skip-permissions" not in cmd
@patch("foundry_mcp.cli.commands.run.os.execvpe")
@patch("foundry_mcp.cli.commands.run.signal.alarm")
@patch("foundry_mcp.cli.commands.run.shutil.which", return_value="/usr/bin/claude")
def test_sets_environment_variables(self, mock_which, mock_alarm, mock_execvpe, cli_runner, temp_specs_dir):
"""run sets FOUNDRY_MCP_ROLE and FOUNDRY_MCP_AUTONOMY_POSTURE env vars."""
result = cli_runner.invoke(
cli,
["--specs-dir", str(temp_specs_dir), "run", "test-spec-001"],
)
mock_execvpe.assert_called_once()
env = mock_execvpe.call_args[0][2]
assert env["FOUNDRY_MCP_ROLE"] == "autonomy_runner"
assert env["FOUNDRY_MCP_AUTONOMY_POSTURE"] == "unattended"
@patch("foundry_mcp.cli.commands.run.os.execvpe")
@patch("foundry_mcp.cli.commands.run.signal.alarm")
@patch("foundry_mcp.cli.commands.run.shutil.which", return_value="/usr/bin/claude")
def test_cancels_alarm_before_exec(self, mock_which, mock_alarm, mock_execvpe, cli_runner, temp_specs_dir):
"""run cancels the SIGALRM timeout before launching claude."""
result = cli_runner.invoke(
cli,
["--specs-dir", str(temp_specs_dir), "run", "test-spec-001"],
)
mock_alarm.assert_called_with(0)