1use crate::network::NetworkMode;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
7#[serde(tag = "kind", rename_all = "snake_case")]
8pub enum TeeConfig {
9 #[default]
11 None,
12
13 SevSnp {
15 workload_id: String,
17 #[serde(default)]
19 generation: SevSnpGeneration,
20 #[serde(default)]
22 simulate: bool,
23 },
24
25 Tdx {
27 workload_id: String,
29 #[serde(default)]
31 simulate: bool,
32 },
33}
34
35#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
37#[serde(rename_all = "lowercase")]
38pub enum SevSnpGeneration {
39 #[default]
41 Milan,
42 Genoa,
44}
45
46impl SevSnpGeneration {
47 pub fn as_str(&self) -> &'static str {
49 match self {
50 SevSnpGeneration::Milan => "milan",
51 SevSnpGeneration::Genoa => "genoa",
52 }
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct CacheConfig {
59 #[serde(default = "default_true")]
61 pub enabled: bool,
62
63 pub cache_dir: Option<PathBuf>,
65
66 #[serde(default = "default_max_rootfs_entries")]
68 pub max_rootfs_entries: usize,
69
70 #[serde(default = "default_max_cache_bytes")]
72 pub max_cache_bytes: u64,
73}
74
75fn default_true() -> bool {
76 true
77}
78
79fn default_max_rootfs_entries() -> usize {
80 10
81}
82
83fn default_max_cache_bytes() -> u64 {
84 10 * 1024 * 1024 * 1024 }
86
87impl Default for CacheConfig {
88 fn default() -> Self {
89 Self {
90 enabled: true,
91 cache_dir: None,
92 max_rootfs_entries: 10,
93 max_cache_bytes: 10 * 1024 * 1024 * 1024,
94 }
95 }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct PoolConfig {
101 #[serde(default)]
103 pub enabled: bool,
104
105 #[serde(default = "default_min_idle")]
107 pub min_idle: usize,
108
109 #[serde(default = "default_max_pool_size")]
111 pub max_size: usize,
112
113 #[serde(default = "default_idle_ttl")]
115 pub idle_ttl_secs: u64,
116
117 #[serde(default)]
119 pub scaling: ScalingPolicy,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ScalingPolicy {
128 #[serde(default)]
130 pub enabled: bool,
131
132 #[serde(default = "default_scale_up_threshold")]
134 pub scale_up_threshold: f64,
135
136 #[serde(default = "default_scale_down_threshold")]
138 pub scale_down_threshold: f64,
139
140 #[serde(default)]
142 pub max_min_idle: usize,
143
144 #[serde(default = "default_cooldown_secs")]
146 pub cooldown_secs: u64,
147
148 #[serde(default = "default_window_secs")]
150 pub window_secs: u64,
151}
152
153fn default_scale_up_threshold() -> f64 {
154 0.3
155}
156
157fn default_scale_down_threshold() -> f64 {
158 0.05
159}
160
161fn default_cooldown_secs() -> u64 {
162 60
163}
164
165fn default_window_secs() -> u64 {
166 120
167}
168
169impl Default for ScalingPolicy {
170 fn default() -> Self {
171 Self {
172 enabled: false,
173 scale_up_threshold: 0.3,
174 scale_down_threshold: 0.05,
175 max_min_idle: 0,
176 cooldown_secs: 60,
177 window_secs: 120,
178 }
179 }
180}
181
182fn default_min_idle() -> usize {
183 1
184}
185
186fn default_max_pool_size() -> usize {
187 5
188}
189
190fn default_idle_ttl() -> u64 {
191 300 }
193
194impl Default for PoolConfig {
195 fn default() -> Self {
196 Self {
197 enabled: false,
198 min_idle: 1,
199 max_size: 5,
200 idle_ttl_secs: 300,
201 scaling: ScalingPolicy::default(),
202 }
203 }
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, Default)]
211pub struct ResourceLimits {
212 #[serde(default)]
215 pub pids_limit: Option<u64>,
216
217 #[serde(default)]
220 pub cpuset_cpus: Option<String>,
221
222 #[serde(default)]
224 pub ulimits: Vec<String>,
225
226 #[serde(default)]
229 pub cpu_shares: Option<u64>,
230
231 #[serde(default)]
234 pub cpu_quota: Option<i64>,
235
236 #[serde(default)]
239 pub cpu_period: Option<u64>,
240
241 #[serde(default)]
244 pub memory_reservation: Option<u64>,
245
246 #[serde(default)]
249 pub memory_swap: Option<i64>,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct BoxConfig {
255 #[serde(default)]
257 pub image: String,
258
259 pub workspace: PathBuf,
261
262 pub resources: ResourceConfig,
264
265 pub log_level: LogLevel,
267
268 pub debug_grpc: bool,
270
271 #[serde(default)]
273 pub tee: TeeConfig,
274
275 #[serde(default)]
277 pub cmd: Vec<String>,
278
279 #[serde(default)]
281 pub entrypoint_override: Option<Vec<String>>,
282
283 #[serde(default)]
285 pub volumes: Vec<String>,
286
287 #[serde(default)]
289 pub extra_env: Vec<(String, String)>,
290
291 #[serde(default)]
293 pub cache: CacheConfig,
294
295 #[serde(default)]
297 pub pool: PoolConfig,
298
299 #[serde(default)]
302 pub port_map: Vec<String>,
303
304 #[serde(default)]
307 pub dns: Vec<String>,
308
309 #[serde(default)]
311 pub network: NetworkMode,
312
313 #[serde(default)]
316 pub tmpfs: Vec<String>,
317
318 #[serde(default)]
320 pub resource_limits: ResourceLimits,
321
322 #[serde(default)]
324 pub cap_add: Vec<String>,
325
326 #[serde(default)]
328 pub cap_drop: Vec<String>,
329
330 #[serde(default)]
332 pub security_opt: Vec<String>,
333
334 #[serde(default)]
336 pub privileged: bool,
337
338 #[serde(default)]
343 pub read_only: bool,
344
345 #[serde(default)]
351 pub sidecar: Option<SidecarConfig>,
352
353 #[serde(default)]
362 pub persistent: bool,
363}
364
365impl Default for BoxConfig {
366 fn default() -> Self {
367 Self {
368 image: String::new(),
369 workspace: PathBuf::new(),
372 resources: ResourceConfig::default(),
373 log_level: LogLevel::Info,
374 debug_grpc: false,
375 tee: TeeConfig::default(),
376 cmd: vec![],
377 entrypoint_override: None,
378 volumes: vec![],
379 extra_env: vec![],
380 cache: CacheConfig::default(),
381 pool: PoolConfig::default(),
382 port_map: vec![],
383 dns: vec![],
384 network: NetworkMode::default(),
385 tmpfs: vec![],
386 resource_limits: ResourceLimits::default(),
387 cap_add: vec![],
388 cap_drop: vec![],
389 security_opt: vec![],
390 privileged: false,
391 read_only: false,
392 sidecar: None,
393 persistent: false,
394 }
395 }
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct SidecarConfig {
414 pub image: String,
416
417 #[serde(default = "default_sidecar_vsock_port")]
419 pub vsock_port: u32,
420
421 #[serde(default)]
423 pub env: Vec<(String, String)>,
424}
425
426fn default_sidecar_vsock_port() -> u32 {
427 4092
428}
429
430impl Default for SidecarConfig {
431 fn default() -> Self {
432 Self {
433 image: String::new(),
434 vsock_port: default_sidecar_vsock_port(),
435 env: vec![],
436 }
437 }
438}
439
440#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct ResourceConfig {
443 pub vcpus: u32,
445
446 pub memory_mb: u32,
448
449 pub disk_mb: u32,
451
452 pub timeout: u64,
454}
455
456impl Default for ResourceConfig {
457 fn default() -> Self {
458 Self {
459 vcpus: 2,
460 memory_mb: 1024,
461 disk_mb: 4096,
462 timeout: 3600, }
464 }
465}
466
467#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
469pub enum LogLevel {
470 Debug,
471 Info,
472 Warn,
473 Error,
474}
475
476impl From<LogLevel> for tracing::Level {
477 fn from(level: LogLevel) -> Self {
478 match level {
479 LogLevel::Debug => tracing::Level::DEBUG,
480 LogLevel::Info => tracing::Level::INFO,
481 LogLevel::Warn => tracing::Level::WARN,
482 LogLevel::Error => tracing::Level::ERROR,
483 }
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn test_box_config_default() {
493 let config = BoxConfig::default();
494
495 assert!(config.image.is_empty());
496 assert!(config.workspace.as_os_str().is_empty());
498 assert_eq!(config.resources.vcpus, 2);
499 assert!(!config.debug_grpc);
500 assert!(!config.read_only);
501 }
502
503 #[test]
504 fn test_box_config_read_only_default_false() {
505 let config = BoxConfig::default();
506 assert!(!config.read_only);
507 }
508
509 #[test]
510 fn test_box_config_read_only_serde() {
511 let json = r#"{"image":"test","workspace":"","resources":{"vcpus":2,"memory_mb":512,"disk_mb":4096,"timeout":3600},"log_level":"Info","debug_grpc":false}"#;
513 let config: BoxConfig = serde_json::from_str(json).unwrap();
514 assert!(!config.read_only);
515
516 let config = BoxConfig {
518 read_only: true,
519 ..Default::default()
520 };
521 let json = serde_json::to_string(&config).unwrap();
522 let deserialized: BoxConfig = serde_json::from_str(&json).unwrap();
523 assert!(deserialized.read_only);
524 }
525
526 #[test]
527 fn test_resource_config_default() {
528 let config = ResourceConfig::default();
529
530 assert_eq!(config.vcpus, 2);
531 assert_eq!(config.memory_mb, 1024);
532 assert_eq!(config.disk_mb, 4096);
533 assert_eq!(config.timeout, 3600);
534 }
535
536 #[test]
537 fn test_resource_config_custom() {
538 let config = ResourceConfig {
539 vcpus: 4,
540 memory_mb: 2048,
541 disk_mb: 8192,
542 timeout: 7200,
543 };
544
545 assert_eq!(config.vcpus, 4);
546 assert_eq!(config.memory_mb, 2048);
547 assert_eq!(config.disk_mb, 8192);
548 assert_eq!(config.timeout, 7200);
549 }
550
551 #[test]
552 fn test_log_level_conversion() {
553 assert_eq!(tracing::Level::from(LogLevel::Debug), tracing::Level::DEBUG);
554 assert_eq!(tracing::Level::from(LogLevel::Info), tracing::Level::INFO);
555 assert_eq!(tracing::Level::from(LogLevel::Warn), tracing::Level::WARN);
556 assert_eq!(tracing::Level::from(LogLevel::Error), tracing::Level::ERROR);
557 }
558
559 #[test]
560 fn test_box_config_serialization() {
561 let config = BoxConfig::default();
562 let json = serde_json::to_string(&config).unwrap();
563
564 assert!(json.contains("workspace"));
565 assert!(json.contains("resources"));
566 }
567
568 #[test]
569 fn test_box_config_deserialization() {
570 let json = r#"{
571 "image": "nginx:alpine",
572 "workspace": "/tmp/workspace",
573 "resources": {
574 "vcpus": 4,
575 "memory_mb": 2048,
576 "disk_mb": 8192,
577 "timeout": 1800
578 },
579 "log_level": "Debug",
580 "debug_grpc": true
581 }"#;
582
583 let config: BoxConfig = serde_json::from_str(json).unwrap();
584 assert_eq!(config.image, "nginx:alpine");
585 assert_eq!(config.workspace.to_str().unwrap(), "/tmp/workspace");
586 assert_eq!(config.resources.vcpus, 4);
587 assert!(config.debug_grpc);
588 }
589
590 #[test]
591 fn test_resource_config_serialization() {
592 let config = ResourceConfig {
593 vcpus: 8,
594 memory_mb: 4096,
595 disk_mb: 16384,
596 timeout: 0,
597 };
598
599 let json = serde_json::to_string(&config).unwrap();
600 let parsed: ResourceConfig = serde_json::from_str(&json).unwrap();
601
602 assert_eq!(parsed.vcpus, 8);
603 assert_eq!(parsed.memory_mb, 4096);
604 assert_eq!(parsed.timeout, 0); }
606
607 #[test]
608 fn test_log_level_serialization() {
609 let levels = vec![
610 LogLevel::Debug,
611 LogLevel::Info,
612 LogLevel::Warn,
613 LogLevel::Error,
614 ];
615
616 for level in levels {
617 let json = serde_json::to_string(&level).unwrap();
618 let parsed: LogLevel = serde_json::from_str(&json).unwrap();
619 assert_eq!(tracing::Level::from(parsed), tracing::Level::from(level));
620 }
621 }
622
623 #[test]
624 fn test_config_clone() {
625 let config = BoxConfig::default();
626 let cloned = config.clone();
627
628 assert_eq!(config.workspace, cloned.workspace);
629 assert_eq!(config.resources.vcpus, cloned.resources.vcpus);
630 }
631
632 #[test]
633 fn test_config_debug() {
634 let config = BoxConfig::default();
635 let debug_str = format!("{:?}", config);
636
637 assert!(debug_str.contains("BoxConfig"));
638 assert!(debug_str.contains("workspace"));
639 }
640
641 #[test]
642 fn test_tee_config_default() {
643 let tee = TeeConfig::default();
644 assert_eq!(tee, TeeConfig::None);
645 }
646
647 #[test]
648 fn test_tee_config_sev_snp() {
649 let tee = TeeConfig::SevSnp {
650 workload_id: "test-agent".to_string(),
651 generation: SevSnpGeneration::Milan,
652 simulate: false,
653 };
654
655 match tee {
656 TeeConfig::SevSnp {
657 workload_id,
658 generation,
659 simulate,
660 } => {
661 assert_eq!(workload_id, "test-agent");
662 assert_eq!(generation, SevSnpGeneration::Milan);
663 assert!(!simulate);
664 }
665 _ => panic!("Expected SevSnp variant"),
666 }
667 }
668
669 #[test]
670 fn test_sev_snp_generation_as_str() {
671 assert_eq!(SevSnpGeneration::Milan.as_str(), "milan");
672 assert_eq!(SevSnpGeneration::Genoa.as_str(), "genoa");
673 }
674
675 #[test]
676 fn test_sev_snp_generation_default() {
677 let gen = SevSnpGeneration::default();
678 assert_eq!(gen, SevSnpGeneration::Milan);
679 }
680
681 #[test]
682 fn test_tee_config_serialization() {
683 let tee = TeeConfig::SevSnp {
684 workload_id: "my-workload".to_string(),
685 generation: SevSnpGeneration::Genoa,
686 simulate: false,
687 };
688
689 let json = serde_json::to_string(&tee).unwrap();
690 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
691
692 assert_eq!(parsed, tee);
693 }
694
695 #[test]
696 fn test_tee_config_none_serialization() {
697 let tee = TeeConfig::None;
698 let json = serde_json::to_string(&tee).unwrap();
699 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
700
701 assert_eq!(parsed, TeeConfig::None);
702 }
703
704 #[test]
705 fn test_tee_config_tdx() {
706 let tee = TeeConfig::Tdx {
707 workload_id: "tdx-workload".to_string(),
708 simulate: false,
709 };
710 let json = serde_json::to_string(&tee).unwrap();
711 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
712 match parsed {
713 TeeConfig::Tdx {
714 workload_id,
715 simulate,
716 } => {
717 assert_eq!(workload_id, "tdx-workload");
718 assert!(!simulate);
719 }
720 _ => panic!("Expected Tdx variant"),
721 }
722 }
723
724 #[test]
725 fn test_tee_config_tdx_simulate() {
726 let tee = TeeConfig::Tdx {
727 workload_id: "test".to_string(),
728 simulate: true,
729 };
730 let json = serde_json::to_string(&tee).unwrap();
731 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
732 match parsed {
733 TeeConfig::Tdx { simulate, .. } => assert!(simulate),
734 _ => panic!("Expected Tdx variant"),
735 }
736 }
737
738 #[test]
739 fn test_box_config_with_tee() {
740 let config = BoxConfig {
741 tee: TeeConfig::SevSnp {
742 workload_id: "secure-agent".to_string(),
743 generation: SevSnpGeneration::Milan,
744 simulate: false,
745 },
746 ..Default::default()
747 };
748
749 let json = serde_json::to_string(&config).unwrap();
750 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
751
752 match parsed.tee {
753 TeeConfig::SevSnp {
754 workload_id,
755 generation,
756 simulate,
757 } => {
758 assert_eq!(workload_id, "secure-agent");
759 assert_eq!(generation, SevSnpGeneration::Milan);
760 assert!(!simulate);
761 }
762 _ => panic!("Expected SevSnp TEE config"),
763 }
764 }
765
766 #[test]
767 fn test_box_config_default_has_no_tee() {
768 let config = BoxConfig::default();
769 assert_eq!(config.tee, TeeConfig::None);
770 }
771
772 #[test]
775 fn test_cache_config_default() {
776 let config = CacheConfig::default();
777 assert!(config.enabled);
778 assert!(config.cache_dir.is_none());
779 assert_eq!(config.max_rootfs_entries, 10);
780 assert_eq!(config.max_cache_bytes, 10 * 1024 * 1024 * 1024);
781 }
782
783 #[test]
784 fn test_cache_config_serialization() {
785 let config = CacheConfig {
786 enabled: false,
787 cache_dir: Some(PathBuf::from("/tmp/cache")),
788 max_rootfs_entries: 5,
789 max_cache_bytes: 1024 * 1024 * 1024,
790 };
791
792 let json = serde_json::to_string(&config).unwrap();
793 let parsed: CacheConfig = serde_json::from_str(&json).unwrap();
794
795 assert!(!parsed.enabled);
796 assert_eq!(parsed.cache_dir, Some(PathBuf::from("/tmp/cache")));
797 assert_eq!(parsed.max_rootfs_entries, 5);
798 assert_eq!(parsed.max_cache_bytes, 1024 * 1024 * 1024);
799 }
800
801 #[test]
802 fn test_cache_config_deserialization_defaults() {
803 let json = "{}";
804 let config: CacheConfig = serde_json::from_str(json).unwrap();
805
806 assert!(config.enabled);
807 assert!(config.cache_dir.is_none());
808 assert_eq!(config.max_rootfs_entries, 10);
809 assert_eq!(config.max_cache_bytes, 10 * 1024 * 1024 * 1024);
810 }
811
812 #[test]
815 fn test_pool_config_default() {
816 let config = PoolConfig::default();
817 assert!(!config.enabled);
818 assert_eq!(config.min_idle, 1);
819 assert_eq!(config.max_size, 5);
820 assert_eq!(config.idle_ttl_secs, 300);
821 }
822
823 #[test]
824 fn test_pool_config_serialization() {
825 let config = PoolConfig {
826 enabled: true,
827 min_idle: 3,
828 max_size: 10,
829 idle_ttl_secs: 600,
830 ..Default::default()
831 };
832
833 let json = serde_json::to_string(&config).unwrap();
834 let parsed: PoolConfig = serde_json::from_str(&json).unwrap();
835
836 assert!(parsed.enabled);
837 assert_eq!(parsed.min_idle, 3);
838 assert_eq!(parsed.max_size, 10);
839 assert_eq!(parsed.idle_ttl_secs, 600);
840 }
841
842 #[test]
843 fn test_pool_config_deserialization_defaults() {
844 let json = "{}";
845 let config: PoolConfig = serde_json::from_str(json).unwrap();
846
847 assert!(!config.enabled);
848 assert_eq!(config.min_idle, 1);
849 assert_eq!(config.max_size, 5);
850 assert_eq!(config.idle_ttl_secs, 300);
851 }
852
853 #[test]
856 fn test_box_config_default_has_cache_and_pool() {
857 let config = BoxConfig::default();
858 assert!(config.cache.enabled);
859 assert!(!config.pool.enabled);
860 }
861
862 #[test]
863 fn test_box_config_with_cache_serialization() {
864 let config = BoxConfig {
865 cache: CacheConfig {
866 enabled: false,
867 cache_dir: Some(PathBuf::from("/custom/cache")),
868 max_rootfs_entries: 20,
869 max_cache_bytes: 5 * 1024 * 1024 * 1024,
870 },
871 ..Default::default()
872 };
873
874 let json = serde_json::to_string(&config).unwrap();
875 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
876
877 assert!(!parsed.cache.enabled);
878 assert_eq!(parsed.cache.cache_dir, Some(PathBuf::from("/custom/cache")));
879 assert_eq!(parsed.cache.max_rootfs_entries, 20);
880 }
881
882 #[test]
883 fn test_box_config_with_pool_serialization() {
884 let config = BoxConfig {
885 pool: PoolConfig {
886 enabled: true,
887 min_idle: 2,
888 max_size: 8,
889 idle_ttl_secs: 120,
890 ..Default::default()
891 },
892 ..Default::default()
893 };
894
895 let json = serde_json::to_string(&config).unwrap();
896 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
897
898 assert!(parsed.pool.enabled);
899 assert_eq!(parsed.pool.min_idle, 2);
900 assert_eq!(parsed.pool.max_size, 8);
901 assert_eq!(parsed.pool.idle_ttl_secs, 120);
902 }
903
904 #[test]
905 fn test_box_config_backward_compatible_deserialization() {
906 let json = r#"{
908 "workspace": "/tmp/workspace",
909 "resources": {
910 "vcpus": 2,
911 "memory_mb": 1024,
912 "disk_mb": 4096,
913 "timeout": 3600
914 },
915 "log_level": "Info",
916 "debug_grpc": false
917 }"#;
918
919 let config: BoxConfig = serde_json::from_str(json).unwrap();
920 assert!(config.cache.enabled);
921 assert!(!config.pool.enabled);
922 }
923
924 #[test]
927 fn test_resource_limits_default() {
928 let limits = ResourceLimits::default();
929 assert!(limits.pids_limit.is_none());
930 assert!(limits.cpuset_cpus.is_none());
931 assert!(limits.ulimits.is_empty());
932 assert!(limits.cpu_shares.is_none());
933 assert!(limits.cpu_quota.is_none());
934 assert!(limits.cpu_period.is_none());
935 assert!(limits.memory_reservation.is_none());
936 assert!(limits.memory_swap.is_none());
937 }
938
939 #[test]
940 fn test_resource_limits_serialization() {
941 let limits = ResourceLimits {
942 pids_limit: Some(100),
943 cpuset_cpus: Some("0,1".to_string()),
944 ulimits: vec!["nofile=1024:4096".to_string()],
945 cpu_shares: Some(512),
946 cpu_quota: Some(50000),
947 cpu_period: Some(100000),
948 memory_reservation: Some(256 * 1024 * 1024),
949 memory_swap: Some(1024 * 1024 * 1024),
950 };
951
952 let json = serde_json::to_string(&limits).unwrap();
953 let parsed: ResourceLimits = serde_json::from_str(&json).unwrap();
954
955 assert_eq!(parsed.pids_limit, Some(100));
956 assert_eq!(parsed.cpuset_cpus, Some("0,1".to_string()));
957 assert_eq!(parsed.ulimits, vec!["nofile=1024:4096"]);
958 assert_eq!(parsed.cpu_shares, Some(512));
959 assert_eq!(parsed.cpu_quota, Some(50000));
960 assert_eq!(parsed.cpu_period, Some(100000));
961 assert_eq!(parsed.memory_reservation, Some(256 * 1024 * 1024));
962 assert_eq!(parsed.memory_swap, Some(1024 * 1024 * 1024));
963 }
964
965 #[test]
966 fn test_resource_limits_deserialization_defaults() {
967 let json = "{}";
968 let limits: ResourceLimits = serde_json::from_str(json).unwrap();
969 assert!(limits.pids_limit.is_none());
970 assert!(limits.ulimits.is_empty());
971 }
972
973 #[test]
974 fn test_resource_limits_memory_swap_unlimited() {
975 let limits = ResourceLimits {
976 memory_swap: Some(-1),
977 ..Default::default()
978 };
979
980 let json = serde_json::to_string(&limits).unwrap();
981 let parsed: ResourceLimits = serde_json::from_str(&json).unwrap();
982 assert_eq!(parsed.memory_swap, Some(-1));
983 }
984
985 #[test]
986 fn test_box_config_with_resource_limits() {
987 let config = BoxConfig {
988 resource_limits: ResourceLimits {
989 pids_limit: Some(256),
990 cpu_shares: Some(1024),
991 ..Default::default()
992 },
993 ..Default::default()
994 };
995
996 let json = serde_json::to_string(&config).unwrap();
997 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
998
999 assert_eq!(parsed.resource_limits.pids_limit, Some(256));
1000 assert_eq!(parsed.resource_limits.cpu_shares, Some(1024));
1001 }
1002
1003 #[test]
1004 fn test_box_config_backward_compat_no_resource_limits() {
1005 let json = r#"{
1007 "workspace": "/tmp/workspace",
1008 "resources": {
1009 "vcpus": 2,
1010 "memory_mb": 1024,
1011 "disk_mb": 4096,
1012 "timeout": 3600
1013 },
1014 "log_level": "Info",
1015 "debug_grpc": false
1016 }"#;
1017
1018 let config: BoxConfig = serde_json::from_str(json).unwrap();
1019 assert!(config.resource_limits.pids_limit.is_none());
1020 assert!(config.resource_limits.ulimits.is_empty());
1021 }
1022
1023 #[test]
1026 fn test_sidecar_config_default() {
1027 let s = SidecarConfig::default();
1028 assert!(s.image.is_empty());
1029 assert_eq!(s.vsock_port, 4092);
1030 assert!(s.env.is_empty());
1031 }
1032
1033 #[test]
1034 fn test_sidecar_config_roundtrip() {
1035 let s = SidecarConfig {
1036 image: "ghcr.io/a3s-lab/safeclaw:latest".to_string(),
1037 vsock_port: 4092,
1038 env: vec![
1039 ("LOG_LEVEL".to_string(), "debug".to_string()),
1040 ("MODE".to_string(), "proxy".to_string()),
1041 ],
1042 };
1043 let json = serde_json::to_string(&s).unwrap();
1044 let parsed: SidecarConfig = serde_json::from_str(&json).unwrap();
1045 assert_eq!(parsed.image, "ghcr.io/a3s-lab/safeclaw:latest");
1046 assert_eq!(parsed.vsock_port, 4092);
1047 assert_eq!(parsed.env.len(), 2);
1048 assert_eq!(
1049 parsed.env[0],
1050 ("LOG_LEVEL".to_string(), "debug".to_string())
1051 );
1052 }
1053
1054 #[test]
1055 fn test_sidecar_config_default_vsock_port_from_json() {
1056 let json = r#"{"image":"safeclaw:latest"}"#;
1057 let s: SidecarConfig = serde_json::from_str(json).unwrap();
1058 assert_eq!(s.vsock_port, 4092);
1059 assert!(s.env.is_empty());
1060 }
1061
1062 #[test]
1063 fn test_box_config_default_has_no_sidecar() {
1064 let config = BoxConfig::default();
1065 assert!(config.sidecar.is_none());
1066 }
1067
1068 #[test]
1069 fn test_box_config_with_sidecar_roundtrip() {
1070 let mut config = BoxConfig::default();
1071 config.sidecar = Some(SidecarConfig {
1072 image: "safeclaw:latest".to_string(),
1073 vsock_port: 4092,
1074 env: vec![],
1075 });
1076 let json = serde_json::to_string(&config).unwrap();
1077 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1078 let sidecar = parsed.sidecar.unwrap();
1079 assert_eq!(sidecar.image, "safeclaw:latest");
1080 assert_eq!(sidecar.vsock_port, 4092);
1081 }
1082
1083 #[test]
1084 fn test_box_config_without_sidecar_deserializes_as_none() {
1085 let config = BoxConfig::default();
1087 let json = serde_json::to_string(&config).unwrap();
1088 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1089 assert!(parsed.sidecar.is_none());
1090 }
1091}