use std::path::PathBuf;
use apcore::{Config, Executor, Registry};
use apcore_cli::security::sandbox::{ModuleExecutionError, Sandbox};
use serde_json::json;
fn make_test_executor() -> Executor {
Executor::new(Registry::new(), Config::default())
}
#[tokio::test]
async fn test_sandbox_disabled_passes_through_to_executor() {
let sandbox = Sandbox::new(false, 0);
assert!(!sandbox.is_enabled(), "sandbox must report disabled");
let executor = make_test_executor();
let result = sandbox
.execute("math.add", json!({"a": 1, "b": 2}), &executor)
.await;
match &result {
Err(ModuleExecutionError::SpawnFailed(msg)) if msg.contains("not wired") => {
panic!("disabled sandbox must passthrough to executor, not return 'not wired'");
}
Err(ModuleExecutionError::ModuleError(_)) => {
}
Err(other) => panic!(
"disabled sandbox must surface ModuleError variant for executor \
failures so exit-code mapping stays consistent with direct exec; \
got: {other:?}"
),
Ok(_) => {
}
}
}
#[tokio::test]
#[ignore = "requires the compiled apcore-cli binary to be present at current_exe(); \
run manually after `cargo build` with `cargo test -- --ignored`"]
async fn test_sandbox_enabled_spawns_subprocess() {
let sandbox = Sandbox::new(true, 5);
assert!(sandbox.is_enabled(), "sandbox must report enabled");
let executor = make_test_executor();
let result = sandbox
.execute("math.add", json!({"a": 1, "b": 2}), &executor)
.await;
match &result {
Err(ModuleExecutionError::SpawnFailed(msg)) if msg.contains("not found") => {
panic!(
"sandbox enabled path must not delegate to executor, \
must spawn subprocess instead. Got: {msg}"
);
}
_ => {} }
}
#[tokio::test]
async fn test_sandbox_enabled_timeout_returns_error() {
let sandbox = Sandbox::new(true, 1); let executor = make_test_executor();
let result = sandbox.execute("slow.module", json!({}), &executor).await;
assert!(
result.is_err(),
"sandbox with 1s timeout must return an error"
);
}
#[tokio::test]
async fn test_nonzero_exit_carries_stderr() {
let err = ModuleExecutionError::NonZeroExit {
module_id: "some.module".to_string(),
exit_code: 2,
stderr: "simulated panic: invalid state".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("simulated panic: invalid state"),
"NonZeroExit Display must include captured stderr, got: {msg}"
);
assert!(msg.contains("exited with code 2"));
}
#[tokio::test]
async fn test_sandbox_with_extensions_root_sets_field() {
let path = PathBuf::from("/tmp/apcore-ext-test");
let sandbox = Sandbox::new(false, 5).with_extensions_root(Some(path.clone()));
assert_eq!(
sandbox.extensions_root(),
Some(&path),
"with_extensions_root must store the supplied path for subprocess injection"
);
}
#[tokio::test]
async fn test_sandbox_with_max_output_bytes_sets_field() {
let sandbox = Sandbox::new(false, 5).with_max_output_bytes(2048);
assert_eq!(
sandbox.max_output_bytes(),
2048,
"with_max_output_bytes must override the 64 MiB default"
);
}
#[tokio::test]
async fn test_sandbox_builder_chains_with_constructor() {
let path = PathBuf::from("/tmp/ext-chain");
let sandbox = Sandbox::new(true, 30)
.with_extensions_root(Some(path.clone()))
.with_max_output_bytes(8192);
assert!(sandbox.is_enabled());
assert_eq!(sandbox.extensions_root(), Some(&path));
assert_eq!(sandbox.max_output_bytes(), 8192);
}
#[tokio::test]
async fn test_sandbox_with_max_output_bytes_enforces_smaller_cap() {
let sandbox = Sandbox::new(false, 1).with_max_output_bytes(1);
assert_eq!(sandbox.max_output_bytes(), 1);
let executor = Executor::new(Registry::new(), Config::default());
let _ = sandbox.execute("noop", json!({}), &executor).await;
}
#[tokio::test]
async fn test_output_size_exceeded_variant_display_and_fields() {
let err = ModuleExecutionError::OutputSizeExceeded {
module_id: "noisy.module".to_string(),
limit_bytes: 64 * 1024 * 1024,
overflow_stream: "stdout".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("MiB"),
"Display must report cap in MiB, got: {msg}"
);
assert!(
msg.contains("stdout"),
"Display must name the overflowing stream, got: {msg}"
);
assert!(
msg.contains("noisy.module"),
"Display must include module id, got: {msg}"
);
match err {
ModuleExecutionError::OutputSizeExceeded { .. } => {}
other => panic!("expected OutputSizeExceeded, got: {other:?}"),
}
}
#[tokio::test]
#[ignore = "requires the compiled apcore-cli binary to be present at current_exe(); \
run manually after `cargo build --bin apcore-cli` with `cargo test -- --ignored`"]
async fn test_sandbox_nonzero_exit_returns_error() {
let sandbox = Sandbox::new(true, 5); let executor = make_test_executor();
let result = sandbox
.execute("__nonexistent_module__", json!({}), &executor)
.await;
assert!(result.is_err(), "unknown module must result in an error");
}