1use std::path::Path;
30
31use serde::{Deserialize, Serialize};
32use tracing::{error, info, warn};
33
34use crate::auto_reply::group::GroupActivation;
35use crate::auto_reply::registry::AutoReplyTrigger;
36
37#[derive(Debug, thiserror::Error)]
39pub enum ConfigError {
40 #[error("IO error: {0}")]
42 Io(#[from] std::io::Error),
43 #[error("JSON parse error: {0}")]
45 Json(#[from] serde_json::Error),
46}
47
48pub type ConfigResult<T> = Result<T, ConfigError>;
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub struct AutoReplyConfig {
64 #[serde(default = "default_true")]
66 pub enabled: bool,
67 #[serde(default)]
69 pub triggers: Vec<AutoReplyTrigger>,
70 #[serde(default)]
72 pub whitelist: Vec<String>,
73 #[serde(default = "default_cooldown")]
75 pub default_cooldown_seconds: u64,
76 #[serde(default)]
78 pub group_activations: Vec<GroupActivation>,
79}
80
81fn default_true() -> bool {
82 true
83}
84
85fn default_cooldown() -> u64 {
86 60
87}
88
89impl Default for AutoReplyConfig {
90 fn default() -> Self {
101 Self {
102 enabled: true,
103 triggers: Vec::new(),
104 whitelist: Vec::new(),
105 default_cooldown_seconds: 60,
106 group_activations: Vec::new(),
107 }
108 }
109}
110
111impl AutoReplyConfig {
112 pub fn load(path: &Path) -> Self {
139 match Self::load_from_file(path) {
140 Ok(config) => {
141 info!("Loaded auto-reply config from {:?}", path);
142 config
143 }
144 Err(ConfigError::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound => {
145 info!(
147 "Auto-reply config file not found at {:?}, using defaults",
148 path
149 );
150 Self::default()
151 }
152 Err(e) => {
153 error!(
155 "Failed to load auto-reply config from {:?}: {}, using defaults",
156 path, e
157 );
158 Self::default()
159 }
160 }
161 }
162
163 fn load_from_file(path: &Path) -> ConfigResult<Self> {
175 let content = std::fs::read_to_string(path)?;
176 let config: Self = serde_json::from_str(&content)?;
177 Ok(config)
178 }
179
180 pub fn save(&self, path: &Path) -> ConfigResult<()> {
205 if let Some(parent) = path.parent() {
207 if !parent.exists() {
208 std::fs::create_dir_all(parent)?;
209 }
210 }
211
212 let content = serde_json::to_string_pretty(self)?;
213 std::fs::write(path, content)?;
214 info!("Saved auto-reply config to {:?}", path);
215 Ok(())
216 }
217
218 pub fn reload(path: &Path) -> ConfigResult<Self> {
251 let config = Self::load_from_file(path)?;
252 info!("Reloaded auto-reply config from {:?}", path);
253 Ok(config)
254 }
255
256 pub fn validate(&self) -> Vec<String> {
265 let mut warnings = Vec::new();
266
267 let mut seen_ids = std::collections::HashSet::new();
269 for trigger in &self.triggers {
270 if !seen_ids.insert(&trigger.id) {
271 warnings.push(format!("Duplicate trigger ID: {}", trigger.id));
272 }
273 }
274
275 let mut seen_group_ids = std::collections::HashSet::new();
277 for group in &self.group_activations {
278 if !seen_group_ids.insert(&group.group_id) {
279 warnings.push(format!("Duplicate group ID: {}", group.group_id));
280 }
281 }
282
283 for warning in &warnings {
285 warn!("Config validation warning: {}", warning);
286 }
287
288 warnings
289 }
290
291 pub fn merge(&mut self, other: Self) {
300 self.enabled = other.enabled;
301 self.triggers.extend(other.triggers);
302 self.whitelist.extend(other.whitelist);
303 self.default_cooldown_seconds = other.default_cooldown_seconds;
304 self.group_activations.extend(other.group_activations);
305 }
306
307 pub fn is_default(&self) -> bool {
309 *self == Self::default()
310 }
311
312 pub fn enabled_trigger_count(&self) -> usize {
314 self.triggers.iter().filter(|t| t.enabled).count()
315 }
316
317 pub fn group_count(&self) -> usize {
319 self.group_activations.len()
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::auto_reply::types::{
327 KeywordTriggerConfig, ScheduleTriggerConfig, ScheduleType, TriggerConfig, TriggerType,
328 WebhookTriggerConfig,
329 };
330 use proptest::prelude::*;
331 use tempfile::TempDir;
332
333 fn create_test_trigger(id: &str) -> AutoReplyTrigger {
335 AutoReplyTrigger {
336 id: id.to_string(),
337 name: format!("Test Trigger {}", id),
338 enabled: true,
339 trigger_type: TriggerType::Keyword,
340 config: TriggerConfig::Keyword(KeywordTriggerConfig {
341 patterns: vec!["test".to_string()],
342 case_insensitive: false,
343 use_regex: false,
344 }),
345 priority: 100,
346 response_template: None,
347 }
348 }
349
350 fn arb_identifier() -> impl Strategy<Value = String> {
358 "[a-zA-Z][a-zA-Z0-9_-]{0,19}".prop_map(|s| s)
359 }
360
361 fn arb_user_id() -> impl Strategy<Value = String> {
363 "[a-zA-Z0-9_-]{1,20}".prop_map(|s| s)
364 }
365
366 fn arb_keyword_pattern() -> impl Strategy<Value = String> {
368 "[a-zA-Z0-9_\\-\\s]{1,30}".prop_map(|s| s)
369 }
370
371 fn arb_trigger_type() -> impl Strategy<Value = TriggerType> {
373 prop_oneof![
374 Just(TriggerType::Mention),
375 Just(TriggerType::Keyword),
376 Just(TriggerType::DirectMessage),
377 Just(TriggerType::Schedule),
378 Just(TriggerType::Webhook),
379 ]
380 }
381
382 fn arb_keyword_config() -> impl Strategy<Value = KeywordTriggerConfig> {
384 (
385 prop::collection::vec(arb_keyword_pattern(), 1..5),
386 any::<bool>(),
387 Just(false),
389 )
390 .prop_map(
391 |(patterns, case_insensitive, use_regex)| KeywordTriggerConfig {
392 patterns,
393 case_insensitive,
394 use_regex,
395 },
396 )
397 }
398
399 fn arb_schedule_type() -> impl Strategy<Value = ScheduleType> {
401 prop_oneof![
402 (
404 Just("0 * * * *".to_string()),
405 proptest::option::of("[A-Za-z/_]{1,20}")
406 )
407 .prop_map(|(expr, timezone)| ScheduleType::Cron { expr, timezone }),
408 (0i64..=i64::MAX).prop_map(|at_ms| ScheduleType::At { at_ms }),
410 (1000u64..=86400000u64).prop_map(|every_ms| ScheduleType::Every { every_ms }),
412 ]
413 }
414
415 fn arb_schedule_config() -> impl Strategy<Value = ScheduleTriggerConfig> {
417 arb_schedule_type().prop_map(|schedule_type| ScheduleTriggerConfig { schedule_type })
418 }
419
420 fn arb_webhook_config() -> impl Strategy<Value = WebhookTriggerConfig> {
422 (
423 "[a-zA-Z0-9]{16,32}".prop_map(|s| s), "/[a-z][a-z0-9/-]{0,30}".prop_map(|s| s), )
426 .prop_map(|(secret, path)| WebhookTriggerConfig { secret, path })
427 }
428
429 fn arb_trigger_config() -> impl Strategy<Value = (TriggerType, TriggerConfig)> {
431 prop_oneof![
432 Just((TriggerType::Mention, TriggerConfig::Mention)),
433 Just((TriggerType::DirectMessage, TriggerConfig::DirectMessage)),
434 arb_keyword_config().prop_map(|c| (TriggerType::Keyword, TriggerConfig::Keyword(c))),
435 arb_schedule_config().prop_map(|c| (TriggerType::Schedule, TriggerConfig::Schedule(c))),
436 arb_webhook_config().prop_map(|c| (TriggerType::Webhook, TriggerConfig::Webhook(c))),
437 ]
438 }
439
440 fn arb_trigger() -> impl Strategy<Value = AutoReplyTrigger> {
442 (
443 arb_identifier(), "[a-zA-Z0-9 _-]{1,50}".prop_map(|s| s), any::<bool>(), arb_trigger_config(), 0u32..=1000u32, proptest::option::of("[a-zA-Z0-9 ]{0,100}"), )
450 .prop_map(
451 |(id, name, enabled, (trigger_type, config), priority, response_template)| {
452 AutoReplyTrigger {
453 id,
454 name,
455 enabled,
456 trigger_type,
457 config,
458 priority,
459 response_template,
460 }
461 },
462 )
463 }
464
465 fn arb_triggers() -> impl Strategy<Value = Vec<AutoReplyTrigger>> {
467 prop::collection::vec(arb_trigger(), 0..10).prop_map(|triggers| {
468 let mut seen_ids = std::collections::HashSet::new();
469 triggers
470 .into_iter()
471 .enumerate()
472 .map(|(i, mut t)| {
473 while seen_ids.contains(&t.id) {
474 t.id = format!("{}_{}", t.id, i);
475 }
476 seen_ids.insert(t.id.clone());
477 t
478 })
479 .collect()
480 })
481 }
482
483 fn arb_group_activation() -> impl Strategy<Value = GroupActivation> {
485 (
486 arb_identifier(), any::<bool>(), any::<bool>(), proptest::option::of(0u64..=3600u64), proptest::option::of(prop::collection::vec(arb_user_id(), 0..5)), )
492 .prop_map(
493 |(group_id, enabled, require_mention, cooldown_seconds, whitelist)| {
494 GroupActivation {
495 group_id,
496 enabled,
497 require_mention,
498 cooldown_seconds,
499 whitelist,
500 }
501 },
502 )
503 }
504
505 fn arb_group_activations() -> impl Strategy<Value = Vec<GroupActivation>> {
507 prop::collection::vec(arb_group_activation(), 0..5).prop_map(|groups| {
508 let mut seen_ids = std::collections::HashSet::new();
509 groups
510 .into_iter()
511 .enumerate()
512 .map(|(i, mut g)| {
513 while seen_ids.contains(&g.group_id) {
514 g.group_id = format!("{}_{}", g.group_id, i);
515 }
516 seen_ids.insert(g.group_id.clone());
517 g
518 })
519 .collect()
520 })
521 }
522
523 fn arb_config() -> impl Strategy<Value = AutoReplyConfig> {
525 (
526 any::<bool>(), arb_triggers(), prop::collection::vec(arb_user_id(), 0..10), 0u64..=3600u64, arb_group_activations(), )
532 .prop_map(
533 |(enabled, triggers, whitelist, default_cooldown_seconds, group_activations)| {
534 AutoReplyConfig {
535 enabled,
536 triggers,
537 whitelist,
538 default_cooldown_seconds,
539 group_activations,
540 }
541 },
542 )
543 }
544
545 proptest! {
550 #![proptest_config(ProptestConfig::with_cases(100))]
551
552 #[test]
562 fn prop_config_json_roundtrip(config in arb_config()) {
563 let json = serde_json::to_string(&config)
568 .expect("AutoReplyConfig should serialize to JSON");
569
570 let parsed: AutoReplyConfig = serde_json::from_str(&json)
572 .expect("JSON should deserialize back to AutoReplyConfig");
573
574 prop_assert_eq!(
576 config.enabled, parsed.enabled,
577 "enabled field should match after round-trip"
578 );
579 prop_assert_eq!(
580 config.triggers.len(), parsed.triggers.len(),
581 "triggers count should match after round-trip"
582 );
583 prop_assert_eq!(
584 &config.whitelist, &parsed.whitelist,
585 "whitelist should match after round-trip"
586 );
587 prop_assert_eq!(
588 config.default_cooldown_seconds, parsed.default_cooldown_seconds,
589 "default_cooldown_seconds should match after round-trip"
590 );
591 prop_assert_eq!(
592 config.group_activations.len(), parsed.group_activations.len(),
593 "group_activations count should match after round-trip"
594 );
595
596 prop_assert_eq!(&config, &parsed, "Config should be equal after JSON round-trip");
598 }
599
600 #[test]
606 fn prop_config_save_load_roundtrip(config in arb_config()) {
607 let temp_dir = TempDir::new().expect("Should create temp dir");
612 let config_path = temp_dir.path().join("auto_reply.json");
613
614 config.save(&config_path).expect("Should save config");
616
617 let loaded = AutoReplyConfig::load(&config_path);
619
620 prop_assert_eq!(
622 config.enabled, loaded.enabled,
623 "enabled field should match after file round-trip"
624 );
625 prop_assert_eq!(
626 config.triggers.len(), loaded.triggers.len(),
627 "triggers count should match after file round-trip"
628 );
629 prop_assert_eq!(
630 &config.whitelist, &loaded.whitelist,
631 "whitelist should match after file round-trip"
632 );
633 prop_assert_eq!(
634 config.default_cooldown_seconds, loaded.default_cooldown_seconds,
635 "default_cooldown_seconds should match after file round-trip"
636 );
637 prop_assert_eq!(
638 config.group_activations.len(), loaded.group_activations.len(),
639 "group_activations count should match after file round-trip"
640 );
641
642 prop_assert_eq!(&config, &loaded, "Config should be equal after file round-trip");
644 }
645
646 #[test]
652 fn prop_config_reload_equals_load(config in arb_config()) {
653 let temp_dir = TempDir::new().expect("Should create temp dir");
658 let config_path = temp_dir.path().join("auto_reply.json");
659
660 config.save(&config_path).expect("Should save config");
662
663 let loaded = AutoReplyConfig::load(&config_path);
665
666 let reloaded = AutoReplyConfig::reload(&config_path)
668 .expect("Should reload config");
669
670 prop_assert_eq!(
672 &loaded, &reloaded,
673 "reload should produce same result as load"
674 );
675 }
676
677 #[test]
683 fn prop_trigger_roundtrip(trigger in arb_trigger()) {
684 let json = serde_json::to_string(&trigger)
688 .expect("AutoReplyTrigger should serialize to JSON");
689 let parsed: AutoReplyTrigger = serde_json::from_str(&json)
690 .expect("JSON should deserialize back to AutoReplyTrigger");
691
692 prop_assert_eq!(&trigger.id, &parsed.id, "id should match");
693 prop_assert_eq!(&trigger.name, &parsed.name, "name should match");
694 prop_assert_eq!(trigger.enabled, parsed.enabled, "enabled should match");
695 prop_assert_eq!(trigger.trigger_type, parsed.trigger_type, "trigger_type should match");
696 prop_assert_eq!(trigger.priority, parsed.priority, "priority should match");
697 prop_assert_eq!(&trigger.response_template, &parsed.response_template, "response_template should match");
698 prop_assert_eq!(&trigger, &parsed, "Trigger should be equal after round-trip");
699 }
700
701 #[test]
707 fn prop_group_activation_roundtrip(group in arb_group_activation()) {
708 let json = serde_json::to_string(&group)
712 .expect("GroupActivation should serialize to JSON");
713 let parsed: GroupActivation = serde_json::from_str(&json)
714 .expect("JSON should deserialize back to GroupActivation");
715
716 prop_assert_eq!(&group.group_id, &parsed.group_id, "group_id should match");
717 prop_assert_eq!(group.enabled, parsed.enabled, "enabled should match");
718 prop_assert_eq!(group.require_mention, parsed.require_mention, "require_mention should match");
719 prop_assert_eq!(group.cooldown_seconds, parsed.cooldown_seconds, "cooldown_seconds should match");
720 prop_assert_eq!(&group.whitelist, &parsed.whitelist, "whitelist should match");
721 prop_assert_eq!(&group, &parsed, "GroupActivation should be equal after round-trip");
722 }
723
724 #[test]
730 fn prop_config_multiple_roundtrips(config in arb_config()) {
731 let temp_dir = TempDir::new().expect("Should create temp dir");
735 let config_path = temp_dir.path().join("auto_reply.json");
736
737 config.save(&config_path).expect("Should save config");
739 let loaded1 = AutoReplyConfig::load(&config_path);
740
741 loaded1.save(&config_path).expect("Should save config again");
743 let loaded2 = AutoReplyConfig::load(&config_path);
744
745 prop_assert_eq!(
747 &config, &loaded1,
748 "First round-trip should preserve config"
749 );
750 prop_assert_eq!(
751 &loaded1, &loaded2,
752 "Second round-trip should preserve config"
753 );
754 prop_assert_eq!(
755 &config, &loaded2,
756 "Config should be stable after multiple round-trips"
757 );
758 }
759
760 #[test]
766 fn prop_config_json_format_independent(config in arb_config()) {
767 let compact_json = serde_json::to_string(&config)
772 .expect("Should serialize to compact JSON");
773 let from_compact: AutoReplyConfig = serde_json::from_str(&compact_json)
774 .expect("Should deserialize from compact JSON");
775
776 let pretty_json = serde_json::to_string_pretty(&config)
778 .expect("Should serialize to pretty JSON");
779 let from_pretty: AutoReplyConfig = serde_json::from_str(&pretty_json)
780 .expect("Should deserialize from pretty JSON");
781
782 prop_assert_eq!(
784 &from_compact, &from_pretty,
785 "Compact and pretty JSON should produce same result"
786 );
787 prop_assert_eq!(
788 &config, &from_compact,
789 "Config should be preserved regardless of JSON format"
790 );
791 }
792 }
793
794 #[test]
801 fn test_default_config() {
802 let config = AutoReplyConfig::default();
803
804 assert!(config.enabled);
805 assert!(config.triggers.is_empty());
806 assert!(config.whitelist.is_empty());
807 assert_eq!(config.default_cooldown_seconds, 60);
808 assert!(config.group_activations.is_empty());
809 }
810
811 #[test]
818 fn test_load_nonexistent_file() {
819 let config = AutoReplyConfig::load(Path::new("/nonexistent/path/config.json"));
820
821 assert!(config.enabled);
823 assert!(config.triggers.is_empty());
824 assert_eq!(config.default_cooldown_seconds, 60);
825 }
826
827 #[test]
830 fn test_load_valid_config() {
831 let temp_dir = TempDir::new().unwrap();
832 let config_path = temp_dir.path().join("config.json");
833
834 let original = AutoReplyConfig {
836 enabled: false,
837 triggers: vec![create_test_trigger("t1")],
838 whitelist: vec!["user1".to_string()],
839 default_cooldown_seconds: 120,
840 group_activations: vec![GroupActivation::new("group1")],
841 };
842
843 original.save(&config_path).unwrap();
845
846 let loaded = AutoReplyConfig::load(&config_path);
848
849 assert!(!loaded.enabled);
850 assert_eq!(loaded.triggers.len(), 1);
851 assert_eq!(loaded.whitelist, vec!["user1".to_string()]);
852 assert_eq!(loaded.default_cooldown_seconds, 120);
853 assert_eq!(loaded.group_activations.len(), 1);
854 }
855
856 #[test]
859 fn test_load_invalid_json() {
860 let temp_dir = TempDir::new().unwrap();
861 let config_path = temp_dir.path().join("config.json");
862
863 std::fs::write(&config_path, "{ invalid json }").unwrap();
865
866 let config = AutoReplyConfig::load(&config_path);
868 assert!(config.enabled);
869 assert!(config.triggers.is_empty());
870 }
871
872 #[test]
879 fn test_save_config() {
880 let temp_dir = TempDir::new().unwrap();
881 let config_path = temp_dir.path().join("config.json");
882
883 let config = AutoReplyConfig {
884 enabled: true,
885 triggers: vec![create_test_trigger("t1")],
886 whitelist: vec!["user1".to_string()],
887 default_cooldown_seconds: 90,
888 group_activations: vec![],
889 };
890
891 let result = config.save(&config_path);
893 assert!(result.is_ok());
894
895 assert!(config_path.exists());
897
898 let content = std::fs::read_to_string(&config_path).unwrap();
900 let parsed: AutoReplyConfig = serde_json::from_str(&content).unwrap();
901 assert_eq!(parsed.default_cooldown_seconds, 90);
902 }
903
904 #[test]
906 fn test_save_creates_parent_dirs() {
907 let temp_dir = TempDir::new().unwrap();
908 let config_path = temp_dir.path().join("nested/dir/config.json");
909
910 let config = AutoReplyConfig::default();
911 let result = config.save(&config_path);
912
913 assert!(result.is_ok());
914 assert!(config_path.exists());
915 }
916
917 #[test]
924 fn test_reload_config() {
925 let temp_dir = TempDir::new().unwrap();
926 let config_path = temp_dir.path().join("config.json");
927
928 let initial = AutoReplyConfig {
930 enabled: true,
931 default_cooldown_seconds: 60,
932 ..Default::default()
933 };
934 initial.save(&config_path).unwrap();
935
936 let modified = AutoReplyConfig {
938 enabled: false,
939 default_cooldown_seconds: 120,
940 ..Default::default()
941 };
942 modified.save(&config_path).unwrap();
943
944 let reloaded = AutoReplyConfig::reload(&config_path).unwrap();
946
947 assert!(!reloaded.enabled);
948 assert_eq!(reloaded.default_cooldown_seconds, 120);
949 }
950
951 #[test]
953 fn test_reload_nonexistent_file() {
954 let result = AutoReplyConfig::reload(Path::new("/nonexistent/config.json"));
955 assert!(result.is_err());
956 }
957
958 #[test]
964 fn test_validate_valid_config() {
965 let config = AutoReplyConfig {
966 triggers: vec![create_test_trigger("t1"), create_test_trigger("t2")],
967 group_activations: vec![GroupActivation::new("g1"), GroupActivation::new("g2")],
968 ..Default::default()
969 };
970
971 let warnings = config.validate();
972 assert!(warnings.is_empty());
973 }
974
975 #[test]
977 fn test_validate_duplicate_trigger_ids() {
978 let config = AutoReplyConfig {
979 triggers: vec![
980 create_test_trigger("t1"),
981 create_test_trigger("t1"), ],
983 ..Default::default()
984 };
985
986 let warnings = config.validate();
987 assert_eq!(warnings.len(), 1);
988 assert!(warnings[0].contains("Duplicate trigger ID"));
989 }
990
991 #[test]
993 fn test_validate_duplicate_group_ids() {
994 let config = AutoReplyConfig {
995 group_activations: vec![
996 GroupActivation::new("g1"),
997 GroupActivation::new("g1"), ],
999 ..Default::default()
1000 };
1001
1002 let warnings = config.validate();
1003 assert_eq!(warnings.len(), 1);
1004 assert!(warnings[0].contains("Duplicate group ID"));
1005 }
1006
1007 #[test]
1013 fn test_is_default() {
1014 let default_config = AutoReplyConfig::default();
1015 assert!(default_config.is_default());
1016
1017 let modified_config = AutoReplyConfig {
1018 enabled: false,
1019 ..Default::default()
1020 };
1021 assert!(!modified_config.is_default());
1022 }
1023
1024 #[test]
1026 fn test_enabled_trigger_count() {
1027 let mut t1 = create_test_trigger("t1");
1028 t1.enabled = true;
1029 let mut t2 = create_test_trigger("t2");
1030 t2.enabled = false;
1031 let mut t3 = create_test_trigger("t3");
1032 t3.enabled = true;
1033
1034 let config = AutoReplyConfig {
1035 triggers: vec![t1, t2, t3],
1036 ..Default::default()
1037 };
1038
1039 assert_eq!(config.enabled_trigger_count(), 2);
1040 }
1041
1042 #[test]
1044 fn test_group_count() {
1045 let config = AutoReplyConfig {
1046 group_activations: vec![GroupActivation::new("g1"), GroupActivation::new("g2")],
1047 ..Default::default()
1048 };
1049
1050 assert_eq!(config.group_count(), 2);
1051 }
1052
1053 #[test]
1055 fn test_merge() {
1056 let mut config1 = AutoReplyConfig {
1057 enabled: true,
1058 triggers: vec![create_test_trigger("t1")],
1059 whitelist: vec!["user1".to_string()],
1060 default_cooldown_seconds: 60,
1061 group_activations: vec![GroupActivation::new("g1")],
1062 };
1063
1064 let config2 = AutoReplyConfig {
1065 enabled: false,
1066 triggers: vec![create_test_trigger("t2")],
1067 whitelist: vec!["user2".to_string()],
1068 default_cooldown_seconds: 120,
1069 group_activations: vec![GroupActivation::new("g2")],
1070 };
1071
1072 config1.merge(config2);
1073
1074 assert!(!config1.enabled);
1075 assert_eq!(config1.triggers.len(), 2);
1076 assert_eq!(config1.whitelist.len(), 2);
1077 assert_eq!(config1.default_cooldown_seconds, 120);
1078 assert_eq!(config1.group_activations.len(), 2);
1079 }
1080
1081 #[test]
1087 fn test_serialization_roundtrip() {
1088 let config = AutoReplyConfig {
1089 enabled: false,
1090 triggers: vec![create_test_trigger("t1")],
1091 whitelist: vec!["user1".to_string(), "user2".to_string()],
1092 default_cooldown_seconds: 90,
1093 group_activations: vec![GroupActivation::new("g1").with_require_mention(true)],
1094 };
1095
1096 let json = serde_json::to_string(&config).unwrap();
1097 let parsed: AutoReplyConfig = serde_json::from_str(&json).unwrap();
1098
1099 assert_eq!(config.enabled, parsed.enabled);
1100 assert_eq!(config.triggers.len(), parsed.triggers.len());
1101 assert_eq!(config.whitelist, parsed.whitelist);
1102 assert_eq!(
1103 config.default_cooldown_seconds,
1104 parsed.default_cooldown_seconds
1105 );
1106 assert_eq!(
1107 config.group_activations.len(),
1108 parsed.group_activations.len()
1109 );
1110 }
1111
1112 #[test]
1114 fn test_deserialization_defaults() {
1115 let json = "{}";
1116 let config: AutoReplyConfig = serde_json::from_str(json).unwrap();
1117
1118 assert!(config.enabled); assert!(config.triggers.is_empty());
1120 assert!(config.whitelist.is_empty());
1121 assert_eq!(config.default_cooldown_seconds, 60); assert!(config.group_activations.is_empty());
1123 }
1124}