use adk_code::{
BackendCapabilities, CodeExecutor, EnvironmentPolicy, ExecutionError, ExecutionLanguage,
ExecutionPayload, ExecutionRequest, ExecutionStatus, FilesystemPolicy, NetworkPolicy,
RustSandboxExecutor, SandboxPolicy, validate_policy,
};
use proptest::prelude::*;
use std::path::PathBuf;
use std::time::Duration;
fn rust_sandbox_caps() -> BackendCapabilities {
RustSandboxExecutor::default().capabilities()
}
fn rust_request(code: &str, sandbox: SandboxPolicy) -> ExecutionRequest {
ExecutionRequest {
language: ExecutionLanguage::Rust,
payload: ExecutionPayload::Source { code: code.to_string() },
argv: vec![],
stdin: None,
input: Some(serde_json::json!({})),
sandbox,
identity: None,
}
}
fn permissive_policy() -> SandboxPolicy {
SandboxPolicy {
network: NetworkPolicy::Enabled,
filesystem: FilesystemPolicy::None,
environment: EnvironmentPolicy::None,
timeout: Duration::from_secs(30),
max_stdout_bytes: 1_048_576,
max_stderr_bytes: 1_048_576,
working_directory: None,
}
}
fn arb_network_policy() -> impl Strategy<Value = NetworkPolicy> {
prop_oneof![Just(NetworkPolicy::Disabled), Just(NetworkPolicy::Enabled)]
}
fn arb_filesystem_policy() -> impl Strategy<Value = FilesystemPolicy> {
prop_oneof![
Just(FilesystemPolicy::None),
"[a-z/]{1,20}".prop_map(|p| FilesystemPolicy::WorkspaceReadOnly { root: PathBuf::from(p) }),
"[a-z/]{1,20}"
.prop_map(|p| FilesystemPolicy::WorkspaceReadWrite { root: PathBuf::from(p) }),
(
proptest::collection::vec("[a-z/]{1,10}".prop_map(PathBuf::from), 0..3),
proptest::collection::vec("[a-z/]{1,10}".prop_map(PathBuf::from), 0..3),
)
.prop_map(|(ro, rw)| FilesystemPolicy::Paths { read_only: ro, read_write: rw }),
]
}
fn arb_environment_policy() -> impl Strategy<Value = EnvironmentPolicy> {
prop_oneof![
Just(EnvironmentPolicy::None),
proptest::collection::vec("[A-Z_]{1,10}", 1..5).prop_map(EnvironmentPolicy::AllowList),
]
}
fn arb_sandbox_policy() -> impl Strategy<Value = SandboxPolicy> {
(arb_network_policy(), arb_filesystem_policy(), arb_environment_policy()).prop_map(
|(network, filesystem, environment)| SandboxPolicy {
network,
filesystem,
environment,
timeout: Duration::from_secs(30),
max_stdout_bytes: 1_048_576,
max_stderr_bytes: 1_048_576,
working_directory: None,
},
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_unsupported_sandbox_controls_fail_closed(policy in arb_sandbox_policy()) {
let caps = rust_sandbox_caps();
let requests_network_restriction = matches!(policy.network, NetworkPolicy::Disabled);
let requests_filesystem_access = !matches!(policy.filesystem, FilesystemPolicy::None);
let requests_env_access = !matches!(policy.environment, EnvironmentPolicy::None);
let should_fail =
requests_network_restriction || requests_filesystem_access || requests_env_access;
let result = validate_policy(&caps, &policy);
if should_fail {
let err = result.expect_err("must reject policies with unenforced controls");
prop_assert!(
matches!(err, ExecutionError::UnsupportedPolicy(_)),
"expected UnsupportedPolicy, got: {err:?}"
);
} else {
prop_assert!(result.is_ok(), "should accept policies without unenforced controls");
}
}
}
#[tokio::test]
async fn test_timeout_produces_timeout_failure_short() {
let executor = RustSandboxExecutor::default();
let code = r#"
fn run(input: serde_json::Value) -> serde_json::Value {
std::thread::sleep(std::time::Duration::from_secs(10));
input
}
"#;
let mut policy = permissive_policy();
policy.timeout = Duration::from_millis(100);
let request = rust_request(code, policy);
let result = executor.execute(request).await.expect("should return result, not infra error");
assert_eq!(
result.status,
ExecutionStatus::Timeout,
"timed-out execution must report Timeout, got: {:?}",
result.status
);
assert_ne!(result.status, ExecutionStatus::Success);
}
#[tokio::test]
async fn test_timeout_during_execution_phase() {
let executor = RustSandboxExecutor::default();
let code = r#"
fn run(input: serde_json::Value) -> serde_json::Value {
loop { std::hint::spin_loop(); }
}
"#;
let mut policy = permissive_policy();
policy.timeout = Duration::from_secs(3);
let request = rust_request(code, policy);
let result = executor.execute(request).await.expect("should return result, not infra error");
assert_eq!(
result.status,
ExecutionStatus::Timeout,
"infinite loop must produce Timeout, got: {:?}",
result.status
);
}
#[tokio::test]
async fn prop_rust_compilation_failure_is_explicit() {
let executor = RustSandboxExecutor::default();
let invalid_snippets = [
"fn run(input: serde_json::Value) -> serde_json::Value { let x = 42 input }",
"fn run(input: serde_json::Value) -> serde_json::Value { undefined_var }",
r#"fn run(input: serde_json::Value) -> serde_json::Value { "not a value" }"#,
"fn run(input: serde_json::Value) -> serde_json::Value { nonexistent_fn() }",
"fn run() -> serde_json::Value { serde_json::json!(null) }",
"fn run(input: serde_json::Value) -> serde_json::Value { let _: NoSuchType = input; input }",
"fn helper() -> i32 { 42 }",
"fn run(input: serde_json::Value) -> serde_json::Value { input }\nfn run(input: serde_json::Value) -> serde_json::Value { input }",
"use nonexistent_crate::Foo;\nfn run(input: serde_json::Value) -> serde_json::Value { input }",
"fn run(input: serde_json::Value) -> serde_json::Value { 42_i64 }",
];
for code in &invalid_snippets {
let request = rust_request(code, permissive_policy());
let result = executor.execute(request).await.unwrap_or_else(|e| {
panic!("invalid code should produce a result, not infra error: {e}")
});
assert_eq!(
result.status,
ExecutionStatus::CompileFailed,
"invalid Rust must produce CompileFailed, got {:?} for: {}",
result.status,
code
);
assert!(
!result.stderr.is_empty(),
"compile failure should include diagnostics in stderr for: {code}"
);
assert_ne!(result.status, ExecutionStatus::Success);
assert_ne!(result.status, ExecutionStatus::Failed);
}
}
#[tokio::test]
async fn test_compile_failure_syntax_error() {
let executor = RustSandboxExecutor::default();
let code = "fn run(input: serde_json::Value) -> serde_json::Value { let x = }";
let request = rust_request(code, permissive_policy());
let result = executor.execute(request).await.unwrap();
assert_eq!(result.status, ExecutionStatus::CompileFailed);
assert!(!result.stderr.is_empty());
}
#[tokio::test]
async fn test_compile_failure_undefined_type() {
let executor = RustSandboxExecutor::default();
let code =
"fn run(input: serde_json::Value) -> serde_json::Value { let x: NoSuchType = input; x }";
let request = rust_request(code, permissive_policy());
let result = executor.execute(request).await.unwrap();
assert_eq!(result.status, ExecutionStatus::CompileFailed);
assert!(!result.stderr.is_empty());
}
#[tokio::test]
async fn test_compile_failure_missing_run_function() {
let executor = RustSandboxExecutor::default();
let code = "fn helper() -> i32 { 42 }";
let request = rust_request(code, permissive_policy());
let result = executor.execute(request).await.unwrap();
assert_eq!(result.status, ExecutionStatus::CompileFailed);
assert!(!result.stderr.is_empty());
}