1use serde::{Deserialize, Serialize};
5
6use crate::permissions::{AutonomyLevel, PermissionPolicy, PermissionsConfig};
7use crate::policy::{PolicyConfig, PolicyRuleConfig};
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 #[serde(default)]
229 pub exempt_tools: Vec<String>,
230}
231
232impl Default for UtilityScoringConfig {
233 fn default() -> Self {
234 Self {
235 enabled: false,
236 threshold: default_utility_threshold(),
237 gain_weight: default_utility_gain_weight(),
238 cost_weight: default_utility_cost_weight(),
239 redundancy_weight: default_utility_redundancy_weight(),
240 uncertainty_bonus: default_utility_uncertainty_bonus(),
241 exempt_tools: Vec::new(),
242 }
243 }
244}
245
246impl UtilityScoringConfig {
247 pub fn validate(&self) -> Result<(), String> {
253 let fields = [
254 ("threshold", self.threshold),
255 ("gain_weight", self.gain_weight),
256 ("cost_weight", self.cost_weight),
257 ("redundancy_weight", self.redundancy_weight),
258 ("uncertainty_bonus", self.uncertainty_bonus),
259 ];
260 for (name, val) in fields {
261 if !val.is_finite() {
262 return Err(format!("[tools.utility] {name} must be finite, got {val}"));
263 }
264 if val < 0.0 {
265 return Err(format!("[tools.utility] {name} must be >= 0, got {val}"));
266 }
267 }
268 Ok(())
269 }
270}
271
272fn default_boost_per_dep() -> f32 {
273 0.15
274}
275
276fn default_max_total_boost() -> f32 {
277 0.2
278}
279
280#[derive(Debug, Clone, Default, Deserialize, Serialize)]
282pub struct ToolDependency {
283 #[serde(default, skip_serializing_if = "Vec::is_empty")]
285 pub requires: Vec<String>,
286 #[serde(default, skip_serializing_if = "Vec::is_empty")]
288 pub prefers: Vec<String>,
289}
290
291#[derive(Debug, Clone, Deserialize, Serialize)]
293pub struct DependencyConfig {
294 #[serde(default)]
296 pub enabled: bool,
297 #[serde(default = "default_boost_per_dep")]
299 pub boost_per_dep: f32,
300 #[serde(default = "default_max_total_boost")]
302 pub max_total_boost: f32,
303 #[serde(default)]
305 pub rules: std::collections::HashMap<String, ToolDependency>,
306}
307
308impl Default for DependencyConfig {
309 fn default() -> Self {
310 Self {
311 enabled: false,
312 boost_per_dep: default_boost_per_dep(),
313 max_total_boost: default_max_total_boost(),
314 rules: std::collections::HashMap::new(),
315 }
316 }
317}
318
319fn default_retry_max_attempts() -> usize {
320 2
321}
322
323fn default_retry_base_ms() -> u64 {
324 500
325}
326
327fn default_retry_max_ms() -> u64 {
328 5_000
329}
330
331fn default_retry_budget_secs() -> u64 {
332 30
333}
334
335#[derive(Debug, Clone, Deserialize, Serialize)]
337pub struct RetryConfig {
338 #[serde(default = "default_retry_max_attempts")]
340 pub max_attempts: usize,
341 #[serde(default = "default_retry_base_ms")]
343 pub base_ms: u64,
344 #[serde(default = "default_retry_max_ms")]
346 pub max_ms: u64,
347 #[serde(default = "default_retry_budget_secs")]
349 pub budget_secs: u64,
350 #[serde(default)]
353 pub parameter_reformat_provider: String,
354}
355
356impl Default for RetryConfig {
357 fn default() -> Self {
358 Self {
359 max_attempts: default_retry_max_attempts(),
360 base_ms: default_retry_base_ms(),
361 max_ms: default_retry_max_ms(),
362 budget_secs: default_retry_budget_secs(),
363 parameter_reformat_provider: String::new(),
364 }
365 }
366}
367
368#[derive(Debug, Clone, Deserialize, Serialize)]
370pub struct AdversarialPolicyConfig {
371 #[serde(default)]
373 pub enabled: bool,
374 #[serde(default)]
378 pub policy_provider: String,
379 pub policy_file: Option<String>,
381 #[serde(default)]
387 pub fail_open: bool,
388 #[serde(default = "default_adversarial_timeout_ms")]
390 pub timeout_ms: u64,
391 #[serde(default = "AdversarialPolicyConfig::default_exempt_tools")]
395 pub exempt_tools: Vec<String>,
396}
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}
409impl AdversarialPolicyConfig {
410 fn default_exempt_tools() -> Vec<String> {
411 vec![
412 "memory_save".into(),
413 "memory_search".into(),
414 "read_overflow".into(),
415 "load_skill".into(),
416 "schedule_deferred".into(),
417 ]
418 }
419}
420
421#[derive(Debug, Clone, Default, Deserialize, Serialize)]
428pub struct FileConfig {
429 #[serde(default)]
431 pub deny_read: Vec<String>,
432 #[serde(default)]
434 pub allow_read: Vec<String>,
435}
436
437#[derive(Debug, Deserialize, Serialize)]
439pub struct ToolsConfig {
440 #[serde(default = "default_true")]
441 pub enabled: bool,
442 #[serde(default = "default_true")]
443 pub summarize_output: bool,
444 #[serde(default)]
445 pub shell: ShellConfig,
446 #[serde(default)]
447 pub scrape: ScrapeConfig,
448 #[serde(default)]
449 pub audit: AuditConfig,
450 #[serde(default)]
451 pub permissions: Option<PermissionsConfig>,
452 #[serde(default)]
453 pub filters: crate::filter::FilterConfig,
454 #[serde(default)]
455 pub overflow: OverflowConfig,
456 #[serde(default)]
457 pub anomaly: AnomalyConfig,
458 #[serde(default)]
459 pub result_cache: ResultCacheConfig,
460 #[serde(default)]
461 pub tafc: TafcConfig,
462 #[serde(default)]
463 pub dependencies: DependencyConfig,
464 #[serde(default)]
465 pub retry: RetryConfig,
466 #[serde(default)]
468 pub policy: PolicyConfig,
469 #[serde(default)]
471 pub adversarial_policy: AdversarialPolicyConfig,
472 #[serde(default)]
474 pub utility: UtilityScoringConfig,
475 #[serde(default)]
477 pub file: FileConfig,
478 #[serde(default)]
483 pub authorization: AuthorizationConfig,
484 #[serde(default)]
487 pub max_tool_calls_per_session: Option<u32>,
488}
489
490impl ToolsConfig {
491 #[must_use]
493 pub fn permission_policy(&self, autonomy_level: AutonomyLevel) -> PermissionPolicy {
494 let policy = if let Some(ref perms) = self.permissions {
495 PermissionPolicy::from(perms.clone())
496 } else {
497 PermissionPolicy::from_legacy(
498 &self.shell.blocked_commands,
499 &self.shell.confirm_patterns,
500 )
501 };
502 policy.with_autonomy(autonomy_level)
503 }
504}
505
506#[derive(Debug, Deserialize, Serialize)]
508#[allow(clippy::struct_excessive_bools)]
509pub struct ShellConfig {
510 #[serde(default = "default_timeout")]
511 pub timeout: u64,
512 #[serde(default)]
513 pub blocked_commands: Vec<String>,
514 #[serde(default)]
515 pub allowed_commands: Vec<String>,
516 #[serde(default)]
517 pub allowed_paths: Vec<String>,
518 #[serde(default = "default_true")]
519 pub allow_network: bool,
520 #[serde(default = "default_confirm_patterns")]
521 pub confirm_patterns: Vec<String>,
522 #[serde(default = "ShellConfig::default_env_blocklist")]
526 pub env_blocklist: Vec<String>,
527 #[serde(default)]
529 pub transactional: bool,
530 #[serde(default)]
534 pub transaction_scope: Vec<String>,
535 #[serde(default)]
539 pub auto_rollback: bool,
540 #[serde(default)]
543 pub auto_rollback_exit_codes: Vec<i32>,
544 #[serde(default)]
547 pub snapshot_required: bool,
548 #[serde(default)]
550 pub max_snapshot_bytes: u64,
551}
552
553impl ShellConfig {
554 #[must_use]
555 pub fn default_env_blocklist() -> Vec<String> {
556 vec![
557 "ZEPH_".into(),
558 "AWS_".into(),
559 "AZURE_".into(),
560 "GCP_".into(),
561 "GOOGLE_".into(),
562 "OPENAI_".into(),
563 "ANTHROPIC_".into(),
564 "HF_".into(),
565 "HUGGING".into(),
566 ]
567 }
568}
569
570#[derive(Debug, Deserialize, Serialize)]
572pub struct AuditConfig {
573 #[serde(default = "default_true")]
574 pub enabled: bool,
575 #[serde(default = "default_audit_destination")]
576 pub destination: String,
577 #[serde(default)]
582 pub tool_risk_summary: bool,
583}
584
585impl Default for ToolsConfig {
586 fn default() -> Self {
587 Self {
588 enabled: true,
589 summarize_output: true,
590 shell: ShellConfig::default(),
591 scrape: ScrapeConfig::default(),
592 audit: AuditConfig::default(),
593 permissions: None,
594 filters: crate::filter::FilterConfig::default(),
595 overflow: OverflowConfig::default(),
596 anomaly: AnomalyConfig::default(),
597 result_cache: ResultCacheConfig::default(),
598 tafc: TafcConfig::default(),
599 dependencies: DependencyConfig::default(),
600 retry: RetryConfig::default(),
601 policy: PolicyConfig::default(),
602 adversarial_policy: AdversarialPolicyConfig::default(),
603 utility: UtilityScoringConfig::default(),
604 file: FileConfig::default(),
605 authorization: AuthorizationConfig::default(),
606 max_tool_calls_per_session: None,
607 }
608 }
609}
610
611impl Default for ShellConfig {
612 fn default() -> Self {
613 Self {
614 timeout: default_timeout(),
615 blocked_commands: Vec::new(),
616 allowed_commands: Vec::new(),
617 allowed_paths: Vec::new(),
618 allow_network: true,
619 confirm_patterns: default_confirm_patterns(),
620 env_blocklist: Self::default_env_blocklist(),
621 transactional: false,
622 transaction_scope: Vec::new(),
623 auto_rollback: false,
624 auto_rollback_exit_codes: Vec::new(),
625 snapshot_required: false,
626 max_snapshot_bytes: 0,
627 }
628 }
629}
630
631impl Default for AuditConfig {
632 fn default() -> Self {
633 Self {
634 enabled: true,
635 destination: default_audit_destination(),
636 tool_risk_summary: false,
637 }
638 }
639}
640
641#[derive(Debug, Clone, Default, Deserialize, Serialize)]
647pub struct AuthorizationConfig {
648 #[serde(default)]
650 pub enabled: bool,
651 #[serde(default)]
653 pub rules: Vec<PolicyRuleConfig>,
654}
655
656fn default_scrape_timeout() -> u64 {
657 15
658}
659
660fn default_max_body_bytes() -> usize {
661 4_194_304
662}
663
664#[derive(Debug, Deserialize, Serialize)]
666pub struct ScrapeConfig {
667 #[serde(default = "default_scrape_timeout")]
668 pub timeout: u64,
669 #[serde(default = "default_max_body_bytes")]
670 pub max_body_bytes: usize,
671 #[serde(default)]
680 pub allowed_domains: Vec<String>,
681 #[serde(default)]
684 pub denied_domains: Vec<String>,
685}
686
687impl Default for ScrapeConfig {
688 fn default() -> Self {
689 Self {
690 timeout: default_scrape_timeout(),
691 max_body_bytes: default_max_body_bytes(),
692 allowed_domains: Vec::new(),
693 denied_domains: Vec::new(),
694 }
695 }
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 #[test]
703 fn deserialize_default_config() {
704 let toml_str = r#"
705 enabled = true
706
707 [shell]
708 timeout = 60
709 blocked_commands = ["rm -rf /", "sudo"]
710 "#;
711
712 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
713 assert!(config.enabled);
714 assert_eq!(config.shell.timeout, 60);
715 assert_eq!(config.shell.blocked_commands.len(), 2);
716 assert_eq!(config.shell.blocked_commands[0], "rm -rf /");
717 assert_eq!(config.shell.blocked_commands[1], "sudo");
718 }
719
720 #[test]
721 fn empty_blocked_commands() {
722 let toml_str = r"
723 [shell]
724 timeout = 30
725 ";
726
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 }
732
733 #[test]
734 fn default_tools_config() {
735 let config = ToolsConfig::default();
736 assert!(config.enabled);
737 assert!(config.summarize_output);
738 assert_eq!(config.shell.timeout, 30);
739 assert!(config.shell.blocked_commands.is_empty());
740 assert!(config.audit.enabled);
741 }
742
743 #[test]
744 fn tools_summarize_output_default_true() {
745 let config = ToolsConfig::default();
746 assert!(config.summarize_output);
747 }
748
749 #[test]
750 fn tools_summarize_output_parsing() {
751 let toml_str = r"
752 summarize_output = true
753 ";
754 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
755 assert!(config.summarize_output);
756 }
757
758 #[test]
759 fn default_shell_config() {
760 let config = ShellConfig::default();
761 assert_eq!(config.timeout, 30);
762 assert!(config.blocked_commands.is_empty());
763 assert!(config.allowed_paths.is_empty());
764 assert!(config.allow_network);
765 assert!(!config.confirm_patterns.is_empty());
766 }
767
768 #[test]
769 fn deserialize_omitted_fields_use_defaults() {
770 let toml_str = "";
771 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
772 assert!(config.enabled);
773 assert_eq!(config.shell.timeout, 30);
774 assert!(config.shell.blocked_commands.is_empty());
775 assert!(config.shell.allow_network);
776 assert!(!config.shell.confirm_patterns.is_empty());
777 assert_eq!(config.scrape.timeout, 15);
778 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
779 assert!(config.audit.enabled);
780 assert_eq!(config.audit.destination, "stdout");
781 assert!(config.summarize_output);
782 }
783
784 #[test]
785 fn default_scrape_config() {
786 let config = ScrapeConfig::default();
787 assert_eq!(config.timeout, 15);
788 assert_eq!(config.max_body_bytes, 4_194_304);
789 }
790
791 #[test]
792 fn deserialize_scrape_config() {
793 let toml_str = r"
794 [scrape]
795 timeout = 30
796 max_body_bytes = 2097152
797 ";
798
799 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
800 assert_eq!(config.scrape.timeout, 30);
801 assert_eq!(config.scrape.max_body_bytes, 2_097_152);
802 }
803
804 #[test]
805 fn tools_config_default_includes_scrape() {
806 let config = ToolsConfig::default();
807 assert_eq!(config.scrape.timeout, 15);
808 assert_eq!(config.scrape.max_body_bytes, 4_194_304);
809 }
810
811 #[test]
812 fn deserialize_allowed_commands() {
813 let toml_str = r#"
814 [shell]
815 timeout = 30
816 allowed_commands = ["curl", "wget"]
817 "#;
818
819 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
820 assert_eq!(config.shell.allowed_commands, vec!["curl", "wget"]);
821 }
822
823 #[test]
824 fn default_allowed_commands_empty() {
825 let config = ShellConfig::default();
826 assert!(config.allowed_commands.is_empty());
827 }
828
829 #[test]
830 fn deserialize_shell_security_fields() {
831 let toml_str = r#"
832 [shell]
833 allowed_paths = ["/tmp", "/home/user"]
834 allow_network = false
835 confirm_patterns = ["rm ", "drop table"]
836 "#;
837
838 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
839 assert_eq!(config.shell.allowed_paths, vec!["/tmp", "/home/user"]);
840 assert!(!config.shell.allow_network);
841 assert_eq!(config.shell.confirm_patterns, vec!["rm ", "drop table"]);
842 }
843
844 #[test]
845 fn deserialize_audit_config() {
846 let toml_str = r#"
847 [audit]
848 enabled = true
849 destination = "/var/log/zeph-audit.log"
850 "#;
851
852 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
853 assert!(config.audit.enabled);
854 assert_eq!(config.audit.destination, "/var/log/zeph-audit.log");
855 }
856
857 #[test]
858 fn default_audit_config() {
859 let config = AuditConfig::default();
860 assert!(config.enabled);
861 assert_eq!(config.destination, "stdout");
862 }
863
864 #[test]
865 fn permission_policy_from_legacy_fields() {
866 let config = ToolsConfig {
867 shell: ShellConfig {
868 blocked_commands: vec!["sudo".to_owned()],
869 confirm_patterns: vec!["rm ".to_owned()],
870 ..ShellConfig::default()
871 },
872 ..ToolsConfig::default()
873 };
874 let policy = config.permission_policy(AutonomyLevel::Supervised);
875 assert_eq!(
876 policy.check("bash", "sudo apt"),
877 crate::permissions::PermissionAction::Deny
878 );
879 assert_eq!(
880 policy.check("bash", "rm file"),
881 crate::permissions::PermissionAction::Ask
882 );
883 }
884
885 #[test]
886 fn permission_policy_from_explicit_config() {
887 let toml_str = r#"
888 [permissions]
889 [[permissions.bash]]
890 pattern = "*sudo*"
891 action = "deny"
892 "#;
893 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
894 let policy = config.permission_policy(AutonomyLevel::Supervised);
895 assert_eq!(
896 policy.check("bash", "sudo rm"),
897 crate::permissions::PermissionAction::Deny
898 );
899 }
900
901 #[test]
902 fn permission_policy_default_uses_legacy() {
903 let config = ToolsConfig::default();
904 assert!(config.permissions.is_none());
905 let policy = config.permission_policy(AutonomyLevel::Supervised);
906 assert!(!config.shell.confirm_patterns.is_empty());
908 assert!(policy.rules().contains_key("bash"));
909 }
910
911 #[test]
912 fn deserialize_overflow_config_full() {
913 let toml_str = r"
914 [overflow]
915 threshold = 100000
916 retention_days = 14
917 max_overflow_bytes = 5242880
918 ";
919 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
920 assert_eq!(config.overflow.threshold, 100_000);
921 assert_eq!(config.overflow.retention_days, 14);
922 assert_eq!(config.overflow.max_overflow_bytes, 5_242_880);
923 }
924
925 #[test]
926 fn deserialize_overflow_config_unknown_dir_field_is_ignored() {
927 let toml_str = r#"
929 [overflow]
930 threshold = 75000
931 dir = "/tmp/overflow"
932 "#;
933 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
934 assert_eq!(config.overflow.threshold, 75_000);
935 }
936
937 #[test]
938 fn deserialize_overflow_config_partial_uses_defaults() {
939 let toml_str = r"
940 [overflow]
941 threshold = 75000
942 ";
943 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
944 assert_eq!(config.overflow.threshold, 75_000);
945 assert_eq!(config.overflow.retention_days, 7);
946 }
947
948 #[test]
949 fn deserialize_overflow_config_omitted_uses_defaults() {
950 let config: ToolsConfig = toml::from_str("").unwrap();
951 assert_eq!(config.overflow.threshold, 50_000);
952 assert_eq!(config.overflow.retention_days, 7);
953 assert_eq!(config.overflow.max_overflow_bytes, 10 * 1024 * 1024);
954 }
955
956 #[test]
957 fn result_cache_config_defaults() {
958 let config = ResultCacheConfig::default();
959 assert!(config.enabled);
960 assert_eq!(config.ttl_secs, 300);
961 }
962
963 #[test]
964 fn deserialize_result_cache_config() {
965 let toml_str = r"
966 [result_cache]
967 enabled = false
968 ttl_secs = 60
969 ";
970 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
971 assert!(!config.result_cache.enabled);
972 assert_eq!(config.result_cache.ttl_secs, 60);
973 }
974
975 #[test]
976 fn result_cache_omitted_uses_defaults() {
977 let config: ToolsConfig = toml::from_str("").unwrap();
978 assert!(config.result_cache.enabled);
979 assert_eq!(config.result_cache.ttl_secs, 300);
980 }
981
982 #[test]
983 fn result_cache_ttl_zero_is_valid() {
984 let toml_str = r"
985 [result_cache]
986 ttl_secs = 0
987 ";
988 let config: ToolsConfig = toml::from_str(toml_str).unwrap();
989 assert_eq!(config.result_cache.ttl_secs, 0);
990 }
991}