use std::{
collections::HashMap,
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
#[cfg(feature = "vm")]
use base64::{Engine, engine::general_purpose::STANDARD};
use mimobox_sdk::{Config, DirEntry, ExecuteResult, FileType, IsolationLevel, Sandbox, SdkError};
use rmcp::handler::server::wrapper::Json;
use rmcp::schemars::JsonSchema;
use rmcp::{
ServerHandler,
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::{ServerCapabilities, ServerInfo},
tool, tool_handler, tool_router,
};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
use tokio::task::JoinError;
use tracing::error;
#[derive(Clone)]
pub struct MimoboxServer {
pub(crate) sandboxes: Arc<Mutex<HashMap<u64, ManagedSandbox>>>,
pub next_id: Arc<AtomicU64>,
pub tool_router: ToolRouter<Self>,
}
struct ManagedSandbox {
sandbox: Sandbox,
created_at_ms: u64,
created_at_instant: Instant,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CreateSandboxRequest {
isolation_level: Option<String>,
timeout_ms: Option<u64>,
memory_limit_mb: Option<u64>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct CreateSandboxResponse {
sandbox_id: u64,
isolation_level: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ExecuteCodeRequest {
sandbox_id: Option<u64>,
code: String,
language: Option<String>,
timeout_ms: Option<u64>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ExecuteCommandRequest {
sandbox_id: Option<u64>,
command: String,
timeout_ms: Option<u64>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct DestroySandboxRequest {
sandbox_id: u64,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListSandboxesRequest {}
#[derive(Debug, Deserialize, JsonSchema)]
#[cfg_attr(not(feature = "vm"), allow(dead_code))]
pub struct ReadFileRequest {
sandbox_id: u64,
path: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[cfg_attr(not(feature = "vm"), allow(dead_code))]
pub struct WriteFileRequest {
sandbox_id: u64,
path: String,
content: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[cfg_attr(not(feature = "vm"), allow(dead_code))]
pub struct SnapshotRequest {
sandbox_id: u64,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[cfg_attr(not(feature = "vm"), allow(dead_code))]
pub struct ForkRequest {
sandbox_id: u64,
}
#[derive(Debug, Deserialize, JsonSchema)]
#[cfg_attr(not(feature = "vm"), allow(dead_code))]
pub struct McpHttpRequest {
sandbox_id: u64,
url: String,
method: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ListDirRequest {
sandbox_id: u64,
path: String,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ExecuteResponse {
stdout: String,
stderr: String,
exit_code: Option<i32>,
timed_out: bool,
elapsed_ms: u128,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct DestroySandboxResponse {
sandbox_id: u64,
destroyed: bool,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ListSandboxesResponse {
sandboxes: Vec<SandboxSummary>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct SandboxSummary {
sandbox_id: u64,
isolation_level: Option<String>,
created_at: u64,
uptime_ms: u128,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ReadFileResponse {
sandbox_id: u64,
path: String,
content: String,
size_bytes: usize,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct WriteFileResponse {
sandbox_id: u64,
path: String,
size_bytes: usize,
written: bool,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct SnapshotResponse {
sandbox_id: u64,
size_bytes: usize,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ForkResponse {
original_sandbox_id: u64,
new_sandbox_id: u64,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct McpHttpResponse {
sandbox_id: u64,
status: u16,
headers: HashMap<String, String>,
body: String,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ListDirEntry {
name: String,
file_type: String,
size: u64,
is_symlink: bool,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ListDirResponse {
sandbox_id: u64,
path: String,
entries: Vec<ListDirEntry>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct ErrorResponse {
error: String,
}
impl MimoboxServer {
pub fn new() -> Self {
Self {
sandboxes: Arc::new(Mutex::new(HashMap::new())),
next_id: Arc::new(AtomicU64::new(1)),
tool_router: Self::tool_router(),
}
}
pub async fn cleanup_all(&self) {
let mut sandboxes = self.sandboxes.lock().await;
let count = sandboxes.len();
let drained = sandboxes.drain().collect::<Vec<_>>();
drop(sandboxes);
for (id, managed) in drained {
tracing::debug!(sandbox_id = id, "Signal cleanup: destroying sandbox");
match tokio::task::spawn_blocking(move || managed.sandbox.destroy()).await {
Ok(Ok(())) => {}
Ok(Err(err)) => {
tracing::warn!(
sandbox_id = id,
error = %format_sdk_error(err),
"Failed to destroy sandbox during signal cleanup"
);
}
Err(err) => {
tracing::warn!(
sandbox_id = id,
error = %format_join_error(err),
"Sandbox cleanup task failed during signal cleanup"
);
}
}
}
tracing::info!(count, "Signal cleanup complete");
}
async fn with_managed_sandbox<T, F>(&self, sandbox_id: u64, operation: F) -> Result<T, String>
where
T: Send + 'static,
F: FnOnce(&mut Sandbox) -> Result<T, SdkError> + Send + 'static,
{
let mut sandboxes = self.sandboxes.lock().await;
let mut managed = sandboxes
.remove(&sandbox_id)
.ok_or_else(|| sandbox_not_found(sandbox_id))?;
drop(sandboxes);
let (managed, result) = tokio::task::spawn_blocking(move || {
let result = operation(&mut managed.sandbox);
(managed, result)
})
.await
.map_err(format_join_error)?;
let mut sandboxes = self.sandboxes.lock().await;
sandboxes.insert(sandbox_id, managed);
result.map_err(format_sdk_error)
}
}
impl Default for MimoboxServer {
fn default() -> Self {
Self::new()
}
}
#[tool_router]
impl MimoboxServer {
#[tool(description = "Create a reusable mimobox sandbox instance")]
async fn create_sandbox(
&self,
Parameters(request): Parameters<CreateSandboxRequest>,
) -> Result<Json<CreateSandboxResponse>, Json<ErrorResponse>> {
let isolation =
parse_isolation_level(request.isolation_level.as_deref()).map_err(to_error)?;
let timeout_ms = request.timeout_ms;
let memory_limit_mb = request.memory_limit_mb;
let sandbox = tokio::task::spawn_blocking(move || {
create_sandbox_with_options(isolation, timeout_ms, memory_limit_mb)
})
.await
.map_err(|error| to_error(format_join_error(error)))?
.map_err(|error| to_error(format_sdk_error(error)))?;
let sandbox_id = self.next_id.fetch_add(1, Ordering::Relaxed);
let mut sandboxes = self.sandboxes.lock().await;
sandboxes.insert(
sandbox_id,
ManagedSandbox {
sandbox,
created_at_ms: unix_timestamp_ms(),
created_at_instant: Instant::now(),
},
);
Ok(Json(CreateSandboxResponse {
sandbox_id,
isolation_level: format_isolation_level(isolation).to_string(),
}))
}
#[tool(description = "Destroy a reusable mimobox sandbox and release its resources")]
async fn destroy_sandbox(
&self,
Parameters(request): Parameters<DestroySandboxRequest>,
) -> Result<Json<DestroySandboxResponse>, Json<ErrorResponse>> {
let mut sandboxes = self.sandboxes.lock().await;
let managed = sandboxes
.remove(&request.sandbox_id)
.ok_or_else(|| to_error(sandbox_not_found(request.sandbox_id)))?;
drop(sandboxes);
match tokio::task::spawn_blocking(move || managed.sandbox.destroy()).await {
Ok(Ok(())) => {}
Ok(Err(err)) => {
error!(
sandbox_id = request.sandbox_id,
error = %format_sdk_error(err),
"Sandbox destroy failed, instance removed from active list"
);
}
Err(err) => {
error!(
sandbox_id = request.sandbox_id,
error = %format_join_error(err),
"Sandbox destroy task failed, instance removed from active list"
);
}
}
Ok(Json(DestroySandboxResponse {
sandbox_id: request.sandbox_id,
destroyed: true,
}))
}
#[tool(description = "List active mimobox sandboxes with their IDs and basic metadata")]
async fn list_sandboxes(
&self,
Parameters(_request): Parameters<ListSandboxesRequest>,
) -> Result<Json<ListSandboxesResponse>, Json<ErrorResponse>> {
let sandboxes = self.sandboxes.lock().await;
let mut summaries = sandboxes
.iter()
.map(|(sandbox_id, managed)| SandboxSummary {
sandbox_id: *sandbox_id,
isolation_level: managed
.sandbox
.active_isolation()
.map(format_isolation_level)
.map(str::to_string),
created_at: managed.created_at_ms,
uptime_ms: managed.created_at_instant.elapsed().as_millis(),
})
.collect::<Vec<_>>();
summaries.sort_by_key(|summary| summary.sandbox_id);
Ok(Json(ListSandboxesResponse {
sandboxes: summaries,
}))
}
#[tool(description = "Execute a code snippet in a mimobox sandbox")]
async fn execute_code(
&self,
Parameters(request): Parameters<ExecuteCodeRequest>,
) -> Result<Json<ExecuteResponse>, Json<ErrorResponse>> {
let command =
build_code_command(request.language.as_deref(), &request.code).map_err(to_error)?;
let result = self
.execute_with_optional_sandbox(request.sandbox_id, &command, request.timeout_ms)
.await
.map_err(to_error)?;
Ok(Json(format_execute_result(result)))
}
#[tool(description = "Execute a shell command in a mimobox sandbox")]
async fn execute_command(
&self,
Parameters(request): Parameters<ExecuteCommandRequest>,
) -> Result<Json<ExecuteResponse>, Json<ErrorResponse>> {
let result = self
.execute_with_optional_sandbox(request.sandbox_id, &request.command, request.timeout_ms)
.await
.map_err(to_error)?;
Ok(Json(format_execute_result(result)))
}
#[tool(description = "Read a file from a microVM-backed mimobox sandbox as base64")]
async fn read_file(
&self,
Parameters(request): Parameters<ReadFileRequest>,
) -> Result<Json<ReadFileResponse>, Json<ErrorResponse>> {
#[cfg(feature = "vm")]
{
let path = request.path;
let content = self
.with_managed_sandbox(request.sandbox_id, {
let path = path.clone();
move |sandbox| sandbox.read_file(&path)
})
.await
.map_err(to_error)?;
let size_bytes = content.len();
Ok(Json(ReadFileResponse {
sandbox_id: request.sandbox_id,
path,
content: STANDARD.encode(&content),
size_bytes,
}))
}
#[cfg(not(feature = "vm"))]
{
let _ = request;
Err(to_error(vm_feature_required("read_file")))
}
}
#[tool(description = "Write a base64-encoded file into a microVM-backed mimobox sandbox")]
async fn write_file(
&self,
Parameters(request): Parameters<WriteFileRequest>,
) -> Result<Json<WriteFileResponse>, Json<ErrorResponse>> {
#[cfg(feature = "vm")]
{
let data = STANDARD
.decode(&request.content)
.map_err(|err| to_error(format!("content is not valid base64: {err}")))?;
let size_bytes = data.len();
let path = request.path;
self.with_managed_sandbox(request.sandbox_id, {
let path = path.clone();
move |sandbox| sandbox.write_file(&path, &data)
})
.await
.map_err(to_error)?;
Ok(Json(WriteFileResponse {
sandbox_id: request.sandbox_id,
path,
size_bytes,
written: true,
}))
}
#[cfg(not(feature = "vm"))]
{
let _ = request;
Err(to_error(vm_feature_required("write_file")))
}
}
#[tool(description = "Create a memory snapshot of a microVM-backed sandbox")]
async fn snapshot(
&self,
Parameters(request): Parameters<SnapshotRequest>,
) -> Result<Json<SnapshotResponse>, Json<ErrorResponse>> {
#[cfg(feature = "vm")]
{
let snapshot = self
.with_managed_sandbox(request.sandbox_id, |sandbox| sandbox.snapshot())
.await
.map_err(to_error)?;
Ok(Json(SnapshotResponse {
sandbox_id: request.sandbox_id,
size_bytes: snapshot.size(),
}))
}
#[cfg(not(feature = "vm"))]
{
let _ = request;
Err(to_error(vm_feature_required("snapshot")))
}
}
#[tool(
description = "Fork a microVM-backed sandbox, creating an independent copy with CoW memory"
)]
async fn fork(
&self,
Parameters(request): Parameters<ForkRequest>,
) -> Result<Json<ForkResponse>, Json<ErrorResponse>> {
#[cfg(feature = "vm")]
{
let mut sandboxes = self.sandboxes.lock().await;
let mut managed = sandboxes
.remove(&request.sandbox_id)
.ok_or_else(|| to_error(sandbox_not_found(request.sandbox_id)))?;
drop(sandboxes);
let (managed, fork_result) = tokio::task::spawn_blocking(move || {
let fork_result = managed.sandbox.fork();
(managed, fork_result)
})
.await
.map_err(|error| to_error(format_join_error(error)))?;
let forked = match fork_result {
Ok(forked) => forked,
Err(error) => {
let mut sandboxes = self.sandboxes.lock().await;
sandboxes.insert(request.sandbox_id, managed);
return Err(to_error(format_sdk_error(error)));
}
};
let new_id = self.next_id.fetch_add(1, Ordering::Relaxed);
let mut sandboxes = self.sandboxes.lock().await;
sandboxes.insert(request.sandbox_id, managed);
sandboxes.insert(
new_id,
ManagedSandbox {
sandbox: forked,
created_at_ms: unix_timestamp_ms(),
created_at_instant: Instant::now(),
},
);
Ok(Json(ForkResponse {
original_sandbox_id: request.sandbox_id,
new_sandbox_id: new_id,
}))
}
#[cfg(not(feature = "vm"))]
{
let _ = request;
Err(to_error(vm_feature_required("fork")))
}
}
#[tool(
description = "Execute an HTTP request from a microVM sandbox through a controlled proxy with domain whitelist"
)]
async fn http_request(
&self,
Parameters(request): Parameters<McpHttpRequest>,
) -> Result<Json<McpHttpResponse>, Json<ErrorResponse>> {
#[cfg(feature = "vm")]
{
let method = request.method.to_ascii_uppercase();
if !matches!(method.as_str(), "GET" | "POST") {
return Err(to_error("method only supports GET and POST".to_string()));
}
let url = request.url;
let response = self
.with_managed_sandbox(request.sandbox_id, move |sandbox| {
sandbox.http_request(&method, &url, HashMap::new(), None)
})
.await
.map_err(to_error)?;
Ok(Json(McpHttpResponse {
sandbox_id: request.sandbox_id,
status: response.status,
headers: response.headers,
body: String::from_utf8_lossy(&response.body).into_owned(),
}))
}
#[cfg(not(feature = "vm"))]
{
let _ = request;
Err(to_error(vm_feature_required("http_request")))
}
}
#[tool(description = "List directory entries in a mimobox sandbox")]
async fn list_dir(
&self,
Parameters(request): Parameters<ListDirRequest>,
) -> Result<Json<ListDirResponse>, Json<ErrorResponse>> {
let entries = self
.with_managed_sandbox(request.sandbox_id, {
let path = request.path.clone();
move |sandbox| sandbox.list_dir(&path)
})
.await
.map_err(to_error)?;
Ok(Json(ListDirResponse {
sandbox_id: request.sandbox_id,
path: request.path,
entries: entries.into_iter().map(format_list_dir_entry).collect(),
}))
}
async fn execute_with_optional_sandbox(
&self,
sandbox_id: Option<u64>,
command: &str,
timeout_ms: Option<u64>,
) -> Result<ExecuteResult, String> {
if let Some(sandbox_id) = sandbox_id {
let command = command.to_string();
return self
.with_managed_sandbox(sandbox_id, move |sandbox| sandbox.execute(&command))
.await;
}
let command = command.to_string();
tokio::task::spawn_blocking(move || {
let mut sandbox = create_sandbox_with_options(IsolationLevel::Auto, timeout_ms, None)?;
let result = sandbox.execute(&command);
if let Err(err) = sandbox.destroy() {
error!(error = %format_sdk_error(err), "Temporary sandbox destroy failed");
}
result
})
.await
.map_err(format_join_error)?
.map_err(format_sdk_error)
}
}
#[tool_handler(router = self.tool_router)]
impl ServerHandler for MimoboxServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build()).with_instructions(
"MimoBox MCP Server — Local Sandbox Runtime for AI Agents. Provides sandbox lifecycle, code execution, file transfer, snapshot, fork, and HTTP proxy tools.",
)
}
}
fn unix_timestamp_ms() -> u64 {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
millis.min(u128::from(u64::MAX)) as u64
}
fn create_sandbox_with_options(
isolation: IsolationLevel,
timeout_ms: Option<u64>,
memory_limit_mb: Option<u64>,
) -> Result<Sandbox, SdkError> {
let mut builder = Config::builder().isolation(isolation);
if let Some(timeout_ms) = timeout_ms {
builder = builder.timeout(Duration::from_millis(timeout_ms));
}
if let Some(memory_limit_mb) = memory_limit_mb {
builder = builder.memory_limit_mb(memory_limit_mb);
}
Sandbox::with_config(builder.build())
}
fn parse_isolation_level(value: Option<&str>) -> Result<IsolationLevel, String> {
match value.unwrap_or("auto").to_ascii_lowercase().as_str() {
"auto" => Ok(IsolationLevel::Auto),
"os" => Ok(IsolationLevel::Os),
"wasm" => Ok(IsolationLevel::Wasm),
"microvm" | "micro_vm" | "micro-vm" | "vm" => Ok(IsolationLevel::MicroVm),
other => Err(format!(
"unsupported isolation_level={other}, valid values: auto, os, wasm, microvm"
)),
}
}
fn format_isolation_level(level: IsolationLevel) -> &'static str {
match level {
IsolationLevel::Auto => "auto",
IsolationLevel::Os => "os",
IsolationLevel::Wasm => "wasm",
IsolationLevel::MicroVm => "microvm",
}
}
fn sandbox_not_found(sandbox_id: u64) -> String {
format!("sandbox instance not found for sandbox_id={sandbox_id}")
}
#[cfg(not(feature = "vm"))]
fn vm_feature_required(operation: &str) -> String {
format!(
"{operation} requires microVM backend; enable vm feature and use MicroVm isolation level"
)
}
fn build_code_command(language: Option<&str>, code: &str) -> Result<String, String> {
let escaped_code = shell_single_quote(code);
match language.unwrap_or("bash").to_ascii_lowercase().as_str() {
"python" | "python3" | "py" => Ok(format!("python3 -c {escaped_code}")),
"javascript" | "js" | "node" | "nodejs" => Ok(format!("node -e {escaped_code}")),
"bash" => Ok(format!("bash -c {escaped_code}")),
"sh" | "shell" => Ok(format!("sh -c {escaped_code}")),
other => Err(format!(
"unsupported language={other}, valid values: python, node, bash, sh"
)),
}
}
fn shell_single_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\\''"))
}
fn format_execute_result(result: ExecuteResult) -> ExecuteResponse {
ExecuteResponse {
stdout: String::from_utf8_lossy(&result.stdout).into_owned(),
stderr: String::from_utf8_lossy(&result.stderr).into_owned(),
exit_code: result.exit_code,
timed_out: result.timed_out,
elapsed_ms: result.elapsed.as_millis(),
}
}
fn format_list_dir_entry(entry: DirEntry) -> ListDirEntry {
ListDirEntry {
name: entry.name,
file_type: match entry.file_type {
FileType::File => "file".to_string(),
FileType::Dir => "dir".to_string(),
FileType::Symlink => "symlink".to_string(),
_ => "other".to_string(),
},
size: entry.size,
is_symlink: entry.is_symlink,
}
}
fn format_sdk_error(error: SdkError) -> String {
match error {
SdkError::Sandbox {
code,
message,
suggestion,
} => match suggestion {
Some(suggestion) => format!("[{}] {message}; suggestion: {suggestion}", code.as_str()),
None => format!("[{}] {message}", code.as_str()),
},
SdkError::BackendUnavailable(message) => format!("backend unavailable: {message}"),
SdkError::Config(message) => format!("config error: {message}"),
SdkError::Io(error) => format!("I/O error: {error}"),
error => format!("SDK error: {error}"),
}
}
fn format_join_error(error: JoinError) -> String {
format!("blocking task failed: {error}")
}
fn to_error(error: impl Into<String>) -> Json<ErrorResponse> {
Json(ErrorResponse {
error: error.into(),
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use mimobox_sdk::ErrorCode;
use std::time::Duration;
#[test]
fn test_parse_isolation_none_defaults_to_auto() {
let result = parse_isolation_level(None);
assert!(result.is_ok());
assert_eq!(result.unwrap(), IsolationLevel::Auto);
}
#[test]
fn test_parse_isolation_explicit_values() {
assert_eq!(
parse_isolation_level(Some("os")).unwrap(),
IsolationLevel::Os
);
assert_eq!(
parse_isolation_level(Some("wasm")).unwrap(),
IsolationLevel::Wasm
);
assert_eq!(
parse_isolation_level(Some("microvm")).unwrap(),
IsolationLevel::MicroVm
);
}
#[test]
fn test_parse_isolation_aliases() {
let aliases = ["micro_vm", "micro-vm", "vm"];
for alias in aliases {
assert_eq!(
parse_isolation_level(Some(alias)).unwrap(),
IsolationLevel::MicroVm,
"alias '{alias}' should resolve to MicroVm"
);
}
}
#[test]
fn test_parse_isolation_case_insensitive() {
assert_eq!(
parse_isolation_level(Some("AUTO")).unwrap(),
IsolationLevel::Auto
);
assert_eq!(
parse_isolation_level(Some("Os")).unwrap(),
IsolationLevel::Os
);
assert_eq!(
parse_isolation_level(Some("WASM")).unwrap(),
IsolationLevel::Wasm
);
assert_eq!(
parse_isolation_level(Some("MICROVM")).unwrap(),
IsolationLevel::MicroVm
);
}
#[test]
fn test_parse_isolation_invalid_values() {
let invalid = ["invalid", "docker", ""];
for val in invalid {
assert!(
parse_isolation_level(Some(val)).is_err(),
"'{val}' should be invalid"
);
}
}
#[test]
fn test_parse_isolation_none_same_as_auto_string() {
let from_none = parse_isolation_level(None).unwrap();
let from_auto = parse_isolation_level(Some("auto")).unwrap();
assert_eq!(from_none, from_auto);
}
#[test]
fn test_build_code_command_python_aliases() {
for lang in ["python", "python3", "py"] {
let cmd = build_code_command(Some(lang), "print(1)").unwrap();
assert!(
cmd.starts_with("python3 -c "),
"language='{lang}' should generate python3 command, got: {cmd}"
);
}
}
#[test]
fn test_build_code_command_node_aliases() {
for lang in ["node", "javascript", "js", "nodejs"] {
let cmd = build_code_command(Some(lang), "console.log(1)").unwrap();
assert!(
cmd.starts_with("node -e "),
"language='{lang}' should generate node command, got: {cmd}"
);
}
}
#[test]
fn test_build_code_command_bash_default() {
let cmd = build_code_command(None, "hello").unwrap();
assert_eq!(cmd, "bash -c 'hello'");
}
#[test]
fn test_build_code_command_sh_and_shell() {
let cmd_sh = build_code_command(Some("sh"), "echo hi").unwrap();
assert!(cmd_sh.starts_with("sh -c "));
let cmd_shell = build_code_command(Some("shell"), "echo hi").unwrap();
assert!(cmd_shell.starts_with("sh -c "));
}
#[test]
fn test_build_code_command_unsupported_language() {
let result = build_code_command(Some("ruby"), "puts 1");
assert!(result.is_err());
assert!(result.unwrap_err().contains("ruby"));
}
#[test]
fn test_shell_single_quote_simple() {
assert_eq!(shell_single_quote("hello"), "'hello'");
}
#[test]
fn test_shell_single_quote_empty() {
assert_eq!(shell_single_quote(""), "''");
}
#[test]
fn test_shell_single_quote_with_single_quote() {
assert_eq!(shell_single_quote("it's"), "'it'\\''s'");
}
#[test]
fn test_shell_single_quote_special_chars() {
let input = r#"hello "world" $var"#;
let quoted = shell_single_quote(input);
assert!(quoted.starts_with('\''));
assert!(quoted.ends_with('\''));
assert!(quoted.contains(r#"hello "world" $var"#));
}
#[test]
fn test_format_sdk_error_sandbox_with_suggestion() {
let err = SdkError::sandbox(
ErrorCode::CommandTimeout,
"timed out",
Some("increase timeout".to_string()),
);
let formatted = format_sdk_error(err);
assert!(
formatted.contains("[command_timeout]"),
"should contain error code"
);
assert!(formatted.contains("timed out"), "should contain message");
assert!(
formatted.contains("suggestion: increase timeout"),
"should contain suggestion"
);
}
#[test]
fn test_format_sdk_error_sandbox_without_suggestion() {
let err = SdkError::sandbox(ErrorCode::FileNotFound, "file not found", None);
let formatted = format_sdk_error(err);
assert!(formatted.contains("[file_not_found]"));
assert!(formatted.contains("file not found"));
assert!(
!formatted.contains("suggestion:"),
"No suggestion should be output when absent"
);
}
#[test]
fn test_format_sdk_error_backend_unavailable() {
let err = SdkError::BackendUnavailable("microvm");
let formatted = format_sdk_error(err);
assert!(formatted.contains("backend unavailable"));
assert!(formatted.contains("microvm"));
}
#[test]
fn test_format_sdk_error_config() {
let err = SdkError::Config("invalid config".to_string());
let formatted = format_sdk_error(err);
assert!(formatted.contains("config error"));
assert!(formatted.contains("invalid config"));
}
#[test]
fn test_format_sdk_error_io() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err = SdkError::Io(io_err);
let formatted = format_sdk_error(err);
assert!(formatted.contains("I/O error"));
assert!(formatted.contains("file not found"));
}
#[test]
fn test_format_isolation_level_roundtrip() {
assert_eq!(format_isolation_level(IsolationLevel::Auto), "auto");
assert_eq!(format_isolation_level(IsolationLevel::Os), "os");
assert_eq!(format_isolation_level(IsolationLevel::Wasm), "wasm");
assert_eq!(format_isolation_level(IsolationLevel::MicroVm), "microvm");
}
#[test]
fn test_format_execute_result_fields() {
let result = ExecuteResult::new(
b"out".to_vec(),
b"err".to_vec(),
Some(0),
false,
Duration::from_millis(42),
);
let resp = format_execute_result(result);
assert_eq!(resp.stdout, "out");
assert_eq!(resp.stderr, "err");
assert_eq!(resp.exit_code, Some(0));
assert!(!resp.timed_out);
assert_eq!(resp.elapsed_ms, 42);
}
#[test]
fn test_format_execute_result_non_utf8() {
let result = ExecuteResult::new(
vec![0xff, 0xfe],
vec![],
None,
true,
Duration::from_millis(100),
);
let resp = format_execute_result(result);
assert!(!resp.stdout.is_empty());
assert!(resp.stderr.is_empty());
assert_eq!(resp.exit_code, None);
assert!(resp.timed_out);
assert_eq!(resp.elapsed_ms, 100);
}
#[test]
fn test_sandbox_not_found_contains_id() {
let msg = sandbox_not_found(42);
assert!(msg.contains("42"), "should contain sandbox_id");
assert!(msg.contains("not found"), "should contain hint");
}
#[test]
fn test_unix_timestamp_ms_reasonable() {
let ts = unix_timestamp_ms();
assert!(
ts > 1_672_531_200_000,
"Timestamp should be after 2023, got: {ts}"
);
assert!(ts < 4_102_444_800_000, "Timestamp should not exceed 2100");
}
#[test]
fn test_unix_timestamp_ms_monotonic() {
let t1 = unix_timestamp_ms();
let t2 = unix_timestamp_ms();
assert!(
t2 >= t1,
"Consecutive calls should be monotonically non-decreasing"
);
}
#[test]
fn test_to_error_contains_message() {
let Json(err) = to_error("test error");
assert_eq!(err.error, "test error");
}
#[cfg(not(feature = "vm"))]
#[test]
fn test_vm_feature_required_message() {
let msg = vm_feature_required("snapshot");
assert!(msg.contains("snapshot"), "should contain operation name");
assert!(msg.contains("microVM") || msg.contains("vm feature"));
}
}