4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
182 lines
6.1 KiB
Python
182 lines
6.1 KiB
Python
"""Tests for the Python sandbox runner."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
from config.constants import OPENSRE_TMP_DIR, ensure_opensre_tmp_dir
|
|
from platform.sandbox.runner import (
|
|
MAX_TIMEOUT,
|
|
SandboxResult,
|
|
run_python_sandbox,
|
|
)
|
|
|
|
|
|
class TestSandboxRunnerBasicExecution:
|
|
def test_runs_simple_code(self) -> None:
|
|
result = run_python_sandbox("print('hello')")
|
|
assert result.success
|
|
assert result.stdout.strip() == "hello"
|
|
assert result.stderr == ""
|
|
assert result.exit_code == 0
|
|
assert not result.timed_out
|
|
|
|
def test_captures_stderr(self) -> None:
|
|
result = run_python_sandbox("import sys; sys.stderr.write('err\\n')")
|
|
assert result.success
|
|
assert "err" in result.stderr
|
|
|
|
def test_captures_exit_code_on_failure(self) -> None:
|
|
result = run_python_sandbox("raise ValueError('boom')")
|
|
assert not result.success
|
|
assert result.exit_code != 0
|
|
assert "ValueError" in result.stderr
|
|
|
|
def test_empty_code_succeeds(self) -> None:
|
|
result = run_python_sandbox("")
|
|
assert result.success
|
|
assert result.stdout == ""
|
|
|
|
def test_result_stores_original_code(self) -> None:
|
|
code = "x = 1 + 1\nprint(x)"
|
|
result = run_python_sandbox(code)
|
|
assert result.code == code
|
|
|
|
def test_result_stores_inputs(self) -> None:
|
|
inputs = {"threshold": 42}
|
|
result = run_python_sandbox("print(inputs['threshold'])", inputs=inputs)
|
|
assert result.success
|
|
assert result.inputs == inputs
|
|
assert "42" in result.stdout
|
|
|
|
def test_no_inputs_stores_empty_dict(self) -> None:
|
|
result = run_python_sandbox("pass")
|
|
assert result.inputs == {}
|
|
|
|
|
|
class TestSandboxRunnerInputInjection:
|
|
def test_inputs_injected_as_variable(self) -> None:
|
|
code = "print(inputs['key'])"
|
|
result = run_python_sandbox(code, inputs={"key": "value123"})
|
|
assert result.success
|
|
assert "value123" in result.stdout
|
|
|
|
def test_inputs_supports_nested_structures(self) -> None:
|
|
code = "print(inputs['data'][0])"
|
|
result = run_python_sandbox(code, inputs={"data": [99, 100]})
|
|
assert result.success
|
|
assert "99" in result.stdout
|
|
|
|
def test_none_inputs_not_injected(self) -> None:
|
|
result = run_python_sandbox("x = 1", inputs=None)
|
|
assert result.success
|
|
assert result.inputs == {}
|
|
|
|
|
|
class TestSandboxNetworkRestrictions:
|
|
def test_socket_creation_blocked(self) -> None:
|
|
code = "import socket; socket.socket()"
|
|
result = run_python_sandbox(code)
|
|
assert not result.success
|
|
assert "PermissionError" in result.stderr or "PermissionError" in result.stdout
|
|
|
|
def test_create_connection_blocked(self) -> None:
|
|
code = "import socket; socket.create_connection(('localhost', 80))"
|
|
result = run_python_sandbox(code)
|
|
assert not result.success
|
|
assert "PermissionError" in result.stderr or "PermissionError" in result.stdout
|
|
|
|
def test_getaddrinfo_blocked(self) -> None:
|
|
code = "import socket; socket.getaddrinfo('localhost', 80)"
|
|
result = run_python_sandbox(code)
|
|
assert not result.success
|
|
assert "PermissionError" in result.stderr or "PermissionError" in result.stdout
|
|
|
|
|
|
class TestSandboxFilesystemRestrictions:
|
|
def test_read_allowed(self) -> None:
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
|
|
f.write("data")
|
|
path = f.name
|
|
try:
|
|
result = run_python_sandbox(f"open({path!r}).read()")
|
|
assert result.success
|
|
finally:
|
|
os.unlink(path)
|
|
|
|
def test_write_outside_tmp_blocked(self) -> None:
|
|
code = "open('/etc/sandbox_test_file', 'w').write('x')"
|
|
result = run_python_sandbox(code)
|
|
assert not result.success
|
|
assert "PermissionError" in result.stderr or "PermissionError" in result.stdout
|
|
|
|
def test_write_inside_opensre_tmp_allowed(self) -> None:
|
|
ensure_opensre_tmp_dir()
|
|
target = os.path.join(os.fspath(OPENSRE_TMP_DIR), "sandbox_write_test.txt")
|
|
code = f"open({target!r}, 'w').write('ok')"
|
|
result = run_python_sandbox(code)
|
|
assert result.success
|
|
if os.path.exists(target):
|
|
os.unlink(target)
|
|
|
|
def test_append_outside_tmp_blocked(self) -> None:
|
|
code = "open('/etc/sandbox_append_test', 'a').write('x')"
|
|
result = run_python_sandbox(code)
|
|
assert not result.success
|
|
assert "PermissionError" in result.stderr or "PermissionError" in result.stdout
|
|
|
|
|
|
class TestSandboxTimeout:
|
|
def test_timeout_enforced(self) -> None:
|
|
result = run_python_sandbox("import time; time.sleep(10)", timeout=1)
|
|
assert not result.success
|
|
assert result.timed_out
|
|
assert result.exit_code == -1
|
|
assert result.error is not None
|
|
assert "timed out" in result.error.lower()
|
|
|
|
def test_timeout_capped_at_max(self) -> None:
|
|
result = run_python_sandbox("pass", timeout=MAX_TIMEOUT + 9999)
|
|
assert result.success
|
|
|
|
def test_fast_code_does_not_time_out(self) -> None:
|
|
result = run_python_sandbox("print('fast')", timeout=30)
|
|
assert result.success
|
|
assert not result.timed_out
|
|
|
|
|
|
class TestSandboxResultModel:
|
|
def test_success_property_true_on_zero_exit(self) -> None:
|
|
r = SandboxResult(
|
|
code="",
|
|
inputs={},
|
|
stdout="",
|
|
stderr="",
|
|
exit_code=0,
|
|
timed_out=False,
|
|
)
|
|
assert r.success is True
|
|
|
|
def test_success_property_false_on_nonzero_exit(self) -> None:
|
|
r = SandboxResult(
|
|
code="",
|
|
inputs={},
|
|
stdout="",
|
|
stderr="",
|
|
exit_code=1,
|
|
timed_out=False,
|
|
)
|
|
assert r.success is False
|
|
|
|
def test_success_property_false_when_timed_out(self) -> None:
|
|
r = SandboxResult(
|
|
code="",
|
|
inputs={},
|
|
stdout="",
|
|
stderr="",
|
|
exit_code=0,
|
|
timed_out=True,
|
|
)
|
|
assert r.success is False
|