use crate::network::NetworkMode;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TeeConfig {
#[default]
None,
SevSnp {
workload_id: String,
#[serde(default)]
generation: SevSnpGeneration,
#[serde(default)]
simulate: bool,
},
Tdx {
workload_id: String,
#[serde(default)]
simulate: bool,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum SevSnpGeneration {
#[default]
Milan,
Genoa,
}
impl SevSnpGeneration {
pub fn as_str(&self) -> &'static str {
match self {
SevSnpGeneration::Milan => "milan",
SevSnpGeneration::Genoa => "genoa",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheConfig {
#[serde(default = "default_true")]
pub enabled: bool,
pub cache_dir: Option<PathBuf>,
#[serde(default = "default_max_rootfs_entries")]
pub max_rootfs_entries: usize,
#[serde(default = "default_max_cache_bytes")]
pub max_cache_bytes: u64,
}
fn default_true() -> bool {
true
}
fn default_max_rootfs_entries() -> usize {
10
}
fn default_max_cache_bytes() -> u64 {
10 * 1024 * 1024 * 1024 }
impl Default for CacheConfig {
fn default() -> Self {
Self {
enabled: true,
cache_dir: None,
max_rootfs_entries: 10,
max_cache_bytes: 10 * 1024 * 1024 * 1024,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PoolConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_min_idle")]
pub min_idle: usize,
#[serde(default = "default_max_pool_size")]
pub max_size: usize,
#[serde(default = "default_idle_ttl")]
pub idle_ttl_secs: u64,
#[serde(default)]
pub scaling: ScalingPolicy,
#[serde(default)]
pub snapshot_fork: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScalingPolicy {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_scale_up_threshold")]
pub scale_up_threshold: f64,
#[serde(default = "default_scale_down_threshold")]
pub scale_down_threshold: f64,
#[serde(default)]
pub max_min_idle: usize,
#[serde(default = "default_cooldown_secs")]
pub cooldown_secs: u64,
#[serde(default = "default_window_secs")]
pub window_secs: u64,
}
fn default_scale_up_threshold() -> f64 {
0.3
}
fn default_scale_down_threshold() -> f64 {
0.05
}
fn default_cooldown_secs() -> u64 {
60
}
fn default_window_secs() -> u64 {
120
}
impl Default for ScalingPolicy {
fn default() -> Self {
Self {
enabled: false,
scale_up_threshold: 0.3,
scale_down_threshold: 0.05,
max_min_idle: 0,
cooldown_secs: 60,
window_secs: 120,
}
}
}
fn default_min_idle() -> usize {
1
}
fn default_max_pool_size() -> usize {
5
}
fn default_idle_ttl() -> u64 {
300 }
impl Default for PoolConfig {
fn default() -> Self {
Self {
enabled: false,
min_idle: 1,
max_size: 5,
idle_ttl_secs: 300,
scaling: ScalingPolicy::default(),
snapshot_fork: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ResourceLimits {
#[serde(default)]
pub pids_limit: Option<u64>,
#[serde(default)]
pub cpuset_cpus: Option<String>,
#[serde(default)]
pub ulimits: Vec<String>,
#[serde(default)]
pub cpu_shares: Option<u64>,
#[serde(default)]
pub cpu_quota: Option<i64>,
#[serde(default)]
pub cpu_period: Option<u64>,
#[serde(default)]
pub memory_reservation: Option<u64>,
#[serde(default)]
pub memory_swap: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoxConfig {
#[serde(default)]
pub image: String,
pub workspace: PathBuf,
pub resources: ResourceConfig,
pub log_level: LogLevel,
pub debug_grpc: bool,
#[serde(default)]
pub tee: TeeConfig,
#[serde(default)]
pub cmd: Vec<String>,
#[serde(default)]
pub entrypoint_override: Option<Vec<String>>,
#[serde(default)]
pub user: Option<String>,
#[serde(default)]
pub workdir: Option<String>,
#[serde(default)]
pub hostname: Option<String>,
#[serde(default)]
pub volumes: Vec<String>,
#[serde(default)]
pub extra_env: Vec<(String, String)>,
#[serde(default)]
pub cache: CacheConfig,
#[serde(default)]
pub pool: PoolConfig,
#[serde(default)]
pub deferred_main: bool,
#[serde(default)]
pub ksm: bool,
#[serde(default)]
pub snapshot_mem_file: Option<String>,
#[serde(default)]
pub snapshot_sock: Option<String>,
#[serde(default)]
pub restore_from: Option<String>,
#[serde(default)]
pub port_map: Vec<String>,
#[serde(default)]
pub dns: Vec<String>,
#[serde(default)]
pub add_hosts: Vec<String>,
#[serde(default)]
pub network: NetworkMode,
#[serde(default)]
pub tmpfs: Vec<String>,
#[serde(default)]
pub resource_limits: ResourceLimits,
#[serde(default)]
pub cap_add: Vec<String>,
#[serde(default)]
pub cap_drop: Vec<String>,
#[serde(default)]
pub security_opt: Vec<String>,
#[serde(default)]
pub sysctls: Vec<(String, String)>,
#[serde(default)]
pub privileged: bool,
#[serde(default)]
pub read_only: bool,
#[serde(default)]
pub sidecar: Option<SidecarConfig>,
#[serde(default)]
pub persistent: bool,
}
impl Default for BoxConfig {
fn default() -> Self {
Self {
image: String::new(),
workspace: PathBuf::new(),
resources: ResourceConfig::default(),
log_level: LogLevel::Info,
debug_grpc: false,
tee: TeeConfig::default(),
cmd: vec![],
entrypoint_override: None,
user: None,
workdir: None,
hostname: None,
volumes: vec![],
extra_env: vec![],
cache: CacheConfig::default(),
pool: PoolConfig::default(),
deferred_main: false,
ksm: false,
snapshot_mem_file: None,
snapshot_sock: None,
restore_from: None,
port_map: vec![],
dns: vec![],
add_hosts: vec![],
network: NetworkMode::default(),
tmpfs: vec![],
resource_limits: ResourceLimits::default(),
cap_add: vec![],
cap_drop: vec![],
security_opt: vec![],
sysctls: vec![],
privileged: false,
read_only: false,
sidecar: None,
persistent: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SidecarConfig {
pub image: String,
#[serde(default = "default_sidecar_vsock_port")]
pub vsock_port: u32,
#[serde(default)]
pub env: Vec<(String, String)>,
}
fn default_sidecar_vsock_port() -> u32 {
4092
}
impl Default for SidecarConfig {
fn default() -> Self {
Self {
image: String::new(),
vsock_port: default_sidecar_vsock_port(),
env: vec![],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResourceConfig {
pub vcpus: u32,
pub memory_mb: u32,
pub disk_mb: u32,
pub timeout: u64,
}
impl Default for ResourceConfig {
fn default() -> Self {
Self {
vcpus: 2,
memory_mb: 1024,
disk_mb: 4096,
timeout: 3600, }
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
impl From<LogLevel> for tracing::Level {
fn from(level: LogLevel) -> Self {
match level {
LogLevel::Debug => tracing::Level::DEBUG,
LogLevel::Info => tracing::Level::INFO,
LogLevel::Warn => tracing::Level::WARN,
LogLevel::Error => tracing::Level::ERROR,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_box_config_default() {
let config = BoxConfig::default();
assert!(config.image.is_empty());
assert!(config.workspace.as_os_str().is_empty());
assert_eq!(config.resources.vcpus, 2);
assert!(!config.debug_grpc);
assert!(!config.read_only);
assert!(config.user.is_none());
assert!(config.workdir.is_none());
assert!(config.hostname.is_none());
assert!(config.add_hosts.is_empty());
}
#[test]
fn test_box_config_read_only_default_false() {
let config = BoxConfig::default();
assert!(!config.read_only);
}
#[test]
fn test_box_config_read_only_serde() {
let json = r#"{"image":"test","workspace":"","resources":{"vcpus":2,"memory_mb":512,"disk_mb":4096,"timeout":3600},"log_level":"Info","debug_grpc":false}"#;
let config: BoxConfig = serde_json::from_str(json).unwrap();
assert!(!config.read_only);
let config = BoxConfig {
read_only: true,
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let deserialized: BoxConfig = serde_json::from_str(&json).unwrap();
assert!(deserialized.read_only);
}
#[test]
fn test_box_config_user_workdir_serde() {
let config = BoxConfig {
user: Some("1000:1000".to_string()),
workdir: Some("/app".to_string()),
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.user.as_deref(), Some("1000:1000"));
assert_eq!(parsed.workdir.as_deref(), Some("/app"));
}
#[test]
fn test_box_config_hostname_add_hosts_serde() {
let config = BoxConfig {
hostname: Some("web".to_string()),
add_hosts: vec!["db.local:10.88.0.10".to_string()],
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.hostname.as_deref(), Some("web"));
assert_eq!(parsed.add_hosts, vec!["db.local:10.88.0.10"]);
}
#[test]
fn test_resource_config_default() {
let config = ResourceConfig::default();
assert_eq!(config.vcpus, 2);
assert_eq!(config.memory_mb, 1024);
assert_eq!(config.disk_mb, 4096);
assert_eq!(config.timeout, 3600);
}
#[test]
fn test_resource_config_custom() {
let config = ResourceConfig {
vcpus: 4,
memory_mb: 2048,
disk_mb: 8192,
timeout: 7200,
};
assert_eq!(config.vcpus, 4);
assert_eq!(config.memory_mb, 2048);
assert_eq!(config.disk_mb, 8192);
assert_eq!(config.timeout, 7200);
}
#[test]
fn test_log_level_conversion() {
assert_eq!(tracing::Level::from(LogLevel::Debug), tracing::Level::DEBUG);
assert_eq!(tracing::Level::from(LogLevel::Info), tracing::Level::INFO);
assert_eq!(tracing::Level::from(LogLevel::Warn), tracing::Level::WARN);
assert_eq!(tracing::Level::from(LogLevel::Error), tracing::Level::ERROR);
}
#[test]
fn test_box_config_serialization() {
let config = BoxConfig::default();
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("workspace"));
assert!(json.contains("resources"));
}
#[test]
fn test_box_config_deserialization() {
let json = r#"{
"image": "nginx:alpine",
"workspace": "/tmp/workspace",
"resources": {
"vcpus": 4,
"memory_mb": 2048,
"disk_mb": 8192,
"timeout": 1800
},
"log_level": "Debug",
"debug_grpc": true
}"#;
let config: BoxConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.image, "nginx:alpine");
assert_eq!(config.workspace.to_str().unwrap(), "/tmp/workspace");
assert_eq!(config.resources.vcpus, 4);
assert!(config.debug_grpc);
}
#[test]
fn test_resource_config_serialization() {
let config = ResourceConfig {
vcpus: 8,
memory_mb: 4096,
disk_mb: 16384,
timeout: 0,
};
let json = serde_json::to_string(&config).unwrap();
let parsed: ResourceConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.vcpus, 8);
assert_eq!(parsed.memory_mb, 4096);
assert_eq!(parsed.timeout, 0); }
#[test]
fn test_log_level_serialization() {
let levels = vec![
LogLevel::Debug,
LogLevel::Info,
LogLevel::Warn,
LogLevel::Error,
];
for level in levels {
let json = serde_json::to_string(&level).unwrap();
let parsed: LogLevel = serde_json::from_str(&json).unwrap();
assert_eq!(tracing::Level::from(parsed), tracing::Level::from(level));
}
}
#[test]
fn test_config_clone() {
let config = BoxConfig::default();
let cloned = config.clone();
assert_eq!(config.workspace, cloned.workspace);
assert_eq!(config.resources.vcpus, cloned.resources.vcpus);
}
#[test]
fn test_config_debug() {
let config = BoxConfig::default();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("BoxConfig"));
assert!(debug_str.contains("workspace"));
}
#[test]
fn test_tee_config_default() {
let tee = TeeConfig::default();
assert_eq!(tee, TeeConfig::None);
}
#[test]
fn test_tee_config_sev_snp() {
let tee = TeeConfig::SevSnp {
workload_id: "test-agent".to_string(),
generation: SevSnpGeneration::Milan,
simulate: false,
};
match tee {
TeeConfig::SevSnp {
workload_id,
generation,
simulate,
} => {
assert_eq!(workload_id, "test-agent");
assert_eq!(generation, SevSnpGeneration::Milan);
assert!(!simulate);
}
_ => panic!("Expected SevSnp variant"),
}
}
#[test]
fn test_sev_snp_generation_as_str() {
assert_eq!(SevSnpGeneration::Milan.as_str(), "milan");
assert_eq!(SevSnpGeneration::Genoa.as_str(), "genoa");
}
#[test]
fn test_sev_snp_generation_default() {
let gen = SevSnpGeneration::default();
assert_eq!(gen, SevSnpGeneration::Milan);
}
#[test]
fn test_tee_config_serialization() {
let tee = TeeConfig::SevSnp {
workload_id: "my-workload".to_string(),
generation: SevSnpGeneration::Genoa,
simulate: false,
};
let json = serde_json::to_string(&tee).unwrap();
let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, tee);
}
#[test]
fn test_tee_config_none_serialization() {
let tee = TeeConfig::None;
let json = serde_json::to_string(&tee).unwrap();
let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, TeeConfig::None);
}
#[test]
fn test_tee_config_tdx() {
let tee = TeeConfig::Tdx {
workload_id: "tdx-workload".to_string(),
simulate: false,
};
let json = serde_json::to_string(&tee).unwrap();
let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
match parsed {
TeeConfig::Tdx {
workload_id,
simulate,
} => {
assert_eq!(workload_id, "tdx-workload");
assert!(!simulate);
}
_ => panic!("Expected Tdx variant"),
}
}
#[test]
fn test_tee_config_tdx_simulate() {
let tee = TeeConfig::Tdx {
workload_id: "test".to_string(),
simulate: true,
};
let json = serde_json::to_string(&tee).unwrap();
let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
match parsed {
TeeConfig::Tdx { simulate, .. } => assert!(simulate),
_ => panic!("Expected Tdx variant"),
}
}
#[test]
fn test_box_config_with_tee() {
let config = BoxConfig {
tee: TeeConfig::SevSnp {
workload_id: "secure-agent".to_string(),
generation: SevSnpGeneration::Milan,
simulate: false,
},
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
match parsed.tee {
TeeConfig::SevSnp {
workload_id,
generation,
simulate,
} => {
assert_eq!(workload_id, "secure-agent");
assert_eq!(generation, SevSnpGeneration::Milan);
assert!(!simulate);
}
_ => panic!("Expected SevSnp TEE config"),
}
}
#[test]
fn test_box_config_default_has_no_tee() {
let config = BoxConfig::default();
assert_eq!(config.tee, TeeConfig::None);
}
#[test]
fn test_cache_config_default() {
let config = CacheConfig::default();
assert!(config.enabled);
assert!(config.cache_dir.is_none());
assert_eq!(config.max_rootfs_entries, 10);
assert_eq!(config.max_cache_bytes, 10 * 1024 * 1024 * 1024);
}
#[test]
fn test_cache_config_serialization() {
let config = CacheConfig {
enabled: false,
cache_dir: Some(PathBuf::from("/tmp/cache")),
max_rootfs_entries: 5,
max_cache_bytes: 1024 * 1024 * 1024,
};
let json = serde_json::to_string(&config).unwrap();
let parsed: CacheConfig = serde_json::from_str(&json).unwrap();
assert!(!parsed.enabled);
assert_eq!(parsed.cache_dir, Some(PathBuf::from("/tmp/cache")));
assert_eq!(parsed.max_rootfs_entries, 5);
assert_eq!(parsed.max_cache_bytes, 1024 * 1024 * 1024);
}
#[test]
fn test_cache_config_deserialization_defaults() {
let json = "{}";
let config: CacheConfig = serde_json::from_str(json).unwrap();
assert!(config.enabled);
assert!(config.cache_dir.is_none());
assert_eq!(config.max_rootfs_entries, 10);
assert_eq!(config.max_cache_bytes, 10 * 1024 * 1024 * 1024);
}
#[test]
fn test_pool_config_default() {
let config = PoolConfig::default();
assert!(!config.enabled);
assert_eq!(config.min_idle, 1);
assert_eq!(config.max_size, 5);
assert_eq!(config.idle_ttl_secs, 300);
}
#[test]
fn test_pool_config_serialization() {
let config = PoolConfig {
enabled: true,
min_idle: 3,
max_size: 10,
idle_ttl_secs: 600,
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let parsed: PoolConfig = serde_json::from_str(&json).unwrap();
assert!(parsed.enabled);
assert_eq!(parsed.min_idle, 3);
assert_eq!(parsed.max_size, 10);
assert_eq!(parsed.idle_ttl_secs, 600);
}
#[test]
fn test_pool_config_deserialization_defaults() {
let json = "{}";
let config: PoolConfig = serde_json::from_str(json).unwrap();
assert!(!config.enabled);
assert_eq!(config.min_idle, 1);
assert_eq!(config.max_size, 5);
assert_eq!(config.idle_ttl_secs, 300);
}
#[test]
fn test_box_config_default_has_cache_and_pool() {
let config = BoxConfig::default();
assert!(config.cache.enabled);
assert!(!config.pool.enabled);
}
#[test]
fn test_box_config_with_cache_serialization() {
let config = BoxConfig {
cache: CacheConfig {
enabled: false,
cache_dir: Some(PathBuf::from("/custom/cache")),
max_rootfs_entries: 20,
max_cache_bytes: 5 * 1024 * 1024 * 1024,
},
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
assert!(!parsed.cache.enabled);
assert_eq!(parsed.cache.cache_dir, Some(PathBuf::from("/custom/cache")));
assert_eq!(parsed.cache.max_rootfs_entries, 20);
}
#[test]
fn test_box_config_with_pool_serialization() {
let config = BoxConfig {
pool: PoolConfig {
enabled: true,
min_idle: 2,
max_size: 8,
idle_ttl_secs: 120,
..Default::default()
},
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
assert!(parsed.pool.enabled);
assert_eq!(parsed.pool.min_idle, 2);
assert_eq!(parsed.pool.max_size, 8);
assert_eq!(parsed.pool.idle_ttl_secs, 120);
}
#[test]
fn test_box_config_backward_compatible_deserialization() {
let json = r#"{
"workspace": "/tmp/workspace",
"resources": {
"vcpus": 2,
"memory_mb": 1024,
"disk_mb": 4096,
"timeout": 3600
},
"log_level": "Info",
"debug_grpc": false
}"#;
let config: BoxConfig = serde_json::from_str(json).unwrap();
assert!(config.cache.enabled);
assert!(!config.pool.enabled);
}
#[test]
fn test_resource_limits_default() {
let limits = ResourceLimits::default();
assert!(limits.pids_limit.is_none());
assert!(limits.cpuset_cpus.is_none());
assert!(limits.ulimits.is_empty());
assert!(limits.cpu_shares.is_none());
assert!(limits.cpu_quota.is_none());
assert!(limits.cpu_period.is_none());
assert!(limits.memory_reservation.is_none());
assert!(limits.memory_swap.is_none());
}
#[test]
fn test_resource_limits_serialization() {
let limits = ResourceLimits {
pids_limit: Some(100),
cpuset_cpus: Some("0,1".to_string()),
ulimits: vec!["nofile=1024:4096".to_string()],
cpu_shares: Some(512),
cpu_quota: Some(50000),
cpu_period: Some(100000),
memory_reservation: Some(256 * 1024 * 1024),
memory_swap: Some(1024 * 1024 * 1024),
};
let json = serde_json::to_string(&limits).unwrap();
let parsed: ResourceLimits = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.pids_limit, Some(100));
assert_eq!(parsed.cpuset_cpus, Some("0,1".to_string()));
assert_eq!(parsed.ulimits, vec!["nofile=1024:4096"]);
assert_eq!(parsed.cpu_shares, Some(512));
assert_eq!(parsed.cpu_quota, Some(50000));
assert_eq!(parsed.cpu_period, Some(100000));
assert_eq!(parsed.memory_reservation, Some(256 * 1024 * 1024));
assert_eq!(parsed.memory_swap, Some(1024 * 1024 * 1024));
}
#[test]
fn test_resource_limits_deserialization_defaults() {
let json = "{}";
let limits: ResourceLimits = serde_json::from_str(json).unwrap();
assert!(limits.pids_limit.is_none());
assert!(limits.ulimits.is_empty());
}
#[test]
fn test_resource_limits_memory_swap_unlimited() {
let limits = ResourceLimits {
memory_swap: Some(-1),
..Default::default()
};
let json = serde_json::to_string(&limits).unwrap();
let parsed: ResourceLimits = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.memory_swap, Some(-1));
}
#[test]
fn test_box_config_with_resource_limits() {
let config = BoxConfig {
resource_limits: ResourceLimits {
pids_limit: Some(256),
cpu_shares: Some(1024),
..Default::default()
},
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.resource_limits.pids_limit, Some(256));
assert_eq!(parsed.resource_limits.cpu_shares, Some(1024));
}
#[test]
fn test_box_config_backward_compat_no_resource_limits() {
let json = r#"{
"workspace": "/tmp/workspace",
"resources": {
"vcpus": 2,
"memory_mb": 1024,
"disk_mb": 4096,
"timeout": 3600
},
"log_level": "Info",
"debug_grpc": false
}"#;
let config: BoxConfig = serde_json::from_str(json).unwrap();
assert!(config.resource_limits.pids_limit.is_none());
assert!(config.resource_limits.ulimits.is_empty());
}
#[test]
fn test_sidecar_config_default() {
let s = SidecarConfig::default();
assert!(s.image.is_empty());
assert_eq!(s.vsock_port, 4092);
assert!(s.env.is_empty());
}
#[test]
fn test_sidecar_config_roundtrip() {
let s = SidecarConfig {
image: "ghcr.io/a3s-lab/safeclaw:latest".to_string(),
vsock_port: 4092,
env: vec![
("LOG_LEVEL".to_string(), "debug".to_string()),
("MODE".to_string(), "proxy".to_string()),
],
};
let json = serde_json::to_string(&s).unwrap();
let parsed: SidecarConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.image, "ghcr.io/a3s-lab/safeclaw:latest");
assert_eq!(parsed.vsock_port, 4092);
assert_eq!(parsed.env.len(), 2);
assert_eq!(
parsed.env[0],
("LOG_LEVEL".to_string(), "debug".to_string())
);
}
#[test]
fn test_sidecar_config_default_vsock_port_from_json() {
let json = r#"{"image":"safeclaw:latest"}"#;
let s: SidecarConfig = serde_json::from_str(json).unwrap();
assert_eq!(s.vsock_port, 4092);
assert!(s.env.is_empty());
}
#[test]
fn test_box_config_default_has_no_sidecar() {
let config = BoxConfig::default();
assert!(config.sidecar.is_none());
}
#[test]
fn test_box_config_with_sidecar_roundtrip() {
let config = BoxConfig {
sidecar: Some(SidecarConfig {
image: "safeclaw:latest".to_string(),
vsock_port: 4092,
env: vec![],
}),
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
let sidecar = parsed.sidecar.unwrap();
assert_eq!(sidecar.image, "safeclaw:latest");
assert_eq!(sidecar.vsock_port, 4092);
}
#[test]
fn test_box_config_without_sidecar_deserializes_as_none() {
let config = BoxConfig::default();
let json = serde_json::to_string(&config).unwrap();
let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
assert!(parsed.sidecar.is_none());
}
}