Files
secure-agent/tests/test_sandbox.py
T
2026-03-04 14:35:52 -07:00

191 lines
6.1 KiB
Python

import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# ─── Unit Tests ───────────────────────────────────────────────
@pytest.mark.unit
def test_sandbox_initializes():
"""Test PodmanSandbox creates client on init."""
with patch("sandbox.session.podman.PodmanClient") as mock_client:
from sandbox.session import PodmanSandbox
sb = PodmanSandbox()
# Client should be created
mock_client.assert_called_once()
assert sb.container is None # Not started yet
@pytest.mark.unit
async def test_sandbox_starts_container():
"""Test that __aenter__ starts a container."""
with patch("sandbox.session.podman.PodmanClient") as mock_client:
# Mock the container
mock_container = MagicMock()
mock_client.return_value.containers.run.return_value = mock_container
from sandbox.session import PodmanSandbox
sb = PodmanSandbox()
await sb.__aenter__()
# Container should be running
mock_client.return_value.containers.run.assert_called_once()
assert sb.container is mock_container
@pytest.mark.unit
async def test_sandbox_stops_container_on_exit():
"""Test that __aexit__ stops the container."""
with patch("sandbox.session.podman.PodmanClient") as mock_client:
mock_container = MagicMock()
mock_client.return_value.containers.run.return_value = mock_container
from sandbox.session import PodmanSandbox
sb = PodmanSandbox()
await sb.__aenter__()
await sb.__aexit__(None, None, None)
# Container should be stopped
mock_container.stop.assert_called_once()
@pytest.mark.unit
async def test_sandbox_run_executes_command():
"""Test that run() passes command to container."""
with patch("sandbox.session.podman.PodmanClient") as mock_client:
# Mock exec_run return value
mock_container = MagicMock()
mock_container.exec_run.return_value = (0, b"hello from sandbox\n")
mock_client.return_value.containers.run.return_value = mock_container
from sandbox.session import PodmanSandbox
sb = PodmanSandbox()
await sb.__aenter__()
result = sb.run("echo 'hello from sandbox'")
# Verify exec_run was called with shell wrapper
mock_container.exec_run.assert_called_once_with(
["/bin/sh", "-c", "echo 'hello from sandbox'"],
workdir="/workspace",
demux=False,
)
assert result == "hello from sandbox\n"
@pytest.mark.unit
async def test_tool_call_fails_if_sandbox_crashes():
"""Test that tool calls fail gracefully when sandbox is unavailable."""
from tools.bash import bash
# Simulate crashed sandbox (container is None)
mock_sandbox = MagicMock()
mock_sandbox.run = MagicMock(side_effect=RuntimeError("Container crashed"))
result = await bash("ls -la", mock_sandbox)
# Should return error message, not raise exception
assert "error" in result.lower()
@pytest.mark.unit
async def test_tool_call_with_no_sandbox():
"""Test tool call handles None sandbox gracefully."""
from tools.bash import bash
result = await bash("ls -la", sandbox=None)
# Should return error, not crash
assert "error" in result.lower()
@pytest.mark.unit
async def test_sandbox_cleanup_on_crash():
"""Test __aexit__ handles container stop failure gracefully."""
with patch("sandbox.session.podman.PodmanClient") as mock_client:
mock_container = MagicMock()
# Simulate container.stop() failing
mock_container.stop.side_effect = RuntimeError("Container already dead")
mock_client.return_value.containers.run.return_value = mock_container
from sandbox.session import PodmanSandbox
sb = PodmanSandbox()
await sb.__aenter__()
# Should not raise even if stop() fails
try:
await sb.__aexit__(None, None, None)
except RuntimeError:
pytest.fail("__aexit__ should handle container stop failure gracefully")
# ─── Integration Tests ────────────────────────────────────────
@pytest.mark.integration
async def test_real_sandbox_starts():
"""Integration: Actually start a real sandbox."""
from sandbox.session import PodmanSandbox
async with PodmanSandbox() as sb:
assert sb.container is not None
# Verify container is actually running
result = await asyncio.to_thread(sb.run, "echo 'sandbox started'")
assert "sandbox started" in result
@pytest.mark.integration
async def test_real_bash_tool_executes():
"""Integration: Actually run a command in sandbox."""
from sandbox.session import PodmanSandbox
from tools.bash import bash
async with PodmanSandbox() as sb:
result = await bash("echo 'tool works'", sb)
assert "tool works" in result
@pytest.mark.integration
async def test_real_sandbox_workspace_mounted():
"""Integration: Verify workspace is mounted correctly."""
import os
from agent.config import settings
from sandbox.session import PodmanSandbox
from tools.bash import bash
async with PodmanSandbox() as sb:
# Write file in sandbox
await bash("echo 'mount test' > /workspace/mount_test.txt", sb)
# Verify file exists on host
host_file = f"{settings.safedir}/mount_test.txt"
assert os.path.exists(host_file)
with open(host_file) as f:
content = f.read()
assert "mount test" in content
# Cleanup
os.remove(host_file)
@pytest.mark.integration
async def test_real_sandbox_no_network():
"""Integration: Verify sandbox has no network access."""
from sandbox.session import PodmanSandbox
from tools.bash import bash
async with PodmanSandbox() as sb:
# Try to reach the internet (should fail)
result = await bash("ping -c 1 -W 8.8.8.8 2>&1 || echo 'no network'", sb)
assert "no network" in result