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