mcp-shell-server
by tumf
- mcp-shell-server
- tests
import os
import tempfile
from typing import IO
from unittest.mock import AsyncMock
import pytest
def clear_env(monkeypatch):
monkeypatch.delenv("ALLOW_COMMANDS", raising=False)
monkeypatch.delenv("ALLOWED_COMMANDS", raising=False)
@pytest.fixture
def temp_test_dir():
"""Create a temporary directory for testing"""
with tempfile.TemporaryDirectory() as tmpdirname:
# Return the real path to handle macOS /private/tmp symlink
yield os.path.realpath(tmpdirname)
@pytest.mark.asyncio
async def test_basic_command_execution(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo")
# Set up mock return values
mock_process = AsyncMock()
mock_process.returncode = 0
mock_process.communicate = AsyncMock(return_value=(b"hello\n", b""))
mock_process.kill = AsyncMock()
mock_process.wait = AsyncMock()
mock_process_manager.create_process.return_value = mock_process
mock_process_manager.execute_with_timeout.return_value = (b"hello\n", b"")
result = await shell_executor_with_mock.execute(["echo", "hello"], temp_test_dir)
assert result["stdout"].strip() == "hello"
assert result["status"] == 0
@pytest.mark.asyncio
async def test_stdin_input(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "cat")
# Set up mock return values
mock_process = AsyncMock()
mock_process.returncode = 0
mock_process.communicate = AsyncMock(return_value=(b"hello world\n", b""))
mock_process.kill = AsyncMock()
mock_process.wait = AsyncMock()
mock_process_manager.create_process.return_value = mock_process
mock_process_manager.execute_with_timeout.return_value = (b"hello world\n", b"")
result = await shell_executor_with_mock.execute(
["cat "], temp_test_dir, stdin="hello world"
)
assert result["stdout"].strip() == "hello world"
assert result["status"] == 0
assert result["error"] is None
@pytest.mark.asyncio
async def test_command_not_allowed(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "ls")
mock_process_manager.execute_with_timeout.side_effect = ValueError(
"Command not allowed: rm"
)
result = await shell_executor_with_mock.execute(["rm", "-rf", "/"], temp_test_dir)
assert result["error"] == "Command not allowed: rm"
assert result["status"] == 1
@pytest.mark.asyncio
async def test_empty_command(
shell_executor_with_mock, mock_process_manager, temp_test_dir
):
mock_process_manager.execute_with_timeout.side_effect = ValueError("Empty command")
result = await shell_executor_with_mock.execute([], temp_test_dir)
assert result["error"] == "Empty command"
assert result["status"] == 1
@pytest.mark.asyncio
async def test_command_with_space_in_allow_commands(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "ls, echo ,cat")
# Set up mock return values
mock_process = AsyncMock()
mock_process.returncode = 0
mock_process.communicate = AsyncMock(return_value=(b"test\n", b""))
mock_process.kill = AsyncMock()
mock_process.wait = AsyncMock()
mock_process_manager.create_process.return_value = mock_process
mock_process_manager.execute_with_timeout.return_value = (b"test\n", b"")
result = await shell_executor_with_mock.execute(["echo", "test"], temp_test_dir)
assert result["stdout"].strip() == "test"
assert result["status"] == 0
assert result["error"] is None
@pytest.mark.asyncio
async def test_multiple_commands_with_operator(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo,ls")
mock_process_manager.execute_with_timeout.side_effect = ValueError(
"Unexpected shell operator: ;"
)
result = await shell_executor_with_mock.execute(
["echo", "hello", ";"], temp_test_dir
)
assert result["error"] == "Unexpected shell operator: ;"
assert result["status"] == 1
@pytest.mark.asyncio
async def test_shell_operators_not_allowed(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo,ls,true")
operators = [";", "&&", "||"]
for op in operators:
mock_process_manager.execute_with_timeout.side_effect = ValueError(
f"Unexpected shell operator: {op}"
)
result = await shell_executor_with_mock.execute(
["echo", "hello", op, "true"], temp_test_dir
)
assert result["error"] == f"Unexpected shell operator: {op}"
assert result["status"] == 1
# New tests for directory functionality
@pytest.mark.asyncio
async def test_execute_in_directory(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test command execution in a specific directory"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "pwd")
mock_process_manager.execute_with_timeout.return_value = (
temp_test_dir.encode() + b"\n",
b"",
)
result = await shell_executor_with_mock.execute(["pwd"], directory=temp_test_dir)
assert result["error"] is None
assert result["status"] == 0
assert result["stdout"].strip() == temp_test_dir
@pytest.mark.asyncio
async def test_execute_with_file_in_directory(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test command execution with a file in the specified directory"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "ls,cat")
# Create a test file in the temporary directory
test_file = os.path.join(temp_test_dir, "test.txt")
with open(test_file, "w") as f:
f.write("test content")
# Test ls command
mock_process_manager.execute_with_timeout.return_value = (b"test.txt\n", b"")
result = await shell_executor_with_mock.execute(["ls"], directory=temp_test_dir)
assert "test.txt" in result["stdout"]
# Test cat command - Set specific mock output for cat command
mock_process_manager.execute_with_timeout.return_value = (b"test content\n", b"")
result = await shell_executor_with_mock.execute(
["cat", "test.txt"], directory=temp_test_dir
)
assert result["stdout"].strip() == "test content"
assert result["error"] is None
assert result["status"] == 0
@pytest.mark.asyncio
async def test_execute_with_nonexistent_directory(
shell_executor_with_mock, mock_process_manager, monkeypatch
):
"""Test command execution with a non-existent directory"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "ls")
mock_process_manager.execute_with_timeout.side_effect = ValueError(
"Directory does not exist: /nonexistent/directory"
)
result = await shell_executor_with_mock.execute(
["ls"], directory="/nonexistent/directory"
)
assert result["error"] == "Directory does not exist: /nonexistent/directory"
assert result["status"] == 1
@pytest.mark.asyncio
async def test_execute_with_file_as_directory(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test command execution with a file specified as directory"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "ls")
# Create a test file
test_file = os.path.join(temp_test_dir, "test.txt")
with open(test_file, "w") as f:
f.write("test content")
mock_process_manager.execute_with_timeout.side_effect = ValueError(
f"Not a directory: {test_file}"
)
result = await shell_executor_with_mock.execute(["ls"], directory=test_file)
assert result["error"] == f"Not a directory: {test_file}"
assert result["status"] == 1
@pytest.mark.asyncio
async def test_execute_with_nested_directory(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test command execution in a nested directory"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "pwd,mkdir,ls")
# Create a nested directory
nested_dir = os.path.join(temp_test_dir, "nested")
os.mkdir(nested_dir)
nested_real_path = os.path.realpath(nested_dir)
mock_process_manager.execute_with_timeout.return_value = (
nested_real_path.encode() + b"\n",
b"",
)
result = await shell_executor_with_mock.execute(["pwd"], directory=nested_dir)
assert result["error"] is None
assert result["status"] == 0
assert result["stdout"].strip() == nested_real_path
@pytest.mark.asyncio
async def test_command_timeout(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test command timeout functionality"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "sleep")
mock_process_manager.execute_with_timeout.side_effect = TimeoutError(
"Command timed out after 1 seconds"
)
result = await shell_executor_with_mock.execute(
["sleep", "2"], temp_test_dir, timeout=1
)
assert result["error"] == "Command timed out after 1 seconds"
assert result["status"] == -1
assert result["stdout"] == ""
assert result["stderr"] == "Command timed out after 1 seconds"
@pytest.mark.asyncio
async def test_command_completes_within_timeout(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test command that completes within timeout period"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "sleep")
result = await shell_executor_with_mock.execute(
["sleep", "1"], temp_test_dir, timeout=2
)
assert result["error"] is None
assert result["status"] == 0
assert result["stdout"] == ""
@pytest.mark.asyncio
async def test_allowed_commands_alias(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test ALLOWED_COMMANDS alias functionality"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo")
mock_process_manager.execute_with_timeout.return_value = (b"hello\n", b"")
result = await shell_executor_with_mock.execute(["echo", "hello"], temp_test_dir)
assert result["stdout"].strip() == "hello"
assert result["status"] == 0
assert result["error"] is None
@pytest.mark.asyncio
async def test_both_allow_commands_vars(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test both ALLOW_COMMANDS and ALLOWED_COMMANDS working together"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo")
monkeypatch.setenv("ALLOWED_COMMANDS", "cat")
# Test command from ALLOW_COMMANDS
mock_process_manager.execute_with_timeout.return_value = (b"hello\n", b"")
result1 = await shell_executor_with_mock.execute(["echo", "hello"], temp_test_dir)
assert result1["stdout"].strip() == "hello"
assert result1["status"] == 0
assert result1["error"] is None
# Test command from ALLOWED_COMMANDS
mock_process_manager.execute_with_timeout.return_value = (b"world\n", b"")
result2 = await shell_executor_with_mock.execute(
["cat"], temp_test_dir, stdin="world"
)
assert result2["stdout"].strip() == "world"
assert result2["status"] == 0
assert result2["error"] is None
@pytest.mark.asyncio
async def test_allow_commands_precedence(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test that commands are combined from both environment variables"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo,ls")
monkeypatch.setenv("ALLOWED_COMMANDS", "echo,cat")
assert set(shell_executor_with_mock.validator.get_allowed_commands()) == {
"echo",
"ls",
"cat",
}
@pytest.mark.asyncio
async def test_pipe_operator(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test that pipe operator works correctly"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo,grep")
mock_process_manager.execute_pipeline.return_value = (b"world\n", b"", 0)
result = await shell_executor_with_mock.execute(
["echo", "hello\nworld", "|", "grep", "world"], temp_test_dir
)
assert result["error"] is None
assert result["status"] == 0
assert result["stdout"].strip() == "world"
@pytest.mark.asyncio
async def test_pipe_commands(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test piping commands together"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo,grep,cut,tr")
# Test multiple pipes
mock_process_manager.execute_pipeline.return_value = (b"WORLD\n", b"", 0)
result = await shell_executor_with_mock.execute(
["echo", "hello world", "|", "cut", "-d", " ", "-f2", "|", "tr", "a-z", "A-Z"],
temp_test_dir,
)
assert result["stdout"].strip() == "WORLD"
@pytest.mark.asyncio
async def test_output_redirection(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test output redirection with > operator"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo,cat")
output_file = os.path.join(temp_test_dir, "out.txt")
# Test > redirection
# Mock empty output for echo commands
mock_process_manager.execute_with_timeout.return_value = (b"", b"")
result = await shell_executor_with_mock.execute(
["echo", "hello", ">", output_file], directory=temp_test_dir
)
assert result["error"] is None
assert result["status"] == 0
# Test >> redirection (append)
mock_process_manager.execute_with_timeout.return_value = (b"", b"")
result = await shell_executor_with_mock.execute(
["echo", "world", ">>", output_file], directory=temp_test_dir
)
assert result["error"] is None
assert result["status"] == 0
# Mock cat command to return the expected file contents
mock_process_manager.execute_with_timeout.return_value = (b"hello\nworld\n", b"")
result = await shell_executor_with_mock.execute(
["cat", output_file], directory=temp_test_dir
)
assert result["status"] == 0
assert result["error"] is None
assert result["stdout"].strip().split("\n") == ["hello", "world"]
@pytest.mark.asyncio
async def test_input_redirection(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
mocker,
):
"""Test input redirection with < operator"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "cat")
input_file = os.path.join(temp_test_dir, "in.txt")
# Mock the file operations
mock_file = mocker.mock_open(read_data="test content")
mocker.patch("builtins.open", mock_file)
# Test < redirection
mock_process_manager.execute_with_timeout.return_value = (b"test content\n", b"")
result = await shell_executor_with_mock.execute(
["cat", "<", input_file], directory=temp_test_dir
)
assert result["error"] is None
assert result["status"] == 0
assert result["stdout"].strip() == "test content"
@pytest.mark.asyncio
async def test_combined_redirections(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test combining input and output redirection"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "cat,tr")
input_file = os.path.join(temp_test_dir, "in.txt")
output_file = os.path.join(temp_test_dir, "out.txt")
# Create input file
with open(input_file, "w") as f:
f.write("hello world")
# Test < and > redirection together
result = await shell_executor_with_mock.execute(
["cat", "<", input_file, "|", "tr", "[:lower:]", "[:upper:]", ">", output_file],
directory=temp_test_dir,
)
assert result["error"] is None
assert result["status"] == 0
# Verify using cat command
mock_process_manager.execute_with_timeout.return_value = (b"HELLO WORLD\n", b"")
result = await shell_executor_with_mock.execute(
["cat", output_file], directory=temp_test_dir
)
assert result["stdout"].strip() == "HELLO WORLD"
@pytest.mark.asyncio
async def test_redirection_error_cases(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test error cases for redirections"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo,cat")
# Missing output file path
result = await shell_executor_with_mock.execute(
["echo", "hello", ">"], directory=temp_test_dir
)
assert result["error"] == "Missing path for output redirection"
# Missing input file path
result = await shell_executor_with_mock.execute(
["cat", "<"], directory=temp_test_dir
)
assert result["error"] == "Missing path for input redirection"
# Non-existent input file
result = await shell_executor_with_mock.execute(
["cat", "<", "nonexistent.txt"], directory=temp_test_dir
)
assert result["error"] == "Failed to open input file"
# Operator as path
result = await shell_executor_with_mock.execute(
["echo", "hello", ">", ">"], directory=temp_test_dir
)
assert result["error"] == "Invalid redirection target: operator found"
@pytest.mark.asyncio
async def test_complex_pipeline_with_redirections(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test complex pipeline with multiple redirections"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo,grep,tr,cat")
input_file = os.path.join(temp_test_dir, "pipeline_input.txt")
output_file = os.path.join(temp_test_dir, "pipeline_output.txt")
# Create a test input file
with open(input_file, "w") as f:
f.write("hello\nworld\ntest\nHELLO\n")
# Mock process execution for pipeline
final_output = "HELLO\nWORLD"
mock_process_manager.execute_pipeline.return_value = (final_output.encode(), b"", 0)
# Complex pipeline: cat < input | grep l | tr a-z A-Z > output
# Set specific process manager behavior for redirection
mock_process_manager.execute_with_timeout.return_value = (b"", b"")
mock_process_manager.execute_pipeline.side_effect = None
mock_process_manager.execute_pipeline.return_value = (b"", b"", 0)
result = await shell_executor_with_mock.execute(
[
"cat",
"<",
input_file,
"|",
"grep",
"l",
"|",
"tr",
"a-z",
"A-Z",
">",
output_file,
],
directory=temp_test_dir,
)
assert result["error"] is None
assert result["status"] == 0
assert result["stdout"] == ""
# Write expected output to simulated file
with open(output_file, "w") as f:
f.write(final_output)
# Check the output file content
with open(output_file, "r") as f:
actual_output = f.read().strip()
assert actual_output == final_output
def test_validate_redirection_syntax(shell_executor_with_mock):
"""Test validation of redirection syntax with various input combinations"""
# Valid cases
shell_executor_with_mock.io_handler.validate_redirection_syntax(
["echo", "hello", ">", "file.txt"]
)
shell_executor_with_mock.io_handler.validate_redirection_syntax(
["cat", "<", "input.txt", ">", "output.txt"]
)
# Test consecutive operators
with pytest.raises(ValueError) as exc:
shell_executor_with_mock.io_handler.validate_redirection_syntax(
["echo", "text", ">", ">", "file.txt"]
)
assert str(exc.value) == "Invalid redirection syntax: consecutive operators"
with pytest.raises(ValueError) as exc:
shell_executor_with_mock.io_handler.validate_redirection_syntax(
["cat", "<", "<", "input.txt"]
)
assert str(exc.value) == "Invalid redirection syntax: consecutive operators"
def test_create_shell_command(shell_executor_with_mock):
"""Test shell command creation with various input combinations"""
# Test basic command
assert (
shell_executor_with_mock.preprocessor.create_shell_command(["echo", "hello"])
== "echo hello"
)
# Test command with space-only argument
assert (
shell_executor_with_mock.preprocessor.create_shell_command(["echo", " "])
== "echo ' '"
)
# Test command with wildcards
assert (
shell_executor_with_mock.preprocessor.create_shell_command(["ls", "*.txt"])
== "ls '*.txt'"
)
# Test command with special characters
assert (
shell_executor_with_mock.preprocessor.create_shell_command(
["echo", "hello;", "world"]
)
== "echo 'hello;' world"
)
# Test empty command
assert shell_executor_with_mock.preprocessor.create_shell_command([]) == ""
def test_preprocess_command(shell_executor_with_mock):
"""Test command preprocessing for pipeline handling"""
# Test basic command
assert shell_executor_with_mock.preprocessor.preprocess_command(["ls"]) == ["ls"]
# Test command with separate pipe
assert shell_executor_with_mock.preprocessor.preprocess_command(
["ls", "|", "grep", "test"]
) == [
"ls",
"|",
"grep",
"test",
]
# Test command with attached pipe
assert shell_executor_with_mock.preprocessor.preprocess_command(
["ls|", "grep", "test"]
) == [
"ls",
"|",
"grep",
"test",
]
# Test command with special operators
assert shell_executor_with_mock.preprocessor.preprocess_command(
["echo", "hello", "&&", "ls"]
) == [
"echo",
"hello",
"&&",
"ls",
]
# Test empty command
assert shell_executor_with_mock.preprocessor.preprocess_command([]) == []
def test_validate_pipeline(shell_executor_with_mock, monkeypatch):
"""Test pipeline validation"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo,grep,cat")
monkeypatch.setenv("ALLOWED_COMMANDS", "echo,grep,cat")
# Test valid pipeline
shell_executor_with_mock.validator.validate_pipeline(
["echo", "hello", "|", "grep", "h"]
)
# Test empty command before pipe
with pytest.raises(ValueError) as exc:
shell_executor_with_mock.validator.validate_pipeline(["|", "grep", "test"])
assert str(exc.value) == "Empty command before pipe operator"
# Test disallowed commands in pipeline
with pytest.raises(ValueError) as exc:
shell_executor_with_mock.validator.validate_pipeline(
["rm", "|", "grep", "test"]
)
assert "Command not allowed: rm" in str(exc.value)
# Test shell operators in pipeline
with pytest.raises(ValueError) as exc:
shell_executor_with_mock.validator.validate_pipeline(
["echo", "hello", "|", "grep", "h", "&&", "ls"]
)
assert "Unexpected shell operator in pipeline: &&" in str(exc.value)
assert shell_executor_with_mock.preprocessor.preprocess_command([]) == []
def test_redirection_path_validation(shell_executor_with_mock):
"""Test validation of redirection paths"""
# Test missing output redirection path
with pytest.raises(ValueError, match="Missing path for output redirection"):
shell_executor_with_mock.preprocessor.parse_command(["echo", "hello", ">"])
# Test missing input redirection path
with pytest.raises(ValueError, match="Missing path for input redirection"):
shell_executor_with_mock.preprocessor.parse_command(["cat", "<"])
# Test operator as redirection target
with pytest.raises(ValueError, match="Invalid redirection target: operator found"):
shell_executor_with_mock.preprocessor.parse_command(["echo", "hello", ">", ">"])
# Test multiple operators
with pytest.raises(ValueError, match="Invalid redirection target: operator found"):
shell_executor_with_mock.preprocessor.parse_command(
["echo", "hello", ">", ">>", "file.txt"]
)
@pytest.mark.asyncio
async def test_io_handle_close(
shell_executor_with_mock,
mock_process_manager,
mock_file,
temp_test_dir,
monkeypatch,
mocker,
):
"""Test IO handle closing functionality"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo")
test_file = os.path.join(temp_test_dir, "test.txt")
# Create file handler that will raise IOError on close
mock_file = mocker.MagicMock(spec=IO)
mock_file.close.side_effect = IOError("Failed to close file")
# Patch the open function to return our mock
mocker.patch("builtins.open", return_value=mock_file)
# Mock logging.warning to capture the warning
mock_warning = mocker.patch("logging.warning")
# Execute should not raise an error
await shell_executor_with_mock.execute(
["echo", "hello", ">", test_file], directory=temp_test_dir
)
# Verify our mock's close method was called
assert mock_file.close.called
# Verify warning was logged
mock_warning.assert_called_once_with("Error closing stdout: Failed to close file")
def test_preprocess_command_pipeline(shell_executor_with_mock):
"""Test pipeline command preprocessing functionality"""
# Test empty command
assert shell_executor_with_mock.preprocessor.preprocess_command([]) == []
# Test single command without pipe
assert shell_executor_with_mock.preprocessor.preprocess_command(
["echo", "hello"]
) == [
"echo",
"hello",
]
# Test simple pipe
assert shell_executor_with_mock.preprocessor.preprocess_command(
["echo", "hello", "|", "grep", "h"]
) == [
"echo",
"hello",
"|",
"grep",
"h",
]
# Test multiple pipes
assert shell_executor_with_mock.preprocessor.preprocess_command(
["cat", "file", "|", "grep", "pattern", "|", "wc", "-l"]
) == ["cat", "file", "|", "grep", "pattern", "|", "wc", "-l"]
# Test command with attached pipe operator
assert shell_executor_with_mock.preprocessor.preprocess_command(
["echo|", "grep", "pattern"]
) == [
"echo",
"|",
"grep",
"pattern",
]
@pytest.mark.asyncio
async def test_command_cleanup_on_error(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test cleanup of processes when error occurs"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "sleep")
# Configure mock to simulate timeout
mock_process_manager.execute_with_timeout.side_effect = TimeoutError(
"Command timed out"
)
async def execute_with_keyboard_interrupt():
# Simulate keyboard interrupt during execution
result = await shell_executor_with_mock.execute(
["sleep", "5"], temp_test_dir, timeout=1
)
return result
result = await execute_with_keyboard_interrupt()
assert result["error"] == "Command timed out after 1 seconds"
assert result["status"] == -1
assert "execution_time" in result
@pytest.mark.asyncio
async def test_output_redirection_with_append(
shell_executor_with_mock,
mock_process_manager,
mock_file,
temp_test_dir,
monkeypatch,
):
"""Test output redirection with append mode"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "echo,cat")
output_file = os.path.join(temp_test_dir, "test.txt")
# Write initial content
await shell_executor_with_mock.execute(
["echo", "hello", ">", output_file], directory=temp_test_dir
)
# Append content
result = await shell_executor_with_mock.execute(
["echo", "world", ">>", output_file], directory=temp_test_dir
)
assert result["error"] is None
assert result["status"] == 0
# Verify contents
mock_process_manager.execute_with_timeout.return_value = (b"hello\nworld\n", b"")
result = await shell_executor_with_mock.execute(
["cat", output_file], directory=temp_test_dir
)
lines = result["stdout"].strip().split("\n")
assert len(lines) == 2
assert lines[0] == "hello"
@pytest.mark.asyncio
async def test_execute_with_custom_env(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test command execution with custom environment variables"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "env,printenv")
custom_env = {"TEST_VAR1": "test_value1", "TEST_VAR2": "test_value2"}
# Test env command
mock_process_manager.execute_with_timeout.return_value = (
b"TEST_VAR1=test_value1\nTEST_VAR2=test_value2\n",
b"",
)
result = await shell_executor_with_mock.execute(
["env"], directory=temp_test_dir, envs=custom_env
)
assert "TEST_VAR1=test_value1" in result["stdout"]
assert "TEST_VAR2=test_value2" in result["stdout"]
# Test specific variable - Update mock for printenv command
mock_process_manager.execute_with_timeout.return_value = (b"test_value1\n", b"")
result = await shell_executor_with_mock.execute(
["printenv", "TEST_VAR1"], directory=temp_test_dir, envs=custom_env
)
assert result["stdout"].strip() == "test_value1"
@pytest.mark.asyncio
async def test_execute_env_override(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test that custom environment variables override system variables"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "env")
monkeypatch.setenv("TEST_VAR", "original_value")
# Mock env command with new environment variable
mock_process_manager.execute_with_timeout.return_value = (
b"TEST_VAR=new_value\n",
b"",
)
# Override system environment variable
result = await shell_executor_with_mock.execute(
["env"], directory=temp_test_dir, envs={"TEST_VAR": "new_value"}
)
assert "TEST_VAR=new_value" in result["stdout"]
assert "TEST_VAR=original_value" not in result["stdout"]
@pytest.mark.asyncio
async def test_execute_with_empty_env(
shell_executor_with_mock,
mock_process_manager,
temp_test_dir,
monkeypatch,
):
"""Test command execution with empty environment variables"""
clear_env(monkeypatch)
monkeypatch.setenv("ALLOW_COMMANDS", "env")
# Mock env command with system environment
mock_process_manager.execute_with_timeout.return_value = (
b"PATH=/usr/bin\nHOME=/home/user\n",
b"",
)
result = await shell_executor_with_mock.execute(
["env"], directory=temp_test_dir, envs={}
)
# Command should still work with system environment
assert result["error"] is None
assert result["status"] == 0
assert len(result["stdout"]) > 0