Skip to main content

aster/auto_reply/
config.rs

1//! 配置持久化
2//!
3//! 自动回复配置的加载和保存。
4//!
5//! # 功能
6//!
7//! - 从 JSON 文件加载配置(Requirement 10.2)
8//! - 保存配置到 JSON 文件(Requirement 10.1)
9//! - 文件不存在时使用默认配置(Requirement 10.3)
10//! - 解析错误时记录日志并使用默认配置(Requirement 10.4)
11//! - 支持配置热重载(Requirement 10.5)
12//!
13//! # 示例
14//!
15//! ```rust,ignore
16//! use std::path::Path;
17//! use aster::auto_reply::AutoReplyConfig;
18//!
19//! // 加载配置
20//! let config = AutoReplyConfig::load(Path::new("auto_reply.json"))?;
21//!
22//! // 保存配置
23//! config.save(Path::new("auto_reply.json"))?;
24//!
25//! // 使用默认配置
26//! let default_config = AutoReplyConfig::default();
27//! ```
28
29use 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/// 配置加载/保存错误
38#[derive(Debug, thiserror::Error)]
39pub enum ConfigError {
40    /// IO 错误
41    #[error("IO error: {0}")]
42    Io(#[from] std::io::Error),
43    /// JSON 解析错误
44    #[error("JSON parse error: {0}")]
45    Json(#[from] serde_json::Error),
46}
47
48/// 配置加载结果类型
49pub type ConfigResult<T> = Result<T, ConfigError>;
50
51/// 自动回复配置
52///
53/// 包含自动回复系统的所有配置项。
54///
55/// # 字段说明
56///
57/// - `enabled`: 全局开关,控制是否启用自动回复
58/// - `triggers`: 触发器列表,定义触发条件
59/// - `whitelist`: 白名单用户列表,空列表表示允许所有用户
60/// - `default_cooldown_seconds`: 默认冷却时间(秒)
61/// - `group_activations`: 群组特定配置列表
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub struct AutoReplyConfig {
64    /// 是否启用自动回复
65    #[serde(default = "default_true")]
66    pub enabled: bool,
67    /// 触发器列表
68    #[serde(default)]
69    pub triggers: Vec<AutoReplyTrigger>,
70    /// 白名单用户
71    #[serde(default)]
72    pub whitelist: Vec<String>,
73    /// 默认冷却时间(秒)
74    #[serde(default = "default_cooldown")]
75    pub default_cooldown_seconds: u64,
76    /// 群组激活配置
77    #[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    /// 创建默认配置
91    ///
92    /// **Validates: Requirement 10.3**
93    ///
94    /// 默认配置:
95    /// - 启用自动回复
96    /// - 无触发器
97    /// - 空白名单(允许所有用户)
98    /// - 60 秒冷却时间
99    /// - 无群组特定配置
100    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    /// 从文件加载配置
113    ///
114    /// **Validates: Requirements 10.2, 10.3, 10.4**
115    ///
116    /// # 行为
117    ///
118    /// - 文件存在且有效:返回解析后的配置
119    /// - 文件不存在:记录 info 日志,返回默认配置(Requirement 10.3)
120    /// - 文件解析失败:记录 error 日志,返回默认配置(Requirement 10.4)
121    ///
122    /// # 参数
123    ///
124    /// * `path` - 配置文件路径
125    ///
126    /// # 返回值
127    ///
128    /// 返回加载的配置,如果加载失败则返回默认配置。
129    ///
130    /// # 示例
131    ///
132    /// ```rust,ignore
133    /// use std::path::Path;
134    /// use aster::auto_reply::AutoReplyConfig;
135    ///
136    /// let config = AutoReplyConfig::load(Path::new("auto_reply.json"));
137    /// ```
138    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                // Requirement 10.3: 文件不存在时使用默认配置
146                info!(
147                    "Auto-reply config file not found at {:?}, using defaults",
148                    path
149                );
150                Self::default()
151            }
152            Err(e) => {
153                // Requirement 10.4: 解析错误时记录日志并使用默认配置
154                error!(
155                    "Failed to load auto-reply config from {:?}: {}, using defaults",
156                    path, e
157                );
158                Self::default()
159            }
160        }
161    }
162
163    /// 从文件加载配置(返回 Result)
164    ///
165    /// 内部方法,用于区分不同的错误类型。
166    ///
167    /// # 参数
168    ///
169    /// * `path` - 配置文件路径
170    ///
171    /// # 返回值
172    ///
173    /// 成功时返回配置,失败时返回错误。
174    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    /// 保存配置到文件
181    ///
182    /// **Validates: Requirement 10.1**
183    ///
184    /// 将配置序列化为格式化的 JSON 并写入文件。
185    /// 如果父目录不存在,会自动创建。
186    ///
187    /// # 参数
188    ///
189    /// * `path` - 配置文件路径
190    ///
191    /// # 返回值
192    ///
193    /// 成功时返回 `Ok(())`,失败时返回错误。
194    ///
195    /// # 示例
196    ///
197    /// ```rust,ignore
198    /// use std::path::Path;
199    /// use aster::auto_reply::AutoReplyConfig;
200    ///
201    /// let config = AutoReplyConfig::default();
202    /// config.save(Path::new("auto_reply.json"))?;
203    /// ```
204    pub fn save(&self, path: &Path) -> ConfigResult<()> {
205        // 确保父目录存在
206        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    /// 重新加载配置
219    ///
220    /// **Validates: Requirement 10.5**
221    ///
222    /// 从文件重新加载配置,支持热重载。
223    /// 如果加载失败,保持当前配置不变并返回错误。
224    ///
225    /// # 参数
226    ///
227    /// * `path` - 配置文件路径
228    ///
229    /// # 返回值
230    ///
231    /// 成功时返回新配置,失败时返回错误(当前配置不变)。
232    ///
233    /// # 示例
234    ///
235    /// ```rust,ignore
236    /// use std::path::Path;
237    /// use aster::auto_reply::AutoReplyConfig;
238    ///
239    /// let mut config = AutoReplyConfig::default();
240    /// match config.reload(Path::new("auto_reply.json")) {
241    ///     Ok(new_config) => {
242    ///         config = new_config;
243    ///         println!("Config reloaded successfully");
244    ///     }
245    ///     Err(e) => {
246    ///         println!("Failed to reload config: {}", e);
247    ///     }
248    /// }
249    /// ```
250    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    /// 验证配置有效性
257    ///
258    /// 检查配置中的各项设置是否有效。
259    ///
260    /// # 返回值
261    ///
262    /// 返回验证结果列表,每个元素是一个警告消息。
263    /// 空列表表示配置完全有效。
264    pub fn validate(&self) -> Vec<String> {
265        let mut warnings = Vec::new();
266
267        // 检查触发器 ID 是否唯一
268        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        // 检查群组配置 ID 是否唯一
276        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        // 记录警告日志
284        for warning in &warnings {
285            warn!("Config validation warning: {}", warning);
286        }
287
288        warnings
289    }
290
291    /// 合并另一个配置
292    ///
293    /// 将另一个配置的内容合并到当前配置中。
294    /// 触发器和群组配置会追加,其他字段会被覆盖。
295    ///
296    /// # 参数
297    ///
298    /// * `other` - 要合并的配置
299    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    /// 检查是否为默认配置
308    pub fn is_default(&self) -> bool {
309        *self == Self::default()
310    }
311
312    /// 获取启用的触发器数量
313    pub fn enabled_trigger_count(&self) -> usize {
314        self.triggers.iter().filter(|t| t.enabled).count()
315    }
316
317    /// 获取群组配置数量
318    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    /// 创建测试用的触发器
334    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    // ============================================================================
351    // Property-Based Test Generators
352    // ============================================================================
353    // Feature: auto-reply-mechanism, Property 10: 配置持久化 Round-Trip
354    // **Validates: Requirements 10.1, 10.2, 10.5**
355
356    /// 生成有效的标识符字符串(用于 ID、名称等)
357    fn arb_identifier() -> impl Strategy<Value = String> {
358        "[a-zA-Z][a-zA-Z0-9_-]{0,19}".prop_map(|s| s)
359    }
360
361    /// 生成有效的用户 ID
362    fn arb_user_id() -> impl Strategy<Value = String> {
363        "[a-zA-Z0-9_-]{1,20}".prop_map(|s| s)
364    }
365
366    /// 生成关键词模式(避免无效正则表达式)
367    fn arb_keyword_pattern() -> impl Strategy<Value = String> {
368        "[a-zA-Z0-9_\\-\\s]{1,30}".prop_map(|s| s)
369    }
370
371    /// 生成 TriggerType
372    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    /// 生成 KeywordTriggerConfig
383    fn arb_keyword_config() -> impl Strategy<Value = KeywordTriggerConfig> {
384        (
385            prop::collection::vec(arb_keyword_pattern(), 1..5),
386            any::<bool>(),
387            // use_regex 设为 false 以避免无效正则表达式问题
388            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    /// 生成 ScheduleType
400    fn arb_schedule_type() -> impl Strategy<Value = ScheduleType> {
401        prop_oneof![
402            // Cron 表达式(使用简单有效的 cron 格式)
403            (
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            // At 一次性定时
409            (0i64..=i64::MAX).prop_map(|at_ms| ScheduleType::At { at_ms }),
410            // Every 固定间隔
411            (1000u64..=86400000u64).prop_map(|every_ms| ScheduleType::Every { every_ms }),
412        ]
413    }
414
415    /// 生成 ScheduleTriggerConfig
416    fn arb_schedule_config() -> impl Strategy<Value = ScheduleTriggerConfig> {
417        arb_schedule_type().prop_map(|schedule_type| ScheduleTriggerConfig { schedule_type })
418    }
419
420    /// 生成 WebhookTriggerConfig
421    fn arb_webhook_config() -> impl Strategy<Value = WebhookTriggerConfig> {
422        (
423            "[a-zA-Z0-9]{16,32}".prop_map(|s| s),     // secret
424            "/[a-z][a-z0-9/-]{0,30}".prop_map(|s| s), // path
425        )
426            .prop_map(|(secret, path)| WebhookTriggerConfig { secret, path })
427    }
428
429    /// 生成 TriggerConfig(与 TriggerType 匹配)
430    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    /// 生成 AutoReplyTrigger
441    fn arb_trigger() -> impl Strategy<Value = AutoReplyTrigger> {
442        (
443            arb_identifier(),                            // id
444            "[a-zA-Z0-9 _-]{1,50}".prop_map(|s| s),      // name
445            any::<bool>(),                               // enabled
446            arb_trigger_config(),                        // (trigger_type, config)
447            0u32..=1000u32,                              // priority
448            proptest::option::of("[a-zA-Z0-9 ]{0,100}"), // response_template
449        )
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    /// 生成具有唯一 ID 的触发器列表
466    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    /// 生成 GroupActivation
484    fn arb_group_activation() -> impl Strategy<Value = GroupActivation> {
485        (
486            arb_identifier(),                                                 // group_id
487            any::<bool>(),                                                    // enabled
488            any::<bool>(),                                                    // require_mention
489            proptest::option::of(0u64..=3600u64),                             // cooldown_seconds
490            proptest::option::of(prop::collection::vec(arb_user_id(), 0..5)), // whitelist
491        )
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    /// 生成具有唯一 group_id 的群组配置列表
506    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    /// 生成 AutoReplyConfig
524    fn arb_config() -> impl Strategy<Value = AutoReplyConfig> {
525        (
526            any::<bool>(),                               // enabled
527            arb_triggers(),                              // triggers
528            prop::collection::vec(arb_user_id(), 0..10), // whitelist
529            0u64..=3600u64,                              // default_cooldown_seconds
530            arb_group_activations(),                     // group_activations
531        )
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    // ============================================================================
546    // Property-Based Tests
547    // ============================================================================
548
549    proptest! {
550        #![proptest_config(ProptestConfig::with_cases(100))]
551
552        /// Property 10: 配置持久化 Round-Trip - JSON 序列化
553        ///
554        /// **Validates: Requirements 10.1, 10.2, 10.5**
555        ///
556        /// For any AutoReplyConfig 实例,序列化为 JSON 后再反序列化应产生等价的配置。
557        /// 这验证了:
558        /// - Requirement 10.1: 配置可以正确保存(序列化)
559        /// - Requirement 10.2: 配置可以正确加载(反序列化)
560        /// - Requirement 10.5: 热重载产生相同结果(reload 使用相同的序列化/反序列化逻辑)
561        #[test]
562        fn prop_config_json_roundtrip(config in arb_config()) {
563            // Feature: auto-reply-mechanism, Property 10: 配置持久化 Round-Trip
564            // **Validates: Requirements 10.1, 10.2, 10.5**
565
566            // 序列化为 JSON
567            let json = serde_json::to_string(&config)
568                .expect("AutoReplyConfig should serialize to JSON");
569
570            // 反序列化回 AutoReplyConfig
571            let parsed: AutoReplyConfig = serde_json::from_str(&json)
572                .expect("JSON should deserialize back to AutoReplyConfig");
573
574            // 验证 round-trip 一致性
575            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            // 验证完整相等性
597            prop_assert_eq!(&config, &parsed, "Config should be equal after JSON round-trip");
598        }
599
600        /// Property 10: 配置持久化 Round-Trip - 文件保存和加载
601        ///
602        /// **Validates: Requirements 10.1, 10.2**
603        ///
604        /// For any AutoReplyConfig 实例,保存到 JSON 文件后再加载应产生等价的配置。
605        #[test]
606        fn prop_config_save_load_roundtrip(config in arb_config()) {
607            // Feature: auto-reply-mechanism, Property 10: 配置持久化 Round-Trip
608            // **Validates: Requirements 10.1, 10.2, 10.5**
609
610            // 创建临时目录
611            let temp_dir = TempDir::new().expect("Should create temp dir");
612            let config_path = temp_dir.path().join("auto_reply.json");
613
614            // 保存配置 (Requirement 10.1)
615            config.save(&config_path).expect("Should save config");
616
617            // 加载配置 (Requirement 10.2)
618            let loaded = AutoReplyConfig::load(&config_path);
619
620            // 验证 round-trip 一致性
621            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            // 验证完整相等性
643            prop_assert_eq!(&config, &loaded, "Config should be equal after file round-trip");
644        }
645
646        /// Property 10: 配置持久化 Round-Trip - reload 产生相同结果
647        ///
648        /// **Validates: Requirement 10.5**
649        ///
650        /// For any AutoReplyConfig 实例,reload 应产生与 load 相同的结果。
651        #[test]
652        fn prop_config_reload_equals_load(config in arb_config()) {
653            // Feature: auto-reply-mechanism, Property 10: 配置持久化 Round-Trip
654            // **Validates: Requirements 10.1, 10.2, 10.5**
655
656            // 创建临时目录
657            let temp_dir = TempDir::new().expect("Should create temp dir");
658            let config_path = temp_dir.path().join("auto_reply.json");
659
660            // 保存配置
661            config.save(&config_path).expect("Should save config");
662
663            // 使用 load 加载
664            let loaded = AutoReplyConfig::load(&config_path);
665
666            // 使用 reload 加载 (Requirement 10.5)
667            let reloaded = AutoReplyConfig::reload(&config_path)
668                .expect("Should reload config");
669
670            // 验证 load 和 reload 产生相同结果
671            prop_assert_eq!(
672                &loaded, &reloaded,
673                "reload should produce same result as load"
674            );
675        }
676
677        /// Property 10 补充: 触发器 round-trip 保持所有字段
678        ///
679        /// **Validates: Requirements 10.1, 10.2**
680        ///
681        /// For any AutoReplyTrigger,序列化后再反序列化应保持所有字段。
682        #[test]
683        fn prop_trigger_roundtrip(trigger in arb_trigger()) {
684            // Feature: auto-reply-mechanism, Property 10: 配置持久化 Round-Trip
685            // **Validates: Requirements 10.1, 10.2, 10.5**
686
687            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        /// Property 10 补充: GroupActivation round-trip 保持所有字段
702        ///
703        /// **Validates: Requirements 10.1, 10.2**
704        ///
705        /// For any GroupActivation,序列化后再反序列化应保持所有字段。
706        #[test]
707        fn prop_group_activation_roundtrip(group in arb_group_activation()) {
708            // Feature: auto-reply-mechanism, Property 10: 配置持久化 Round-Trip
709            // **Validates: Requirements 10.1, 10.2, 10.5**
710
711            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        /// Property 10 补充: 多次保存加载保持一致性
725        ///
726        /// **Validates: Requirements 10.1, 10.2, 10.5**
727        ///
728        /// For any AutoReplyConfig,多次保存和加载应产生相同结果。
729        #[test]
730        fn prop_config_multiple_roundtrips(config in arb_config()) {
731            // Feature: auto-reply-mechanism, Property 10: 配置持久化 Round-Trip
732            // **Validates: Requirements 10.1, 10.2, 10.5**
733
734            let temp_dir = TempDir::new().expect("Should create temp dir");
735            let config_path = temp_dir.path().join("auto_reply.json");
736
737            // 第一次 round-trip
738            config.save(&config_path).expect("Should save config");
739            let loaded1 = AutoReplyConfig::load(&config_path);
740
741            // 第二次 round-trip
742            loaded1.save(&config_path).expect("Should save config again");
743            let loaded2 = AutoReplyConfig::load(&config_path);
744
745            // 验证多次 round-trip 后仍然一致
746            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        /// Property 10 补充: pretty JSON 格式不影响加载
761        ///
762        /// **Validates: Requirements 10.1, 10.2**
763        ///
764        /// 无论使用 compact 还是 pretty JSON 格式,加载结果应相同。
765        #[test]
766        fn prop_config_json_format_independent(config in arb_config()) {
767            // Feature: auto-reply-mechanism, Property 10: 配置持久化 Round-Trip
768            // **Validates: Requirements 10.1, 10.2, 10.5**
769
770            // Compact JSON
771            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            // Pretty JSON
777            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            // 两种格式应产生相同结果
783            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    // ============================================================================
795    // Default 测试
796    // ============================================================================
797
798    /// 测试默认配置
799    /// **Validates: Requirement 10.3**
800    #[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    // ============================================================================
812    // Load 测试
813    // ============================================================================
814
815    /// 测试加载不存在的文件
816    /// **Validates: Requirement 10.3**
817    #[test]
818    fn test_load_nonexistent_file() {
819        let config = AutoReplyConfig::load(Path::new("/nonexistent/path/config.json"));
820
821        // 应该返回默认配置
822        assert!(config.enabled);
823        assert!(config.triggers.is_empty());
824        assert_eq!(config.default_cooldown_seconds, 60);
825    }
826
827    /// 测试加载有效的配置文件
828    /// **Validates: Requirement 10.2**
829    #[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        // 创建测试配置
835        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        // 保存配置
844        original.save(&config_path).unwrap();
845
846        // 加载配置
847        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    /// 测试加载无效的 JSON 文件
857    /// **Validates: Requirement 10.4**
858    #[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        // 写入无效的 JSON
864        std::fs::write(&config_path, "{ invalid json }").unwrap();
865
866        // 应该返回默认配置
867        let config = AutoReplyConfig::load(&config_path);
868        assert!(config.enabled);
869        assert!(config.triggers.is_empty());
870    }
871
872    // ============================================================================
873    // Save 测试
874    // ============================================================================
875
876    /// 测试保存配置
877    /// **Validates: Requirement 10.1**
878    #[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        // 保存应该成功
892        let result = config.save(&config_path);
893        assert!(result.is_ok());
894
895        // 文件应该存在
896        assert!(config_path.exists());
897
898        // 内容应该是有效的 JSON
899        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    /// 测试保存到嵌套目录
905    #[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    // ============================================================================
918    // Reload 测试
919    // ============================================================================
920
921    /// 测试重新加载配置
922    /// **Validates: Requirement 10.5**
923    #[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        // 保存初始配置
929        let initial = AutoReplyConfig {
930            enabled: true,
931            default_cooldown_seconds: 60,
932            ..Default::default()
933        };
934        initial.save(&config_path).unwrap();
935
936        // 修改文件
937        let modified = AutoReplyConfig {
938            enabled: false,
939            default_cooldown_seconds: 120,
940            ..Default::default()
941        };
942        modified.save(&config_path).unwrap();
943
944        // 重新加载
945        let reloaded = AutoReplyConfig::reload(&config_path).unwrap();
946
947        assert!(!reloaded.enabled);
948        assert_eq!(reloaded.default_cooldown_seconds, 120);
949    }
950
951    /// 测试重新加载不存在的文件
952    #[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    // ============================================================================
959    // Validate 测试
960    // ============================================================================
961
962    /// 测试验证有效配置
963    #[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    /// 测试验证重复触发器 ID
976    #[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"), // 重复
982            ],
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    /// 测试验证重复群组 ID
992    #[test]
993    fn test_validate_duplicate_group_ids() {
994        let config = AutoReplyConfig {
995            group_activations: vec![
996                GroupActivation::new("g1"),
997                GroupActivation::new("g1"), // 重复
998            ],
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    // ============================================================================
1008    // 辅助方法测试
1009    // ============================================================================
1010
1011    /// 测试 is_default
1012    #[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    /// 测试 enabled_trigger_count
1025    #[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    /// 测试 group_count
1043    #[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    /// 测试 merge
1054    #[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    // ============================================================================
1082    // 序列化 Round-Trip 测试
1083    // ============================================================================
1084
1085    /// 测试序列化和反序列化 round-trip
1086    #[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    /// 测试默认值反序列化
1113    #[test]
1114    fn test_deserialization_defaults() {
1115        let json = "{}";
1116        let config: AutoReplyConfig = serde_json::from_str(json).unwrap();
1117
1118        assert!(config.enabled); // default_true
1119        assert!(config.triggers.is_empty());
1120        assert!(config.whitelist.is_empty());
1121        assert_eq!(config.default_cooldown_seconds, 60); // default_cooldown
1122        assert!(config.group_activations.is_empty());
1123    }
1124}