Skip to main content

aster/auto_reply/
cooldown.rs

1//! 冷却时间追踪器
2//!
3//! 防止用户频繁触发自动回复。
4
5use std::collections::HashMap;
6use std::time::{Duration, Instant};
7
8use crate::auto_reply::types::TriggerType;
9
10/// 冷却检查结果
11#[derive(Debug, Clone)]
12pub enum CooldownCheckResult {
13    /// 允许触发
14    Allowed,
15    /// 在冷却中
16    InCooldown { remaining: Duration },
17}
18
19/// 冷却时间追踪器
20pub struct CooldownTracker {
21    /// 用户最后触发时间
22    last_trigger: HashMap<String, Instant>,
23    /// 默认冷却时间
24    default_cooldown: Duration,
25    /// 每种触发类型的冷却时间
26    type_cooldowns: HashMap<TriggerType, Duration>,
27}
28
29impl CooldownTracker {
30    /// 创建新的冷却追踪器
31    pub fn new(default_cooldown: Duration) -> Self {
32        Self {
33            last_trigger: HashMap::new(),
34            default_cooldown,
35            type_cooldowns: HashMap::new(),
36        }
37    }
38
39    /// 检查用户是否在冷却中
40    pub fn check_cooldown(&self, user_id: &str, trigger_type: TriggerType) -> CooldownCheckResult {
41        let cooldown = self
42            .type_cooldowns
43            .get(&trigger_type)
44            .copied()
45            .unwrap_or(self.default_cooldown);
46
47        match self.last_trigger.get(user_id) {
48            Some(last) => {
49                let elapsed = last.elapsed();
50                if elapsed < cooldown {
51                    CooldownCheckResult::InCooldown {
52                        remaining: cooldown - elapsed,
53                    }
54                } else {
55                    CooldownCheckResult::Allowed
56                }
57            }
58            None => CooldownCheckResult::Allowed,
59        }
60    }
61
62    /// 记录触发时间
63    pub fn record_trigger(&mut self, user_id: &str) {
64        self.last_trigger
65            .insert(user_id.to_string(), Instant::now());
66    }
67
68    /// 设置特定触发类型的冷却时间
69    pub fn set_type_cooldown(&mut self, trigger_type: TriggerType, duration: Duration) {
70        self.type_cooldowns.insert(trigger_type, duration);
71    }
72
73    /// 重置用户冷却
74    pub fn reset_cooldown(&mut self, user_id: &str) {
75        self.last_trigger.remove(user_id);
76    }
77
78    /// 清理过期记录
79    pub fn cleanup_expired(&mut self) {
80        let max_cooldown = self
81            .type_cooldowns
82            .values()
83            .max()
84            .copied()
85            .unwrap_or(self.default_cooldown);
86
87        self.last_trigger
88            .retain(|_, instant| instant.elapsed() < max_cooldown * 2);
89    }
90
91    /// 获取默认冷却时间
92    pub fn default_cooldown(&self) -> Duration {
93        self.default_cooldown
94    }
95
96    /// 获取特定触发类型的冷却时间
97    pub fn get_type_cooldown(&self, trigger_type: TriggerType) -> Duration {
98        self.type_cooldowns
99            .get(&trigger_type)
100            .copied()
101            .unwrap_or(self.default_cooldown)
102    }
103
104    /// 检查用户是否有触发记录
105    pub fn has_trigger_record(&self, user_id: &str) -> bool {
106        self.last_trigger.contains_key(user_id)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use proptest::prelude::*;
114    use std::thread;
115
116    // ============================================================================
117    // Property-Based Tests
118    // ============================================================================
119    // Feature: auto-reply-mechanism, Property 4: 冷却时间行为一致性
120    // **Validates: Requirements 4.1-4.6**
121
122    /// 生成有效的用户 ID
123    fn arb_user_id() -> impl Strategy<Value = String> {
124        "[a-zA-Z0-9_]{1,20}".prop_map(|s| s)
125    }
126
127    /// 生成有效的冷却时间(毫秒)
128    /// 使用较短的时间以避免测试过慢
129    fn arb_cooldown_ms() -> impl Strategy<Value = u64> {
130        10u64..500
131    }
132
133    /// 生成 TriggerType
134    fn arb_trigger_type() -> impl Strategy<Value = TriggerType> {
135        prop_oneof![
136            Just(TriggerType::Mention),
137            Just(TriggerType::Keyword),
138            Just(TriggerType::DirectMessage),
139            Just(TriggerType::Schedule),
140            Just(TriggerType::Webhook),
141        ]
142    }
143
144    proptest! {
145        #![proptest_config(ProptestConfig::with_cases(20))]
146
147        /// Property 4.1: 新用户(无触发记录)总是被允许
148        /// **Validates: Requirement 4.1, 4.5**
149        #[test]
150        fn prop_new_user_always_allowed(
151            user_id in arb_user_id(),
152            cooldown_ms in arb_cooldown_ms(),
153            trigger_type in arb_trigger_type()
154        ) {
155            let tracker = CooldownTracker::new(Duration::from_millis(cooldown_ms));
156
157            // 新用户应该总是被允许
158            let result = tracker.check_cooldown(&user_id, trigger_type);
159            prop_assert!(
160                matches!(result, CooldownCheckResult::Allowed),
161                "New user should always be allowed, got {:?}",
162                result
163            );
164        }
165
166        /// Property 4.2: 记录触发后,用户在冷却期内
167        /// **Validates: Requirements 4.1, 4.2**
168        #[test]
169        fn prop_after_trigger_user_in_cooldown(
170            user_id in arb_user_id(),
171            cooldown_ms in 100u64..1000,  // 使用较长的冷却时间确保测试稳定
172            trigger_type in arb_trigger_type()
173        ) {
174            let mut tracker = CooldownTracker::new(Duration::from_millis(cooldown_ms));
175
176            // 记录触发
177            tracker.record_trigger(&user_id);
178
179            // 立即检查应该在冷却中
180            let result = tracker.check_cooldown(&user_id, trigger_type);
181            prop_assert!(
182                matches!(result, CooldownCheckResult::InCooldown { .. }),
183                "User should be in cooldown immediately after trigger, got {:?}",
184                result
185            );
186        }
187
188        /// Property 4.3: 剩余时间总是 <= 配置的冷却时间
189        /// **Validates: Requirements 4.3, 4.6**
190        #[test]
191        fn prop_remaining_time_bounded_by_cooldown(
192            user_id in arb_user_id(),
193            cooldown_ms in 100u64..1000,
194            trigger_type in arb_trigger_type()
195        ) {
196            let cooldown = Duration::from_millis(cooldown_ms);
197            let mut tracker = CooldownTracker::new(cooldown);
198
199            // 记录触发
200            tracker.record_trigger(&user_id);
201
202            // 检查冷却状态
203            let result = tracker.check_cooldown(&user_id, trigger_type);
204            if let CooldownCheckResult::InCooldown { remaining } = result {
205                prop_assert!(
206                    remaining <= cooldown,
207                    "Remaining time {:?} should be <= cooldown {:?}",
208                    remaining,
209                    cooldown
210                );
211            }
212        }
213
214        /// Property 4.4: 不同触发类型可以有不同的冷却时间
215        /// **Validates: Requirement 4.4**
216        #[test]
217        fn prop_different_trigger_types_different_cooldowns(
218            user_id in arb_user_id(),
219            default_ms in 100u64..500,
220            mention_ms in 10u64..50,
221            keyword_ms in 200u64..500
222        ) {
223            // 确保 mention 冷却时间明显短于 keyword
224            prop_assume!(mention_ms < keyword_ms);
225
226            let mut tracker = CooldownTracker::new(Duration::from_millis(default_ms));
227            tracker.set_type_cooldown(TriggerType::Mention, Duration::from_millis(mention_ms));
228            tracker.set_type_cooldown(TriggerType::Keyword, Duration::from_millis(keyword_ms));
229
230            // 记录触发
231            tracker.record_trigger(&user_id);
232
233            // 等待 mention 冷却过期但 keyword 还在冷却中
234            thread::sleep(Duration::from_millis(mention_ms + 10));
235
236            // Mention 应该被允许
237            let mention_result = tracker.check_cooldown(&user_id, TriggerType::Mention);
238            prop_assert!(
239                matches!(mention_result, CooldownCheckResult::Allowed),
240                "Mention should be allowed after its cooldown expires, got {:?}",
241                mention_result
242            );
243
244            // Keyword 应该还在冷却中
245            let keyword_result = tracker.check_cooldown(&user_id, TriggerType::Keyword);
246            prop_assert!(
247                matches!(keyword_result, CooldownCheckResult::InCooldown { .. }),
248                "Keyword should still be in cooldown, got {:?}",
249                keyword_result
250            );
251        }
252
253        /// Property 4.5: 重置冷却后允许立即重新触发
254        /// **Validates: Requirement 4.5**
255        #[test]
256        fn prop_reset_cooldown_allows_immediate_retrigger(
257            user_id in arb_user_id(),
258            cooldown_ms in 100u64..1000,
259            trigger_type in arb_trigger_type()
260        ) {
261            let mut tracker = CooldownTracker::new(Duration::from_millis(cooldown_ms));
262
263            // 记录触发
264            tracker.record_trigger(&user_id);
265
266            // 确认在冷却中
267            let result_before = tracker.check_cooldown(&user_id, trigger_type);
268            prop_assert!(
269                matches!(result_before, CooldownCheckResult::InCooldown { .. }),
270                "Should be in cooldown before reset"
271            );
272
273            // 重置冷却
274            tracker.reset_cooldown(&user_id);
275
276            // 重置后应该被允许
277            let result_after = tracker.check_cooldown(&user_id, trigger_type);
278            prop_assert!(
279                matches!(result_after, CooldownCheckResult::Allowed),
280                "Should be allowed after reset, got {:?}",
281                result_after
282            );
283        }
284
285        /// Property 4.6: 多用户冷却独立
286        /// **Validates: Requirement 4.1**
287        #[test]
288        fn prop_independent_user_cooldowns(
289            user1 in arb_user_id(),
290            user2 in arb_user_id(),
291            cooldown_ms in 100u64..1000,
292            trigger_type in arb_trigger_type()
293        ) {
294            // 确保两个用户不同
295            prop_assume!(user1 != user2);
296
297            let mut tracker = CooldownTracker::new(Duration::from_millis(cooldown_ms));
298
299            // 只记录 user1 的触发
300            tracker.record_trigger(&user1);
301
302            // user1 应该在冷却中
303            let result1 = tracker.check_cooldown(&user1, trigger_type);
304            prop_assert!(
305                matches!(result1, CooldownCheckResult::InCooldown { .. }),
306                "User1 should be in cooldown"
307            );
308
309            // user2 应该被允许(没有触发记录)
310            let result2 = tracker.check_cooldown(&user2, trigger_type);
311            prop_assert!(
312                matches!(result2, CooldownCheckResult::Allowed),
313                "User2 should be allowed, got {:?}",
314                result2
315            );
316        }
317
318        /// Property 4.7: 冷却时间过期后允许触发
319        /// **Validates: Requirements 4.2, 4.5**
320        #[test]
321        fn prop_cooldown_expires_allows_trigger(
322            user_id in arb_user_id(),
323            trigger_type in arb_trigger_type()
324        ) {
325            // 使用非常短的冷却时间
326            let cooldown = Duration::from_millis(20);
327            let mut tracker = CooldownTracker::new(cooldown);
328
329            // 记录触发
330            tracker.record_trigger(&user_id);
331
332            // 立即检查应该在冷却中
333            let result_immediate = tracker.check_cooldown(&user_id, trigger_type);
334            prop_assert!(
335                matches!(result_immediate, CooldownCheckResult::InCooldown { .. }),
336                "Should be in cooldown immediately"
337            );
338
339            // 等待冷却过期
340            thread::sleep(Duration::from_millis(30));
341
342            // 现在应该被允许
343            let result_after = tracker.check_cooldown(&user_id, trigger_type);
344            prop_assert!(
345                matches!(result_after, CooldownCheckResult::Allowed),
346                "Should be allowed after cooldown expires, got {:?}",
347                result_after
348            );
349        }
350
351        /// Property 4.8: 获取的类型冷却时间与设置一致
352        /// **Validates: Requirements 4.3, 4.4**
353        #[test]
354        fn prop_get_type_cooldown_consistent(
355            default_ms in arb_cooldown_ms(),
356            type_ms in arb_cooldown_ms(),
357            trigger_type in arb_trigger_type()
358        ) {
359            let default_cooldown = Duration::from_millis(default_ms);
360            let type_cooldown = Duration::from_millis(type_ms);
361
362            let mut tracker = CooldownTracker::new(default_cooldown);
363
364            // 未设置类型冷却时,应返回默认值
365            prop_assert_eq!(
366                tracker.get_type_cooldown(trigger_type),
367                default_cooldown,
368                "Should return default cooldown when type not set"
369            );
370
371            // 设置类型冷却后,应返回设置的值
372            tracker.set_type_cooldown(trigger_type, type_cooldown);
373            prop_assert_eq!(
374                tracker.get_type_cooldown(trigger_type),
375                type_cooldown,
376                "Should return set cooldown for type"
377            );
378        }
379
380        /// Property 4.9: 触发记录状态一致性
381        /// **Validates: Requirement 4.1**
382        #[test]
383        fn prop_trigger_record_consistency(
384            user_id in arb_user_id(),
385            cooldown_ms in arb_cooldown_ms()
386        ) {
387            let mut tracker = CooldownTracker::new(Duration::from_millis(cooldown_ms));
388
389            // 初始状态:无触发记录
390            prop_assert!(
391                !tracker.has_trigger_record(&user_id),
392                "Should not have trigger record initially"
393            );
394
395            // 记录触发后:有触发记录
396            tracker.record_trigger(&user_id);
397            prop_assert!(
398                tracker.has_trigger_record(&user_id),
399                "Should have trigger record after recording"
400            );
401
402            // 重置后:无触发记录
403            tracker.reset_cooldown(&user_id);
404            prop_assert!(
405                !tracker.has_trigger_record(&user_id),
406                "Should not have trigger record after reset"
407            );
408        }
409    }
410
411    // ============================================================
412    // 单元测试:验证 Requirements 4.1-4.6
413    // ============================================================
414
415    /// Requirement 4.1: THE Auto_Reply_Manager SHALL track last trigger time per user
416    #[test]
417    fn test_track_last_trigger_time_per_user() {
418        let mut tracker = CooldownTracker::new(Duration::from_secs(60));
419
420        // 初始状态:没有触发记录
421        assert!(!tracker.has_trigger_record("user1"));
422        assert!(!tracker.has_trigger_record("user2"));
423
424        // 记录 user1 的触发
425        tracker.record_trigger("user1");
426        assert!(tracker.has_trigger_record("user1"));
427        assert!(!tracker.has_trigger_record("user2"));
428
429        // 记录 user2 的触发
430        tracker.record_trigger("user2");
431        assert!(tracker.has_trigger_record("user1"));
432        assert!(tracker.has_trigger_record("user2"));
433    }
434
435    /// Requirement 4.2: WHEN a user triggers within cooldown period,
436    /// THE Auto_Reply_Manager SHALL reject the trigger
437    #[test]
438    fn test_reject_trigger_within_cooldown() {
439        let mut tracker = CooldownTracker::new(Duration::from_secs(60));
440
441        // 记录触发
442        tracker.record_trigger("user1");
443
444        // 立即检查应该被拒绝(在冷却期内)
445        let result = tracker.check_cooldown("user1", TriggerType::Mention);
446        match result {
447            CooldownCheckResult::InCooldown { remaining } => {
448                // 剩余时间应该接近 60 秒
449                assert!(remaining.as_secs() <= 60);
450                assert!(remaining.as_secs() >= 59);
451            }
452            CooldownCheckResult::Allowed => {
453                panic!("Should be in cooldown");
454            }
455        }
456    }
457
458    /// Requirement 4.3: THE Auto_Reply_Manager SHALL support configurable cooldown duration
459    #[test]
460    fn test_configurable_cooldown_duration() {
461        // 测试不同的默认冷却时间
462        let tracker_short = CooldownTracker::new(Duration::from_secs(10));
463        let tracker_long = CooldownTracker::new(Duration::from_secs(300));
464
465        assert_eq!(tracker_short.default_cooldown(), Duration::from_secs(10));
466        assert_eq!(tracker_long.default_cooldown(), Duration::from_secs(300));
467    }
468
469    /// Requirement 4.4: THE Auto_Reply_Manager SHALL support per-trigger-type cooldown settings
470    #[test]
471    fn test_per_trigger_type_cooldown() {
472        let mut tracker = CooldownTracker::new(Duration::from_secs(60));
473
474        // 设置不同触发类型的冷却时间
475        tracker.set_type_cooldown(TriggerType::Mention, Duration::from_secs(30));
476        tracker.set_type_cooldown(TriggerType::Keyword, Duration::from_secs(120));
477
478        // 验证不同类型使用不同的冷却时间
479        assert_eq!(
480            tracker.get_type_cooldown(TriggerType::Mention),
481            Duration::from_secs(30)
482        );
483        assert_eq!(
484            tracker.get_type_cooldown(TriggerType::Keyword),
485            Duration::from_secs(120)
486        );
487        // 未设置的类型使用默认值
488        assert_eq!(
489            tracker.get_type_cooldown(TriggerType::DirectMessage),
490            Duration::from_secs(60)
491        );
492    }
493
494    /// Requirement 4.5: WHEN cooldown expires, THE Auto_Reply_Manager SHALL allow the user to trigger again
495    #[test]
496    fn test_allow_after_cooldown_expires() {
497        // 使用非常短的冷却时间进行测试
498        let mut tracker = CooldownTracker::new(Duration::from_millis(50));
499
500        // 记录触发
501        tracker.record_trigger("user1");
502
503        // 立即检查应该被拒绝
504        let result = tracker.check_cooldown("user1", TriggerType::Mention);
505        assert!(matches!(result, CooldownCheckResult::InCooldown { .. }));
506
507        // 等待冷却时间过期
508        thread::sleep(Duration::from_millis(60));
509
510        // 现在应该允许
511        let result = tracker.check_cooldown("user1", TriggerType::Mention);
512        assert!(matches!(result, CooldownCheckResult::Allowed));
513    }
514
515    /// Requirement 4.6: THE Auto_Reply_Manager SHALL provide remaining cooldown time in rejection response
516    #[test]
517    fn test_remaining_cooldown_time_in_rejection() {
518        let mut tracker = CooldownTracker::new(Duration::from_secs(60));
519
520        // 记录触发
521        tracker.record_trigger("user1");
522
523        // 检查冷却状态
524        let result = tracker.check_cooldown("user1", TriggerType::Mention);
525        match result {
526            CooldownCheckResult::InCooldown { remaining } => {
527                // 验证返回了剩余时间
528                assert!(remaining > Duration::ZERO);
529                assert!(remaining <= Duration::from_secs(60));
530            }
531            CooldownCheckResult::Allowed => {
532                panic!("Should be in cooldown with remaining time");
533            }
534        }
535    }
536
537    // ============================================================
538    // 额外单元测试:边界情况和辅助方法
539    // ============================================================
540
541    /// 测试新用户(无触发记录)应该被允许
542    #[test]
543    fn test_new_user_allowed() {
544        let tracker = CooldownTracker::new(Duration::from_secs(60));
545
546        // 新用户应该被允许
547        let result = tracker.check_cooldown("new_user", TriggerType::Mention);
548        assert!(matches!(result, CooldownCheckResult::Allowed));
549    }
550
551    /// 测试重置冷却功能
552    #[test]
553    fn test_reset_cooldown() {
554        let mut tracker = CooldownTracker::new(Duration::from_secs(60));
555
556        // 记录触发
557        tracker.record_trigger("user1");
558        assert!(tracker.has_trigger_record("user1"));
559
560        // 重置冷却
561        tracker.reset_cooldown("user1");
562        assert!(!tracker.has_trigger_record("user1"));
563
564        // 重置后应该被允许
565        let result = tracker.check_cooldown("user1", TriggerType::Mention);
566        assert!(matches!(result, CooldownCheckResult::Allowed));
567    }
568
569    /// 测试清理过期记录
570    #[test]
571    fn test_cleanup_expired() {
572        let mut tracker = CooldownTracker::new(Duration::from_millis(10));
573
574        // 记录多个用户的触发
575        tracker.record_trigger("user1");
576        tracker.record_trigger("user2");
577
578        // 等待记录过期
579        thread::sleep(Duration::from_millis(30));
580
581        // 清理过期记录
582        tracker.cleanup_expired();
583
584        // 过期记录应该被清理
585        assert!(!tracker.has_trigger_record("user1"));
586        assert!(!tracker.has_trigger_record("user2"));
587    }
588
589    /// 测试多用户独立冷却
590    #[test]
591    fn test_independent_user_cooldowns() {
592        let mut tracker = CooldownTracker::new(Duration::from_secs(60));
593
594        // user1 触发
595        tracker.record_trigger("user1");
596
597        // user1 在冷却中
598        let result1 = tracker.check_cooldown("user1", TriggerType::Mention);
599        assert!(matches!(result1, CooldownCheckResult::InCooldown { .. }));
600
601        // user2 没有触发过,应该被允许
602        let result2 = tracker.check_cooldown("user2", TriggerType::Mention);
603        assert!(matches!(result2, CooldownCheckResult::Allowed));
604    }
605
606    /// 测试不同触发类型使用不同冷却时间
607    #[test]
608    fn test_different_cooldown_per_type() {
609        let mut tracker = CooldownTracker::new(Duration::from_millis(100));
610        tracker.set_type_cooldown(TriggerType::Mention, Duration::from_millis(20));
611        tracker.set_type_cooldown(TriggerType::Keyword, Duration::from_millis(200));
612
613        // 记录触发
614        tracker.record_trigger("user1");
615
616        // 等待 Mention 冷却过期但 Keyword 还在冷却中
617        thread::sleep(Duration::from_millis(30));
618
619        // Mention 应该被允许(冷却时间 20ms 已过)
620        let result_mention = tracker.check_cooldown("user1", TriggerType::Mention);
621        assert!(matches!(result_mention, CooldownCheckResult::Allowed));
622
623        // Keyword 应该还在冷却中(冷却时间 200ms)
624        let result_keyword = tracker.check_cooldown("user1", TriggerType::Keyword);
625        assert!(matches!(
626            result_keyword,
627            CooldownCheckResult::InCooldown { .. }
628        ));
629    }
630
631    /// 测试 CooldownCheckResult 的 Debug 实现
632    #[test]
633    fn test_cooldown_check_result_debug() {
634        let allowed = CooldownCheckResult::Allowed;
635        let in_cooldown = CooldownCheckResult::InCooldown {
636            remaining: Duration::from_secs(30),
637        };
638
639        // 验证 Debug 实现不会 panic
640        let _ = format!("{:?}", allowed);
641        let _ = format!("{:?}", in_cooldown);
642    }
643
644    /// 测试 CooldownCheckResult 的 Clone 实现
645    #[test]
646    fn test_cooldown_check_result_clone() {
647        let original = CooldownCheckResult::InCooldown {
648            remaining: Duration::from_secs(30),
649        };
650        let cloned = original.clone();
651
652        match cloned {
653            CooldownCheckResult::InCooldown { remaining } => {
654                assert_eq!(remaining, Duration::from_secs(30));
655            }
656            _ => panic!("Clone should preserve variant"),
657        }
658    }
659}