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, Deserialize, Serialize)]
425pub struct ToolsConfig {
426 #[serde(default = "default_true")]
427 pub enabled: bool,
428 #[serde(default = "default_true")]
429 pub summarize_output: bool,
430 #[serde(default)]
431 pub shell: ShellConfig,
432 #[serde(default)]
433 pub scrape: ScrapeConfig,
434 #[serde(default)]
435 pub audit: AuditConfig,
436 #[serde(default)]
437 pub permissions: Option<PermissionsConfig>,
438 #[serde(default)]
439 pub filters: crate::filter::FilterConfig,
440 #[serde(default)]
441 pub overflow: OverflowConfig,
442 #[serde(default)]
443 pub anomaly: AnomalyConfig,
444 #[serde(default)]
445 pub result_cache: ResultCacheConfig,
446 #[serde(default)]
447 pub tafc: TafcConfig,
448 #[serde(default)]
449 pub dependencies: DependencyConfig,
450 #[serde(default)]
451 pub retry: RetryConfig,
452 #[cfg(feature = "policy-enforcer")]
454 #[serde(default)]
455 pub policy: PolicyConfig,
456 #[cfg(feature = "policy-enforcer")]
458 #[serde(default)]
459 pub adversarial_policy: AdversarialPolicyConfig,
460 #[serde(default)]
462 pub utility: UtilityScoringConfig,
463}
464
465impl ToolsConfig {
466 #[must_use]
468 pub fn permission_policy(&self, autonomy_level: AutonomyLevel) -> PermissionPolicy {
469 let policy = if let Some(ref perms) = self.permissions {
470 PermissionPolicy::from(perms.clone())
471 } else {
472 PermissionPolicy::from_legacy(
473 &self.shell.blocked_commands,
474 &self.shell.confirm_patterns,
475 )
476 };
477 policy.with_autonomy(autonomy_level)
478 }
479}
480
481#[derive(Debug, Deserialize, Serialize)]
483#[allow(clippy::struct_excessive_bools)]
484pub struct ShellConfig {
485 #[serde(default = "default_timeout")]
486 pub timeout: u64,
487 #[serde(default)]
488 pub blocked_commands: Vec<String>,
489 #[serde(default)]
490 pub allowed_commands: Vec<String>,
491 #[serde(default)]
492 pub allowed_paths: Vec<String>,
493 #[serde(default = "default_true")]
494 pub allow_network: bool,
495 #[serde(default = "default_confirm_patterns")]
496 pub confirm_patterns: Vec<String>,
497 #[serde(default = "ShellConfig::default_env_blocklist")]
501 pub env_blocklist: Vec<String>,
502 #[serde(default)]
504 pub transactional: bool,
505 #[serde(default)]
509 pub transaction_scope: Vec<String>,
510 #[serde(default)]
514 pub auto_rollback: bool,
515 #[serde(default)]
518 pub auto_rollback_exit_codes: Vec<i32>,
519 #[serde(default)]
522 pub snapshot_required: bool,
523 #[serde(default)]
525 pub max_snapshot_bytes: u64,
526}
527
528impl ShellConfig {
529 #[must_use]
530 pub fn default_env_blocklist() -> Vec<String> {
531 vec![
532 "ZEPH_".into(),
533 "AWS_".into(),
534 "AZURE_".into(),
535 "GCP_".into(),
536 "GOOGLE_".into(),
537 "OPENAI_".into(),
538 "ANTHROPIC_".into(),
539 "HF_".into(),
540 "HUGGING".into(),
541 ]
542 }
543}
544
545#[derive(Debug, Deserialize, Serialize)]
547pub struct AuditConfig {
548 #[serde(default = "default_true")]
549 pub enabled: bool,
550 #[serde(default = "default_audit_destination")]
551 pub destination: String,
552}
553
554impl Default for ToolsConfig {
555 fn default() -> Self {
556 Self {
557 enabled: true,
558 summarize_output: true,
559 shell: ShellConfig::default(),
560 scrape: ScrapeConfig::default(),
561 audit: AuditConfig::default(),
562 permissions: None,
563 filters: crate::filter::FilterConfig::default(),
564 overflow: OverflowConfig::default(),
565 anomaly: AnomalyConfig::default(),
566 result_cache: ResultCacheConfig::default(),
567 tafc: TafcConfig::default(),
568 dependencies: DependencyConfig::default(),
569 retry: RetryConfig::default(),
570 #[cfg(feature = "policy-enforcer")]
571 policy: PolicyConfig::default(),
572 #[cfg(feature = "policy-enforcer")]
573 adversarial_policy: AdversarialPolicyConfig::default(),
574 utility: UtilityScoringConfig::default(),
575 }
576 }
577}
578
579impl Default for ShellConfig {
580 fn default() -> Self {
581 Self {
582 timeout: default_timeout(),
583 blocked_commands: Vec::new(),
584 allowed_commands: Vec::new(),
585 allowed_paths: Vec::new(),
586 allow_network: true,
587 confirm_patterns: default_confirm_patterns(),
588 env_blocklist: Self::default_env_blocklist(),
589 transactional: false,
590 transaction_scope: Vec::new(),
591 auto_rollback: false,
592 auto_rollback_exit_codes: Vec::new(),
593 snapshot_required: false,
594 max_snapshot_bytes: 0,
595 }
596 }
597}
598
599impl Default for AuditConfig {
600 fn default() -> Self {
601 Self {
602 enabled: true,
603 destination: default_audit_destination(),
604 }
605 }
606}
607
608fn default_scrape_timeout() -> u64 {
609 15
610}
611
612fn default_max_body_bytes() -> usize {
613 4_194_304
614}
615
616#[derive(Debug, Deserialize, Serialize)]
618pub struct ScrapeConfig {
619 #[serde(default = "default_scrape_timeout")]
620 pub timeout: u64,
621 #[serde(default = "default_max_body_bytes")]
622 pub max_body_bytes: usize,
623}
624
625impl Default for ScrapeConfig {
626 fn default() -> Self {
627 Self {
628 timeout: default_scrape_timeout(),
629 max_body_bytes: default_max_body_bytes(),
630 }
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637
638 #[test]
639 fn deserialize_default_config() {
640 let toml_str = r#"
641 enabled = true
642
643 [shell]
644 timeout = 60
645 blocked_commands = ["rm -rf /", "sudo"]
646 "#;
647
648 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
649 assert!(config.enabled);
650 assert_eq!(config.shell.timeout, 60);
651 assert_eq!(config.shell.blocked_commands.len(), 2);
652 assert_eq!(config.shell.blocked_commands[0], "rm -rf /");
653 assert_eq!(config.shell.blocked_commands[1], "sudo");
654 }
655
656 #[test]
657 fn empty_blocked_commands() {
658 let toml_str = r"
659 [shell]
660 timeout = 30
661 ";
662
663 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
664 assert!(config.enabled);
665 assert_eq!(config.shell.timeout, 30);
666 assert!(config.shell.blocked_commands.is_empty());
667 }
668
669 #[test]
670 fn default_tools_config() {
671 let config = ToolsConfig::default();
672 assert!(config.enabled);
673 assert!(config.summarize_output);
674 assert_eq!(config.shell.timeout, 30);
675 assert!(config.shell.blocked_commands.is_empty());
676 assert!(config.audit.enabled);
677 }
678
679 #[test]
680 fn tools_summarize_output_default_true() {
681 let config = ToolsConfig::default();
682 assert!(config.summarize_output);
683 }
684
685 #[test]
686 fn tools_summarize_output_parsing() {
687 let toml_str = r"
688 summarize_output = true
689 ";
690 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
691 assert!(config.summarize_output);
692 }
693
694 #[test]
695 fn default_shell_config() {
696 let config = ShellConfig::default();
697 assert_eq!(config.timeout, 30);
698 assert!(config.blocked_commands.is_empty());
699 assert!(config.allowed_paths.is_empty());
700 assert!(config.allow_network);
701 assert!(!config.confirm_patterns.is_empty());
702 }
703
704 #[test]
705 fn deserialize_omitted_fields_use_defaults() {
706 let toml_str = "";
707 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
708 assert!(config.enabled);
709 assert_eq!(config.shell.timeout, 30);
710 assert!(config.shell.blocked_commands.is_empty());
711 assert!(config.shell.allow_network);
712 assert!(!config.shell.confirm_patterns.is_empty());
713 assert_eq!(config.scrape.timeout, 15);
714 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
715 assert!(config.audit.enabled);
716 assert_eq!(config.audit.destination, "stdout");
717 assert!(config.summarize_output);
718 }
719
720 #[test]
721 fn default_scrape_config() {
722 let config = ScrapeConfig::default();
723 assert_eq!(config.timeout, 15);
724 assert_eq!(config.max_body_bytes, 4_194_304);
725 }
726
727 #[test]
728 fn deserialize_scrape_config() {
729 let toml_str = r"
730 [scrape]
731 timeout = 30
732 max_body_bytes = 2097152
733 ";
734
735 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
736 assert_eq!(config.scrape.timeout, 30);
737 assert_eq!(config.scrape.max_body_bytes, 2_097_152);
738 }
739
740 #[test]
741 fn tools_config_default_includes_scrape() {
742 let config = ToolsConfig::default();
743 assert_eq!(config.scrape.timeout, 15);
744 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
745 }
746
747 #[test]
748 fn deserialize_allowed_commands() {
749 let toml_str = r#"
750 [shell]
751 timeout = 30
752 allowed_commands = ["curl", "wget"]
753 "#;
754
755 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
756 assert_eq!(config.shell.allowed_commands, vec!["curl", "wget"]);
757 }
758
759 #[test]
760 fn default_allowed_commands_empty() {
761 let config = ShellConfig::default();
762 assert!(config.allowed_commands.is_empty());
763 }
764
765 #[test]
766 fn deserialize_shell_security_fields() {
767 let toml_str = r#"
768 [shell]
769 allowed_paths = ["/tmp", "/home/user"]
770 allow_network = false
771 confirm_patterns = ["rm ", "drop table"]
772 "#;
773
774 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
775 assert_eq!(config.shell.allowed_paths, vec!["/tmp", "/home/user"]);
776 assert!(!config.shell.allow_network);
777 assert_eq!(config.shell.confirm_patterns, vec!["rm ", "drop table"]);
778 }
779
780 #[test]
781 fn deserialize_audit_config() {
782 let toml_str = r#"
783 [audit]
784 enabled = true
785 destination = "/var/log/zeph-audit.log"
786 "#;
787
788 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
789 assert!(config.audit.enabled);
790 assert_eq!(config.audit.destination, "/var/log/zeph-audit.log");
791 }
792
793 #[test]
794 fn default_audit_config() {
795 let config = AuditConfig::default();
796 assert!(config.enabled);
797 assert_eq!(config.destination, "stdout");
798 }
799
800 #[test]
801 fn permission_policy_from_legacy_fields() {
802 let config = ToolsConfig {
803 shell: ShellConfig {
804 blocked_commands: vec!["sudo".to_owned()],
805 confirm_patterns: vec!["rm ".to_owned()],
806 ..ShellConfig::default()
807 },
808 ..ToolsConfig::default()
809 };
810 let policy = config.permission_policy(AutonomyLevel::Supervised);
811 assert_eq!(
812 policy.check("bash", "sudo apt"),
813 crate::permissions::PermissionAction::Deny
814 );
815 assert_eq!(
816 policy.check("bash", "rm file"),
817 crate::permissions::PermissionAction::Ask
818 );
819 }
820
821 #[test]
822 fn permission_policy_from_explicit_config() {
823 let toml_str = r#"
824 [permissions]
825 [[permissions.bash]]
826 pattern = "*sudo*"
827 action = "deny"
828 "#;
829 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
830 let policy = config.permission_policy(AutonomyLevel::Supervised);
831 assert_eq!(
832 policy.check("bash", "sudo rm"),
833 crate::permissions::PermissionAction::Deny
834 );
835 }
836
837 #[test]
838 fn permission_policy_default_uses_legacy() {
839 let config = ToolsConfig::default();
840 assert!(config.permissions.is_none());
841 let policy = config.permission_policy(AutonomyLevel::Supervised);
842 assert!(!config.shell.confirm_patterns.is_empty());
844 assert!(policy.rules().contains_key("bash"));
845 }
846
847 #[test]
848 fn deserialize_overflow_config_full() {
849 let toml_str = r"
850 [overflow]
851 threshold = 100000
852 retention_days = 14
853 max_overflow_bytes = 5242880
854 ";
855 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
856 assert_eq!(config.overflow.threshold, 100_000);
857 assert_eq!(config.overflow.retention_days, 14);
858 assert_eq!(config.overflow.max_overflow_bytes, 5_242_880);
859 }
860
861 #[test]
862 fn deserialize_overflow_config_unknown_dir_field_is_ignored() {
863 let toml_str = r#"
865 [overflow]
866 threshold = 75000
867 dir = "/tmp/overflow"
868 "#;
869 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
870 assert_eq!(config.overflow.threshold, 75_000);
871 }
872
873 #[test]
874 fn deserialize_overflow_config_partial_uses_defaults() {
875 let toml_str = r"
876 [overflow]
877 threshold = 75000
878 ";
879 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
880 assert_eq!(config.overflow.threshold, 75_000);
881 assert_eq!(config.overflow.retention_days, 7);
882 }
883
884 #[test]
885 fn deserialize_overflow_config_omitted_uses_defaults() {
886 let config: ToolsConfig = toml::from_str("").unwrap();
887 assert_eq!(config.overflow.threshold, 50_000);
888 assert_eq!(config.overflow.retention_days, 7);
889 assert_eq!(config.overflow.max_overflow_bytes, 10 * 1024 * 1024);
890 }
891
892 #[test]
893 fn result_cache_config_defaults() {
894 let config = ResultCacheConfig::default();
895 assert!(config.enabled);
896 assert_eq!(config.ttl_secs, 300);
897 }
898
899 #[test]
900 fn deserialize_result_cache_config() {
901 let toml_str = r"
902 [result_cache]
903 enabled = false
904 ttl_secs = 60
905 ";
906 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
907 assert!(!config.result_cache.enabled);
908 assert_eq!(config.result_cache.ttl_secs, 60);
909 }
910
911 #[test]
912 fn result_cache_omitted_uses_defaults() {
913 let config: ToolsConfig = toml::from_str("").unwrap();
914 assert!(config.result_cache.enabled);
915 assert_eq!(config.result_cache.ttl_secs, 300);
916 }
917
918 #[test]
919 fn result_cache_ttl_zero_is_valid() {
920 let toml_str = r"
921 [result_cache]
922 ttl_secs = 0
923 ";
924 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
925 assert_eq!(config.result_cache.ttl_secs, 0);
926 }
927}