1use serde::{Deserialize, Serialize};
5
6use crate::permissions::{AutonomyLevel, PermissionPolicy, PermissionsConfig};
7#[cfg(feature = "policy-enforcer")]
8use crate::policy::PolicyConfig;
9
10fn default_true() -> bool {
11 true
12}
13
14#[cfg(feature = "policy-enforcer")]
15fn default_adversarial_timeout_ms() -> u64 {
16 3_000
17}
18
19fn default_timeout() -> u64 {
20 30
21}
22
23fn default_cache_ttl_secs() -> u64 {
24 300
25}
26
27fn default_confirm_patterns() -> Vec<String> {
28 vec![
29 "rm ".into(),
30 "git push -f".into(),
31 "git push --force".into(),
32 "drop table".into(),
33 "drop database".into(),
34 "truncate ".into(),
35 "$(".into(),
36 "`".into(),
37 "<(".into(),
38 ">(".into(),
39 "<<<".into(),
40 "eval ".into(),
41 ]
42}
43
44fn default_audit_destination() -> String {
45 "stdout".into()
46}
47
48fn default_overflow_threshold() -> usize {
49 50_000
50}
51
52fn default_retention_days() -> u64 {
53 7
54}
55
56fn default_max_overflow_bytes() -> usize {
57 10 * 1024 * 1024 }
59
60#[derive(Debug, Clone, Deserialize, Serialize)]
62pub struct OverflowConfig {
63 #[serde(default = "default_overflow_threshold")]
64 pub threshold: usize,
65 #[serde(default = "default_retention_days")]
66 pub retention_days: u64,
67 #[serde(default = "default_max_overflow_bytes")]
69 pub max_overflow_bytes: usize,
70}
71
72impl Default for OverflowConfig {
73 fn default() -> Self {
74 Self {
75 threshold: default_overflow_threshold(),
76 retention_days: default_retention_days(),
77 max_overflow_bytes: default_max_overflow_bytes(),
78 }
79 }
80}
81
82fn default_anomaly_window() -> usize {
83 10
84}
85
86fn default_anomaly_error_threshold() -> f64 {
87 0.5
88}
89
90fn default_anomaly_critical_threshold() -> f64 {
91 0.8
92}
93
94#[derive(Debug, Clone, Deserialize, Serialize)]
96pub struct AnomalyConfig {
97 #[serde(default = "default_true")]
98 pub enabled: bool,
99 #[serde(default = "default_anomaly_window")]
100 pub window_size: usize,
101 #[serde(default = "default_anomaly_error_threshold")]
102 pub error_threshold: f64,
103 #[serde(default = "default_anomaly_critical_threshold")]
104 pub critical_threshold: f64,
105 #[serde(default = "default_true")]
110 pub reasoning_model_warning: bool,
111}
112
113impl Default for AnomalyConfig {
114 fn default() -> Self {
115 Self {
116 enabled: true,
117 window_size: default_anomaly_window(),
118 error_threshold: default_anomaly_error_threshold(),
119 critical_threshold: default_anomaly_critical_threshold(),
120 reasoning_model_warning: true,
121 }
122 }
123}
124
125#[derive(Debug, Clone, Deserialize, Serialize)]
127pub struct ResultCacheConfig {
128 #[serde(default = "default_true")]
130 pub enabled: bool,
131 #[serde(default = "default_cache_ttl_secs")]
133 pub ttl_secs: u64,
134}
135
136impl Default for ResultCacheConfig {
137 fn default() -> Self {
138 Self {
139 enabled: true,
140 ttl_secs: default_cache_ttl_secs(),
141 }
142 }
143}
144
145fn default_tafc_complexity_threshold() -> f64 {
146 0.6
147}
148
149#[derive(Debug, Clone, Deserialize, Serialize)]
151pub struct TafcConfig {
152 #[serde(default)]
154 pub enabled: bool,
155 #[serde(default = "default_tafc_complexity_threshold")]
158 pub complexity_threshold: f64,
159}
160
161impl Default for TafcConfig {
162 fn default() -> Self {
163 Self {
164 enabled: false,
165 complexity_threshold: default_tafc_complexity_threshold(),
166 }
167 }
168}
169
170impl TafcConfig {
171 #[must_use]
173 pub fn validated(mut self) -> Self {
174 if self.complexity_threshold.is_finite() {
175 self.complexity_threshold = self.complexity_threshold.clamp(0.0, 1.0);
176 } else {
177 self.complexity_threshold = 0.6;
178 }
179 self
180 }
181}
182
183fn default_utility_threshold() -> f32 {
184 0.1
185}
186
187fn default_utility_gain_weight() -> f32 {
188 1.0
189}
190
191fn default_utility_cost_weight() -> f32 {
192 0.5
193}
194
195fn default_utility_redundancy_weight() -> f32 {
196 0.3
197}
198
199fn default_utility_uncertainty_bonus() -> f32 {
200 0.2
201}
202
203#[derive(Debug, Clone, Deserialize, Serialize)]
209#[serde(default)]
210pub struct UtilityScoringConfig {
211 pub enabled: bool,
213 #[serde(default = "default_utility_threshold")]
215 pub threshold: f32,
216 #[serde(default = "default_utility_gain_weight")]
218 pub gain_weight: f32,
219 #[serde(default = "default_utility_cost_weight")]
221 pub cost_weight: f32,
222 #[serde(default = "default_utility_redundancy_weight")]
224 pub redundancy_weight: f32,
225 #[serde(default = "default_utility_uncertainty_bonus")]
227 pub uncertainty_bonus: f32,
228}
229
230impl Default for UtilityScoringConfig {
231 fn default() -> Self {
232 Self {
233 enabled: false,
234 threshold: default_utility_threshold(),
235 gain_weight: default_utility_gain_weight(),
236 cost_weight: default_utility_cost_weight(),
237 redundancy_weight: default_utility_redundancy_weight(),
238 uncertainty_bonus: default_utility_uncertainty_bonus(),
239 }
240 }
241}
242
243impl UtilityScoringConfig {
244 pub fn validate(&self) -> Result<(), String> {
250 let fields = [
251 ("threshold", self.threshold),
252 ("gain_weight", self.gain_weight),
253 ("cost_weight", self.cost_weight),
254 ("redundancy_weight", self.redundancy_weight),
255 ("uncertainty_bonus", self.uncertainty_bonus),
256 ];
257 for (name, val) in fields {
258 if !val.is_finite() {
259 return Err(format!("[tools.utility] {name} must be finite, got {val}"));
260 }
261 if val < 0.0 {
262 return Err(format!("[tools.utility] {name} must be >= 0, got {val}"));
263 }
264 }
265 Ok(())
266 }
267}
268
269fn default_boost_per_dep() -> f32 {
270 0.15
271}
272
273fn default_max_total_boost() -> f32 {
274 0.2
275}
276
277#[derive(Debug, Clone, Default, Deserialize, Serialize)]
279pub struct ToolDependency {
280 #[serde(default, skip_serializing_if = "Vec::is_empty")]
282 pub requires: Vec<String>,
283 #[serde(default, skip_serializing_if = "Vec::is_empty")]
285 pub prefers: Vec<String>,
286}
287
288#[derive(Debug, Clone, Deserialize, Serialize)]
290pub struct DependencyConfig {
291 #[serde(default)]
293 pub enabled: bool,
294 #[serde(default = "default_boost_per_dep")]
296 pub boost_per_dep: f32,
297 #[serde(default = "default_max_total_boost")]
299 pub max_total_boost: f32,
300 #[serde(default)]
302 pub rules: std::collections::HashMap<String, ToolDependency>,
303}
304
305impl Default for DependencyConfig {
306 fn default() -> Self {
307 Self {
308 enabled: false,
309 boost_per_dep: default_boost_per_dep(),
310 max_total_boost: default_max_total_boost(),
311 rules: std::collections::HashMap::new(),
312 }
313 }
314}
315
316fn default_retry_max_attempts() -> usize {
317 2
318}
319
320fn default_retry_base_ms() -> u64 {
321 500
322}
323
324fn default_retry_max_ms() -> u64 {
325 5_000
326}
327
328fn default_retry_budget_secs() -> u64 {
329 30
330}
331
332#[derive(Debug, Clone, Deserialize, Serialize)]
334pub struct RetryConfig {
335 #[serde(default = "default_retry_max_attempts")]
337 pub max_attempts: usize,
338 #[serde(default = "default_retry_base_ms")]
340 pub base_ms: u64,
341 #[serde(default = "default_retry_max_ms")]
343 pub max_ms: u64,
344 #[serde(default = "default_retry_budget_secs")]
346 pub budget_secs: u64,
347 #[serde(default)]
350 pub parameter_reformat_provider: String,
351}
352
353impl Default for RetryConfig {
354 fn default() -> Self {
355 Self {
356 max_attempts: default_retry_max_attempts(),
357 base_ms: default_retry_base_ms(),
358 max_ms: default_retry_max_ms(),
359 budget_secs: default_retry_budget_secs(),
360 parameter_reformat_provider: String::new(),
361 }
362 }
363}
364
365#[cfg(feature = "policy-enforcer")]
367#[derive(Debug, Clone, Deserialize, Serialize)]
368pub struct AdversarialPolicyConfig {
369 #[serde(default)]
371 pub enabled: bool,
372 #[serde(default)]
376 pub policy_provider: String,
377 pub policy_file: Option<String>,
379 #[serde(default)]
385 pub fail_open: bool,
386 #[serde(default = "default_adversarial_timeout_ms")]
388 pub timeout_ms: u64,
389 #[serde(default = "AdversarialPolicyConfig::default_exempt_tools")]
393 pub exempt_tools: Vec<String>,
394}
395
396#[cfg(feature = "policy-enforcer")]
397impl Default for AdversarialPolicyConfig {
398 fn default() -> Self {
399 Self {
400 enabled: false,
401 policy_provider: String::new(),
402 policy_file: None,
403 fail_open: false,
404 timeout_ms: default_adversarial_timeout_ms(),
405 exempt_tools: Self::default_exempt_tools(),
406 }
407 }
408}
409
410#[cfg(feature = "policy-enforcer")]
411impl AdversarialPolicyConfig {
412 fn default_exempt_tools() -> Vec<String> {
413 vec![
414 "memory_save".into(),
415 "memory_search".into(),
416 "read_overflow".into(),
417 "load_skill".into(),
418 "schedule_deferred".into(),
419 ]
420 }
421}
422
423#[derive(Debug, Clone, Default, Deserialize, Serialize)]
430pub struct FileConfig {
431 #[serde(default)]
433 pub deny_read: Vec<String>,
434 #[serde(default)]
436 pub allow_read: Vec<String>,
437}
438
439#[derive(Debug, Deserialize, Serialize)]
441pub struct ToolsConfig {
442 #[serde(default = "default_true")]
443 pub enabled: bool,
444 #[serde(default = "default_true")]
445 pub summarize_output: bool,
446 #[serde(default)]
447 pub shell: ShellConfig,
448 #[serde(default)]
449 pub scrape: ScrapeConfig,
450 #[serde(default)]
451 pub audit: AuditConfig,
452 #[serde(default)]
453 pub permissions: Option<PermissionsConfig>,
454 #[serde(default)]
455 pub filters: crate::filter::FilterConfig,
456 #[serde(default)]
457 pub overflow: OverflowConfig,
458 #[serde(default)]
459 pub anomaly: AnomalyConfig,
460 #[serde(default)]
461 pub result_cache: ResultCacheConfig,
462 #[serde(default)]
463 pub tafc: TafcConfig,
464 #[serde(default)]
465 pub dependencies: DependencyConfig,
466 #[serde(default)]
467 pub retry: RetryConfig,
468 #[cfg(feature = "policy-enforcer")]
470 #[serde(default)]
471 pub policy: PolicyConfig,
472 #[cfg(feature = "policy-enforcer")]
474 #[serde(default)]
475 pub adversarial_policy: AdversarialPolicyConfig,
476 #[serde(default)]
478 pub utility: UtilityScoringConfig,
479 #[serde(default)]
481 pub file: FileConfig,
482}
483
484impl ToolsConfig {
485 #[must_use]
487 pub fn permission_policy(&self, autonomy_level: AutonomyLevel) -> PermissionPolicy {
488 let policy = if let Some(ref perms) = self.permissions {
489 PermissionPolicy::from(perms.clone())
490 } else {
491 PermissionPolicy::from_legacy(
492 &self.shell.blocked_commands,
493 &self.shell.confirm_patterns,
494 )
495 };
496 policy.with_autonomy(autonomy_level)
497 }
498}
499
500#[derive(Debug, Deserialize, Serialize)]
502#[allow(clippy::struct_excessive_bools)]
503pub struct ShellConfig {
504 #[serde(default = "default_timeout")]
505 pub timeout: u64,
506 #[serde(default)]
507 pub blocked_commands: Vec<String>,
508 #[serde(default)]
509 pub allowed_commands: Vec<String>,
510 #[serde(default)]
511 pub allowed_paths: Vec<String>,
512 #[serde(default = "default_true")]
513 pub allow_network: bool,
514 #[serde(default = "default_confirm_patterns")]
515 pub confirm_patterns: Vec<String>,
516 #[serde(default = "ShellConfig::default_env_blocklist")]
520 pub env_blocklist: Vec<String>,
521 #[serde(default)]
523 pub transactional: bool,
524 #[serde(default)]
528 pub transaction_scope: Vec<String>,
529 #[serde(default)]
533 pub auto_rollback: bool,
534 #[serde(default)]
537 pub auto_rollback_exit_codes: Vec<i32>,
538 #[serde(default)]
541 pub snapshot_required: bool,
542 #[serde(default)]
544 pub max_snapshot_bytes: u64,
545}
546
547impl ShellConfig {
548 #[must_use]
549 pub fn default_env_blocklist() -> Vec<String> {
550 vec![
551 "ZEPH_".into(),
552 "AWS_".into(),
553 "AZURE_".into(),
554 "GCP_".into(),
555 "GOOGLE_".into(),
556 "OPENAI_".into(),
557 "ANTHROPIC_".into(),
558 "HF_".into(),
559 "HUGGING".into(),
560 ]
561 }
562}
563
564#[derive(Debug, Deserialize, Serialize)]
566pub struct AuditConfig {
567 #[serde(default = "default_true")]
568 pub enabled: bool,
569 #[serde(default = "default_audit_destination")]
570 pub destination: String,
571}
572
573impl Default for ToolsConfig {
574 fn default() -> Self {
575 Self {
576 enabled: true,
577 summarize_output: true,
578 shell: ShellConfig::default(),
579 scrape: ScrapeConfig::default(),
580 audit: AuditConfig::default(),
581 permissions: None,
582 filters: crate::filter::FilterConfig::default(),
583 overflow: OverflowConfig::default(),
584 anomaly: AnomalyConfig::default(),
585 result_cache: ResultCacheConfig::default(),
586 tafc: TafcConfig::default(),
587 dependencies: DependencyConfig::default(),
588 retry: RetryConfig::default(),
589 #[cfg(feature = "policy-enforcer")]
590 policy: PolicyConfig::default(),
591 #[cfg(feature = "policy-enforcer")]
592 adversarial_policy: AdversarialPolicyConfig::default(),
593 utility: UtilityScoringConfig::default(),
594 file: FileConfig::default(),
595 }
596 }
597}
598
599impl Default for ShellConfig {
600 fn default() -> Self {
601 Self {
602 timeout: default_timeout(),
603 blocked_commands: Vec::new(),
604 allowed_commands: Vec::new(),
605 allowed_paths: Vec::new(),
606 allow_network: true,
607 confirm_patterns: default_confirm_patterns(),
608 env_blocklist: Self::default_env_blocklist(),
609 transactional: false,
610 transaction_scope: Vec::new(),
611 auto_rollback: false,
612 auto_rollback_exit_codes: Vec::new(),
613 snapshot_required: false,
614 max_snapshot_bytes: 0,
615 }
616 }
617}
618
619impl Default for AuditConfig {
620 fn default() -> Self {
621 Self {
622 enabled: true,
623 destination: default_audit_destination(),
624 }
625 }
626}
627
628fn default_scrape_timeout() -> u64 {
629 15
630}
631
632fn default_max_body_bytes() -> usize {
633 4_194_304
634}
635
636#[derive(Debug, Deserialize, Serialize)]
638pub struct ScrapeConfig {
639 #[serde(default = "default_scrape_timeout")]
640 pub timeout: u64,
641 #[serde(default = "default_max_body_bytes")]
642 pub max_body_bytes: usize,
643}
644
645impl Default for ScrapeConfig {
646 fn default() -> Self {
647 Self {
648 timeout: default_scrape_timeout(),
649 max_body_bytes: default_max_body_bytes(),
650 }
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 #[test]
659 fn deserialize_default_config() {
660 let toml_str = r#"
661 enabled = true
662
663 [shell]
664 timeout = 60
665 blocked_commands = ["rm -rf /", "sudo"]
666 "#;
667
668 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
669 assert!(config.enabled);
670 assert_eq!(config.shell.timeout, 60);
671 assert_eq!(config.shell.blocked_commands.len(), 2);
672 assert_eq!(config.shell.blocked_commands[0], "rm -rf /");
673 assert_eq!(config.shell.blocked_commands[1], "sudo");
674 }
675
676 #[test]
677 fn empty_blocked_commands() {
678 let toml_str = r"
679 [shell]
680 timeout = 30
681 ";
682
683 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
684 assert!(config.enabled);
685 assert_eq!(config.shell.timeout, 30);
686 assert!(config.shell.blocked_commands.is_empty());
687 }
688
689 #[test]
690 fn default_tools_config() {
691 let config = ToolsConfig::default();
692 assert!(config.enabled);
693 assert!(config.summarize_output);
694 assert_eq!(config.shell.timeout, 30);
695 assert!(config.shell.blocked_commands.is_empty());
696 assert!(config.audit.enabled);
697 }
698
699 #[test]
700 fn tools_summarize_output_default_true() {
701 let config = ToolsConfig::default();
702 assert!(config.summarize_output);
703 }
704
705 #[test]
706 fn tools_summarize_output_parsing() {
707 let toml_str = r"
708 summarize_output = true
709 ";
710 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
711 assert!(config.summarize_output);
712 }
713
714 #[test]
715 fn default_shell_config() {
716 let config = ShellConfig::default();
717 assert_eq!(config.timeout, 30);
718 assert!(config.blocked_commands.is_empty());
719 assert!(config.allowed_paths.is_empty());
720 assert!(config.allow_network);
721 assert!(!config.confirm_patterns.is_empty());
722 }
723
724 #[test]
725 fn deserialize_omitted_fields_use_defaults() {
726 let toml_str = "";
727 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
728 assert!(config.enabled);
729 assert_eq!(config.shell.timeout, 30);
730 assert!(config.shell.blocked_commands.is_empty());
731 assert!(config.shell.allow_network);
732 assert!(!config.shell.confirm_patterns.is_empty());
733 assert_eq!(config.scrape.timeout, 15);
734 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
735 assert!(config.audit.enabled);
736 assert_eq!(config.audit.destination, "stdout");
737 assert!(config.summarize_output);
738 }
739
740 #[test]
741 fn default_scrape_config() {
742 let config = ScrapeConfig::default();
743 assert_eq!(config.timeout, 15);
744 assert_eq!(config.max_body_bytes, 4_194_304);
745 }
746
747 #[test]
748 fn deserialize_scrape_config() {
749 let toml_str = r"
750 [scrape]
751 timeout = 30
752 max_body_bytes = 2097152
753 ";
754
755 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
756 assert_eq!(config.scrape.timeout, 30);
757 assert_eq!(config.scrape.max_body_bytes, 2_097_152);
758 }
759
760 #[test]
761 fn tools_config_default_includes_scrape() {
762 let config = ToolsConfig::default();
763 assert_eq!(config.scrape.timeout, 15);
764 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
765 }
766
767 #[test]
768 fn deserialize_allowed_commands() {
769 let toml_str = r#"
770 [shell]
771 timeout = 30
772 allowed_commands = ["curl", "wget"]
773 "#;
774
775 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
776 assert_eq!(config.shell.allowed_commands, vec!["curl", "wget"]);
777 }
778
779 #[test]
780 fn default_allowed_commands_empty() {
781 let config = ShellConfig::default();
782 assert!(config.allowed_commands.is_empty());
783 }
784
785 #[test]
786 fn deserialize_shell_security_fields() {
787 let toml_str = r#"
788 [shell]
789 allowed_paths = ["/tmp", "/home/user"]
790 allow_network = false
791 confirm_patterns = ["rm ", "drop table"]
792 "#;
793
794 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
795 assert_eq!(config.shell.allowed_paths, vec!["/tmp", "/home/user"]);
796 assert!(!config.shell.allow_network);
797 assert_eq!(config.shell.confirm_patterns, vec!["rm ", "drop table"]);
798 }
799
800 #[test]
801 fn deserialize_audit_config() {
802 let toml_str = r#"
803 [audit]
804 enabled = true
805 destination = "/var/log/zeph-audit.log"
806 "#;
807
808 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
809 assert!(config.audit.enabled);
810 assert_eq!(config.audit.destination, "/var/log/zeph-audit.log");
811 }
812
813 #[test]
814 fn default_audit_config() {
815 let config = AuditConfig::default();
816 assert!(config.enabled);
817 assert_eq!(config.destination, "stdout");
818 }
819
820 #[test]
821 fn permission_policy_from_legacy_fields() {
822 let config = ToolsConfig {
823 shell: ShellConfig {
824 blocked_commands: vec!["sudo".to_owned()],
825 confirm_patterns: vec!["rm ".to_owned()],
826 ..ShellConfig::default()
827 },
828 ..ToolsConfig::default()
829 };
830 let policy = config.permission_policy(AutonomyLevel::Supervised);
831 assert_eq!(
832 policy.check("bash", "sudo apt"),
833 crate::permissions::PermissionAction::Deny
834 );
835 assert_eq!(
836 policy.check("bash", "rm file"),
837 crate::permissions::PermissionAction::Ask
838 );
839 }
840
841 #[test]
842 fn permission_policy_from_explicit_config() {
843 let toml_str = r#"
844 [permissions]
845 [[permissions.bash]]
846 pattern = "*sudo*"
847 action = "deny"
848 "#;
849 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
850 let policy = config.permission_policy(AutonomyLevel::Supervised);
851 assert_eq!(
852 policy.check("bash", "sudo rm"),
853 crate::permissions::PermissionAction::Deny
854 );
855 }
856
857 #[test]
858 fn permission_policy_default_uses_legacy() {
859 let config = ToolsConfig::default();
860 assert!(config.permissions.is_none());
861 let policy = config.permission_policy(AutonomyLevel::Supervised);
862 assert!(!config.shell.confirm_patterns.is_empty());
864 assert!(policy.rules().contains_key("bash"));
865 }
866
867 #[test]
868 fn deserialize_overflow_config_full() {
869 let toml_str = r"
870 [overflow]
871 threshold = 100000
872 retention_days = 14
873 max_overflow_bytes = 5242880
874 ";
875 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
876 assert_eq!(config.overflow.threshold, 100_000);
877 assert_eq!(config.overflow.retention_days, 14);
878 assert_eq!(config.overflow.max_overflow_bytes, 5_242_880);
879 }
880
881 #[test]
882 fn deserialize_overflow_config_unknown_dir_field_is_ignored() {
883 let toml_str = r#"
885 [overflow]
886 threshold = 75000
887 dir = "/tmp/overflow"
888 "#;
889 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
890 assert_eq!(config.overflow.threshold, 75_000);
891 }
892
893 #[test]
894 fn deserialize_overflow_config_partial_uses_defaults() {
895 let toml_str = r"
896 [overflow]
897 threshold = 75000
898 ";
899 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
900 assert_eq!(config.overflow.threshold, 75_000);
901 assert_eq!(config.overflow.retention_days, 7);
902 }
903
904 #[test]
905 fn deserialize_overflow_config_omitted_uses_defaults() {
906 let config: ToolsConfig = toml::from_str("").unwrap();
907 assert_eq!(config.overflow.threshold, 50_000);
908 assert_eq!(config.overflow.retention_days, 7);
909 assert_eq!(config.overflow.max_overflow_bytes, 10 * 1024 * 1024);
910 }
911
912 #[test]
913 fn result_cache_config_defaults() {
914 let config = ResultCacheConfig::default();
915 assert!(config.enabled);
916 assert_eq!(config.ttl_secs, 300);
917 }
918
919 #[test]
920 fn deserialize_result_cache_config() {
921 let toml_str = r"
922 [result_cache]
923 enabled = false
924 ttl_secs = 60
925 ";
926 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
927 assert!(!config.result_cache.enabled);
928 assert_eq!(config.result_cache.ttl_secs, 60);
929 }
930
931 #[test]
932 fn result_cache_omitted_uses_defaults() {
933 let config: ToolsConfig = toml::from_str("").unwrap();
934 assert!(config.result_cache.enabled);
935 assert_eq!(config.result_cache.ttl_secs, 300);
936 }
937
938 #[test]
939 fn result_cache_ttl_zero_is_valid() {
940 let toml_str = r"
941 [result_cache]
942 ttl_secs = 0
943 ";
944 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
945 assert_eq!(config.result_cache.ttl_secs, 0);
946 }
947}