Skip to main content

aster/auto_reply/
schedule.rs

1//! Scheduler 集成模块
2//!
3//! 本模块实现自动回复系统与 Scheduler 的集成,支持定时触发自动回复。
4//!
5//! # 功能
6//!
7//! - 支持 Cron 表达式调度
8//! - 支持一次性定时 (At) 调度
9//! - 支持固定间隔 (Every) 调度
10//! - 创建触发事件并传递上下文给 Agent
11//!
12//! # 需求映射
13//!
14//! - **Requirement 8.1**: 集成现有 Scheduler 模块
15//! - **Requirement 8.2**: Schedule 触发时创建触发事件
16//! - **Requirement 8.3**: 支持 Cron 表达式配置
17//! - **Requirement 8.4**: 支持一次性 (At) 调度
18//! - **Requirement 8.5**: 支持间隔 (Every) 调度
19//! - **Requirement 8.6**: 传递 schedule 上下文给 Agent
20
21use std::collections::HashMap;
22use std::sync::Arc;
23
24use chrono::{DateTime, Utc};
25use serde::{Deserialize, Serialize};
26use tokio::sync::RwLock;
27
28use crate::auto_reply::message::{IncomingMessage, TriggerContext, TriggerResult};
29use crate::auto_reply::registry::AutoReplyTrigger;
30use crate::auto_reply::types::{ScheduleTriggerConfig, ScheduleType, TriggerConfig, TriggerType};
31
32/// Schedule 触发事件
33///
34/// 当定时触发器触发时创建的事件,包含触发的上下文信息。
35///
36/// **Validates: Requirement 8.2**
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ScheduleTriggerEvent {
39    /// 触发器 ID
40    pub trigger_id: String,
41    /// 调度类型
42    pub schedule_type: ScheduleType,
43    /// 触发时间
44    pub triggered_at: DateTime<Utc>,
45    /// 下次触发时间(如果有)
46    pub next_trigger_at: Option<DateTime<Utc>>,
47}
48
49/// Schedule 上下文
50///
51/// 传递给 Agent 的调度上下文信息。
52///
53/// **Validates: Requirement 8.6**
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ScheduleContext {
56    /// 触发器 ID
57    pub trigger_id: String,
58    /// 触发器名称
59    pub trigger_name: String,
60    /// 调度类型描述
61    pub schedule_description: String,
62    /// 触发时间
63    pub triggered_at: DateTime<Utc>,
64    /// 是否为首次触发
65    pub is_first_trigger: bool,
66    /// 上次触发时间(如果有)
67    pub last_triggered_at: Option<DateTime<Utc>>,
68    /// 附加元数据
69    #[serde(default)]
70    pub metadata: HashMap<String, serde_json::Value>,
71}
72
73/// 已注册的调度触发器状态
74#[derive(Debug, Clone)]
75struct RegisteredSchedule {
76    /// 触发器配置
77    trigger: AutoReplyTrigger,
78    /// 调度配置
79    schedule_config: ScheduleTriggerConfig,
80    /// 下次触发时间
81    next_trigger_at: Option<DateTime<Utc>>,
82    /// 上次触发时间
83    last_triggered_at: Option<DateTime<Utc>>,
84    /// 是否已触发过
85    has_triggered: bool,
86}
87
88/// Schedule 触发处理器
89///
90/// 管理定时触发器,支持 Cron、At、Every 三种调度类型。
91///
92/// # 功能
93///
94/// - 注册/注销调度触发器
95/// - 计算下次触发时间
96/// - 检查并触发到期的调度
97/// - 创建触发事件和上下文
98///
99/// # 需求映射
100///
101/// - **Requirement 8.1**: 集成现有 Scheduler 模块
102/// - **Requirement 8.2**: Schedule 触发时创建触发事件
103/// - **Requirement 8.3**: 支持 Cron 表达式配置
104/// - **Requirement 8.4**: 支持一次性 (At) 调度
105/// - **Requirement 8.5**: 支持间隔 (Every) 调度
106/// - **Requirement 8.6**: 传递 schedule 上下文给 Agent
107pub struct ScheduleTriggerHandler {
108    /// 已注册的调度触发器
109    schedules: Arc<RwLock<HashMap<String, RegisteredSchedule>>>,
110}
111
112impl Default for ScheduleTriggerHandler {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118impl ScheduleTriggerHandler {
119    /// 创建新的 Schedule 触发处理器
120    ///
121    /// **Validates: Requirement 8.1**
122    pub fn new() -> Self {
123        Self {
124            schedules: Arc::new(RwLock::new(HashMap::new())),
125        }
126    }
127
128    /// 注册调度触发器
129    ///
130    /// # 参数
131    /// - `trigger`: 自动回复触发器配置
132    ///
133    /// # 返回值
134    /// - `Ok(())`: 注册成功
135    /// - `Err(String)`: 注册失败(配置无效或类型不匹配)
136    ///
137    /// **Validates: Requirements 8.1, 8.3, 8.4, 8.5**
138    pub async fn register(&self, trigger: AutoReplyTrigger) -> Result<(), String> {
139        // 验证触发器类型
140        if trigger.trigger_type != TriggerType::Schedule {
141            return Err(format!("触发器 {} 不是 Schedule 类型", trigger.id));
142        }
143
144        // 提取调度配置
145        let schedule_config = match &trigger.config {
146            TriggerConfig::Schedule(config) => config.clone(),
147            _ => {
148                return Err(format!("触发器 {} 配置类型不匹配", trigger.id));
149            }
150        };
151
152        // 验证调度配置
153        self.validate_schedule_type(&schedule_config.schedule_type)?;
154
155        // 计算下次触发时间
156        let now = Utc::now();
157        let next_trigger_at = self.calculate_next_trigger(&schedule_config.schedule_type, now);
158
159        let registered = RegisteredSchedule {
160            trigger,
161            schedule_config,
162            next_trigger_at,
163            last_triggered_at: None,
164            has_triggered: false,
165        };
166
167        let trigger_id = registered.trigger.id.clone();
168        let mut schedules = self.schedules.write().await;
169        schedules.insert(trigger_id.clone(), registered);
170
171        tracing::info!("已注册调度触发器: {}", trigger_id);
172        Ok(())
173    }
174
175    /// 注销调度触发器
176    ///
177    /// # 参数
178    /// - `trigger_id`: 触发器 ID
179    ///
180    /// # 返回值
181    /// - `Some(AutoReplyTrigger)`: 被移除的触发器
182    /// - `None`: 触发器不存在
183    pub async fn unregister(&self, trigger_id: &str) -> Option<AutoReplyTrigger> {
184        let mut schedules = self.schedules.write().await;
185        schedules.remove(trigger_id).map(|s| {
186            tracing::info!("已注销调度触发器: {}", trigger_id);
187            s.trigger
188        })
189    }
190
191    /// 验证调度类型配置
192    ///
193    /// **Validates: Requirements 8.3, 8.4, 8.5**
194    fn validate_schedule_type(&self, schedule_type: &ScheduleType) -> Result<(), String> {
195        match schedule_type {
196            ScheduleType::Cron { expr, timezone } => {
197                // 验证 Cron 表达式 (Requirement 8.3)
198                if expr.is_empty() {
199                    return Err("Cron 表达式不能为空".to_string());
200                }
201                // 验证时区(如果提供)
202                if let Some(tz) = timezone {
203                    if tz.parse::<chrono_tz::Tz>().is_err() {
204                        return Err(format!("无效的时区: {}", tz));
205                    }
206                }
207                // 验证 cron 表达式格式
208                if cron::Schedule::from_str(expr).is_err() {
209                    return Err(format!("无效的 Cron 表达式: {}", expr));
210                }
211                Ok(())
212            }
213            ScheduleType::At { at_ms } => {
214                // 验证一次性定时 (Requirement 8.4)
215                if *at_ms <= 0 {
216                    return Err("At 调度时间戳必须为正数".to_string());
217                }
218                Ok(())
219            }
220            ScheduleType::Every { every_ms } => {
221                // 验证固定间隔 (Requirement 8.5)
222                if *every_ms == 0 {
223                    return Err("Every 间隔必须大于 0".to_string());
224                }
225                Ok(())
226            }
227        }
228    }
229
230    /// 计算下次触发时间
231    ///
232    /// **Validates: Requirements 8.3, 8.4, 8.5**
233    fn calculate_next_trigger(
234        &self,
235        schedule_type: &ScheduleType,
236        now: DateTime<Utc>,
237    ) -> Option<DateTime<Utc>> {
238        match schedule_type {
239            ScheduleType::Cron { expr, timezone } => {
240                // Cron 表达式调度 (Requirement 8.3)
241                self.next_cron_trigger(expr, timezone.as_deref(), now)
242            }
243            ScheduleType::At { at_ms } => {
244                // 一次性定时 (Requirement 8.4)
245                let at_time = DateTime::from_timestamp_millis(*at_ms)?;
246                if at_time > now {
247                    Some(at_time)
248                } else {
249                    None // 已过期
250                }
251            }
252            ScheduleType::Every { every_ms } => {
253                // 固定间隔 (Requirement 8.5)
254                if *every_ms == 0 {
255                    return None;
256                }
257                let next = now + chrono::Duration::milliseconds(*every_ms as i64);
258                Some(next)
259            }
260        }
261    }
262
263    /// 计算 Cron 表达式的下次触发时间
264    ///
265    /// **Validates: Requirement 8.3**
266    fn next_cron_trigger(
267        &self,
268        expr: &str,
269        timezone: Option<&str>,
270        now: DateTime<Utc>,
271    ) -> Option<DateTime<Utc>> {
272        use cron::Schedule;
273        use std::str::FromStr;
274
275        let schedule = Schedule::from_str(expr).ok()?;
276        let tz: chrono_tz::Tz = timezone
277            .and_then(|s| s.parse().ok())
278            .unwrap_or(chrono_tz::UTC);
279
280        let now_in_tz = now.with_timezone(&tz);
281        schedule
282            .after(&now_in_tz)
283            .next()
284            .map(|dt| dt.with_timezone(&Utc))
285    }
286
287    /// 检查并获取到期的触发器
288    ///
289    /// 返回所有已到期需要触发的调度,并更新其状态。
290    ///
291    /// **Validates: Requirement 8.2**
292    pub async fn check_and_fire(&self) -> Vec<ScheduleTriggerEvent> {
293        let now = Utc::now();
294        let mut events = Vec::new();
295        let mut schedules = self.schedules.write().await;
296
297        for (trigger_id, schedule) in schedules.iter_mut() {
298            // 跳过禁用的触发器
299            if !schedule.trigger.enabled {
300                continue;
301            }
302
303            // 检查是否到期
304            if let Some(next_at) = schedule.next_trigger_at {
305                if next_at <= now {
306                    // 创建触发事件 (Requirement 8.2)
307                    let event = ScheduleTriggerEvent {
308                        trigger_id: trigger_id.clone(),
309                        schedule_type: schedule.schedule_config.schedule_type.clone(),
310                        triggered_at: now,
311                        next_trigger_at: None, // 稍后计算
312                    };
313
314                    // 更新状态
315                    schedule.last_triggered_at = Some(now);
316                    schedule.has_triggered = true;
317
318                    // 计算下次触发时间
319                    let next =
320                        self.calculate_next_trigger(&schedule.schedule_config.schedule_type, now);
321                    schedule.next_trigger_at = next;
322
323                    let mut event = event;
324                    event.next_trigger_at = next;
325                    events.push(event);
326                }
327            }
328        }
329
330        events
331    }
332
333    /// 创建触发结果
334    ///
335    /// 根据触发事件创建 TriggerResult,用于与 AutoReplyManager 集成。
336    ///
337    /// **Validates: Requirements 8.2, 8.6**
338    pub async fn create_trigger_result(
339        &self,
340        event: &ScheduleTriggerEvent,
341    ) -> Option<TriggerResult> {
342        let schedules = self.schedules.read().await;
343        let schedule = schedules.get(&event.trigger_id)?;
344
345        // 创建虚拟的入站消息(用于 Schedule 触发)
346        let message = IncomingMessage {
347            id: format!(
348                "schedule-{}-{}",
349                event.trigger_id,
350                event.triggered_at.timestamp_millis()
351            ),
352            sender_id: "system".to_string(),
353            sender_name: Some("Scheduler".to_string()),
354            content: format!(
355                "[定时触发] {} - {}",
356                schedule.trigger.name,
357                self.describe_schedule_type(&schedule.schedule_config.schedule_type)
358            ),
359            channel: "schedule".to_string(),
360            group_id: None,
361            is_direct_message: false,
362            mentions_bot: false,
363            timestamp: event.triggered_at,
364            metadata: HashMap::new(),
365        };
366
367        // 创建触发上下文 (Requirement 8.6)
368        let mut extra = HashMap::new();
369        extra.insert(
370            "schedule_type".to_string(),
371            serde_json::to_value(&event.schedule_type).unwrap_or_default(),
372        );
373        if let Some(next) = event.next_trigger_at {
374            extra.insert(
375                "next_trigger_at".to_string(),
376                serde_json::Value::String(next.to_rfc3339()),
377            );
378        }
379
380        let context = TriggerContext {
381            trigger_id: event.trigger_id.clone(),
382            trigger_type: TriggerType::Schedule,
383            message,
384            match_details: None,
385            triggered_at: event.triggered_at,
386            extra,
387        };
388
389        Some(TriggerResult::Triggered {
390            trigger: Box::new(schedule.trigger.clone()),
391            context: Box::new(context),
392        })
393    }
394
395    /// 创建 Schedule 上下文
396    ///
397    /// 创建传递给 Agent 的调度上下文信息。
398    ///
399    /// **Validates: Requirement 8.6**
400    pub async fn create_schedule_context(
401        &self,
402        event: &ScheduleTriggerEvent,
403    ) -> Option<ScheduleContext> {
404        let schedules = self.schedules.read().await;
405        let schedule = schedules.get(&event.trigger_id)?;
406
407        Some(ScheduleContext {
408            trigger_id: event.trigger_id.clone(),
409            trigger_name: schedule.trigger.name.clone(),
410            schedule_description: self
411                .describe_schedule_type(&schedule.schedule_config.schedule_type),
412            triggered_at: event.triggered_at,
413            is_first_trigger: !schedule.has_triggered,
414            last_triggered_at: schedule.last_triggered_at,
415            metadata: HashMap::new(),
416        })
417    }
418
419    /// 描述调度类型
420    fn describe_schedule_type(&self, schedule_type: &ScheduleType) -> String {
421        match schedule_type {
422            ScheduleType::Cron { expr, timezone } => {
423                let tz_info = timezone
424                    .as_ref()
425                    .map(|tz| format!(" ({})", tz))
426                    .unwrap_or_default();
427                format!("Cron: {}{}", expr, tz_info)
428            }
429            ScheduleType::At { at_ms } => {
430                if let Some(dt) = DateTime::from_timestamp_millis(*at_ms) {
431                    format!("一次性: {}", dt.format("%Y-%m-%d %H:%M:%S UTC"))
432                } else {
433                    format!("一次性: {}ms", at_ms)
434                }
435            }
436            ScheduleType::Every { every_ms } => {
437                let duration = format_duration(*every_ms);
438                format!("每隔: {}", duration)
439            }
440        }
441    }
442
443    /// 获取所有已注册的调度触发器
444    pub async fn list_schedules(&self) -> Vec<(String, ScheduleType, Option<DateTime<Utc>>)> {
445        let schedules = self.schedules.read().await;
446        schedules
447            .iter()
448            .map(|(id, s)| {
449                (
450                    id.clone(),
451                    s.schedule_config.schedule_type.clone(),
452                    s.next_trigger_at,
453                )
454            })
455            .collect()
456    }
457
458    /// 获取指定触发器的下次触发时间
459    pub async fn get_next_trigger_time(&self, trigger_id: &str) -> Option<DateTime<Utc>> {
460        let schedules = self.schedules.read().await;
461        schedules.get(trigger_id).and_then(|s| s.next_trigger_at)
462    }
463
464    /// 检查触发器是否已注册
465    pub async fn is_registered(&self, trigger_id: &str) -> bool {
466        let schedules = self.schedules.read().await;
467        schedules.contains_key(trigger_id)
468    }
469
470    /// 获取已注册的触发器数量
471    pub async fn count(&self) -> usize {
472        let schedules = self.schedules.read().await;
473        schedules.len()
474    }
475}
476
477/// 格式化毫秒为可读的时间间隔
478fn format_duration(ms: u64) -> String {
479    let seconds = ms / 1000;
480    let minutes = seconds / 60;
481    let hours = minutes / 60;
482    let days = hours / 24;
483
484    if days > 0 {
485        format!("{}天{}小时", days, hours % 24)
486    } else if hours > 0 {
487        format!("{}小时{}分钟", hours, minutes % 60)
488    } else if minutes > 0 {
489        format!("{}分钟{}秒", minutes, seconds % 60)
490    } else if seconds > 0 {
491        format!("{}秒", seconds)
492    } else {
493        format!("{}毫秒", ms)
494    }
495}
496
497// 需要引入 std::str::FromStr(cron::Schedule 在方法内部使用)
498use std::str::FromStr;
499
500#[cfg(test)]
501mod tests {
502    use super::*;
503
504    // ========== 辅助函数测试 ==========
505
506    #[test]
507    fn test_format_duration_milliseconds() {
508        assert_eq!(format_duration(500), "500毫秒");
509        assert_eq!(format_duration(999), "999毫秒");
510    }
511
512    #[test]
513    fn test_format_duration_seconds() {
514        assert_eq!(format_duration(1000), "1秒");
515        assert_eq!(format_duration(30000), "30秒");
516        assert_eq!(format_duration(59000), "59秒");
517    }
518
519    #[test]
520    fn test_format_duration_minutes() {
521        assert_eq!(format_duration(60000), "1分钟0秒");
522        assert_eq!(format_duration(90000), "1分钟30秒");
523        assert_eq!(format_duration(3600000 - 1000), "59分钟59秒");
524    }
525
526    #[test]
527    fn test_format_duration_hours() {
528        assert_eq!(format_duration(3600000), "1小时0分钟");
529        assert_eq!(format_duration(5400000), "1小时30分钟");
530    }
531
532    #[test]
533    fn test_format_duration_days() {
534        assert_eq!(format_duration(86400000), "1天0小时");
535        assert_eq!(format_duration(90000000), "1天1小时");
536    }
537
538    // ========== ScheduleTriggerHandler 测试 ==========
539
540    fn create_cron_trigger(id: &str, expr: &str) -> AutoReplyTrigger {
541        AutoReplyTrigger {
542            id: id.to_string(),
543            name: format!("Cron Trigger {}", id),
544            enabled: true,
545            trigger_type: TriggerType::Schedule,
546            config: TriggerConfig::Schedule(ScheduleTriggerConfig {
547                schedule_type: ScheduleType::Cron {
548                    expr: expr.to_string(),
549                    timezone: None,
550                },
551            }),
552            priority: 10,
553            response_template: None,
554        }
555    }
556
557    fn create_at_trigger(id: &str, at_ms: i64) -> AutoReplyTrigger {
558        AutoReplyTrigger {
559            id: id.to_string(),
560            name: format!("At Trigger {}", id),
561            enabled: true,
562            trigger_type: TriggerType::Schedule,
563            config: TriggerConfig::Schedule(ScheduleTriggerConfig {
564                schedule_type: ScheduleType::At { at_ms },
565            }),
566            priority: 10,
567            response_template: None,
568        }
569    }
570
571    fn create_every_trigger(id: &str, every_ms: u64) -> AutoReplyTrigger {
572        AutoReplyTrigger {
573            id: id.to_string(),
574            name: format!("Every Trigger {}", id),
575            enabled: true,
576            trigger_type: TriggerType::Schedule,
577            config: TriggerConfig::Schedule(ScheduleTriggerConfig {
578                schedule_type: ScheduleType::Every { every_ms },
579            }),
580            priority: 10,
581            response_template: None,
582        }
583    }
584
585    #[tokio::test]
586    async fn test_handler_new() {
587        let handler = ScheduleTriggerHandler::new();
588        assert_eq!(handler.count().await, 0);
589    }
590
591    #[tokio::test]
592    async fn test_handler_default() {
593        let handler = ScheduleTriggerHandler::default();
594        assert_eq!(handler.count().await, 0);
595    }
596
597    /// **Validates: Requirement 8.3** - 支持 Cron 表达式配置
598    #[tokio::test]
599    async fn test_register_cron_trigger() {
600        let handler = ScheduleTriggerHandler::new();
601        let trigger = create_cron_trigger("cron-1", "0 0 * * * *"); // 每小时
602
603        let result = handler.register(trigger).await;
604        assert!(result.is_ok());
605        assert!(handler.is_registered("cron-1").await);
606        assert_eq!(handler.count().await, 1);
607    }
608
609    /// **Validates: Requirement 8.4** - 支持一次性 (At) 调度
610    #[tokio::test]
611    async fn test_register_at_trigger() {
612        let handler = ScheduleTriggerHandler::new();
613        // 设置为未来时间
614        let future_ms = Utc::now().timestamp_millis() + 3600000; // 1小时后
615        let trigger = create_at_trigger("at-1", future_ms);
616
617        let result = handler.register(trigger).await;
618        assert!(result.is_ok());
619        assert!(handler.is_registered("at-1").await);
620    }
621
622    /// **Validates: Requirement 8.5** - 支持间隔 (Every) 调度
623    #[tokio::test]
624    async fn test_register_every_trigger() {
625        let handler = ScheduleTriggerHandler::new();
626        let trigger = create_every_trigger("every-1", 60000); // 每分钟
627
628        let result = handler.register(trigger).await;
629        assert!(result.is_ok());
630        assert!(handler.is_registered("every-1").await);
631    }
632
633    #[tokio::test]
634    async fn test_register_invalid_trigger_type() {
635        let handler = ScheduleTriggerHandler::new();
636        // 创建一个非 Schedule 类型的触发器
637        let trigger = AutoReplyTrigger {
638            id: "mention-1".to_string(),
639            name: "Mention Trigger".to_string(),
640            enabled: true,
641            trigger_type: TriggerType::Mention,
642            config: TriggerConfig::Mention,
643            priority: 10,
644            response_template: None,
645        };
646
647        let result = handler.register(trigger).await;
648        assert!(result.is_err());
649        assert!(result.unwrap_err().contains("不是 Schedule 类型"));
650    }
651
652    #[tokio::test]
653    async fn test_register_invalid_cron_expression() {
654        let handler = ScheduleTriggerHandler::new();
655        let trigger = create_cron_trigger("invalid-cron", "invalid cron");
656
657        let result = handler.register(trigger).await;
658        assert!(result.is_err());
659        assert!(result.unwrap_err().contains("无效的 Cron 表达式"));
660    }
661
662    #[tokio::test]
663    async fn test_register_invalid_every_interval() {
664        let handler = ScheduleTriggerHandler::new();
665        let trigger = create_every_trigger("invalid-every", 0);
666
667        let result = handler.register(trigger).await;
668        assert!(result.is_err());
669        assert!(result.unwrap_err().contains("间隔必须大于 0"));
670    }
671
672    #[tokio::test]
673    async fn test_unregister_existing_trigger() {
674        let handler = ScheduleTriggerHandler::new();
675        let trigger = create_every_trigger("every-1", 60000);
676        handler.register(trigger).await.unwrap();
677
678        let removed = handler.unregister("every-1").await;
679        assert!(removed.is_some());
680        assert_eq!(removed.unwrap().id, "every-1");
681        assert!(!handler.is_registered("every-1").await);
682    }
683
684    #[tokio::test]
685    async fn test_unregister_nonexistent_trigger() {
686        let handler = ScheduleTriggerHandler::new();
687        let removed = handler.unregister("nonexistent").await;
688        assert!(removed.is_none());
689    }
690
691    #[tokio::test]
692    async fn test_list_schedules() {
693        let handler = ScheduleTriggerHandler::new();
694        handler
695            .register(create_cron_trigger("cron-1", "0 0 * * * *"))
696            .await
697            .unwrap();
698        handler
699            .register(create_every_trigger("every-1", 60000))
700            .await
701            .unwrap();
702
703        let schedules = handler.list_schedules().await;
704        assert_eq!(schedules.len(), 2);
705    }
706
707    #[tokio::test]
708    async fn test_get_next_trigger_time() {
709        let handler = ScheduleTriggerHandler::new();
710        let trigger = create_every_trigger("every-1", 60000);
711        handler.register(trigger).await.unwrap();
712
713        let next = handler.get_next_trigger_time("every-1").await;
714        assert!(next.is_some());
715    }
716
717    /// **Validates: Requirement 8.2** - Schedule 触发时创建触发事件
718    #[tokio::test]
719    async fn test_check_and_fire_expired_at_trigger() {
720        let handler = ScheduleTriggerHandler::new();
721        // 创建一个已过期的 At 触发器(过去时间)
722        let past_ms = Utc::now().timestamp_millis() - 1000; // 1秒前
723        let trigger = create_at_trigger("at-past", past_ms);
724
725        // 手动注册(绕过验证,因为正常注册会拒绝过期时间)
726        {
727            let mut schedules = handler.schedules.write().await;
728            schedules.insert(
729                "at-past".to_string(),
730                RegisteredSchedule {
731                    trigger,
732                    schedule_config: ScheduleTriggerConfig {
733                        schedule_type: ScheduleType::At { at_ms: past_ms },
734                    },
735                    next_trigger_at: Some(DateTime::from_timestamp_millis(past_ms).unwrap()),
736                    last_triggered_at: None,
737                    has_triggered: false,
738                },
739            );
740        }
741
742        let events = handler.check_and_fire().await;
743        assert_eq!(events.len(), 1);
744        assert_eq!(events[0].trigger_id, "at-past");
745    }
746
747    /// **Validates: Requirement 8.6** - 传递 schedule 上下文给 Agent
748    #[tokio::test]
749    async fn test_create_schedule_context() {
750        let handler = ScheduleTriggerHandler::new();
751        let trigger = create_every_trigger("every-1", 60000);
752        handler.register(trigger).await.unwrap();
753
754        let event = ScheduleTriggerEvent {
755            trigger_id: "every-1".to_string(),
756            schedule_type: ScheduleType::Every { every_ms: 60000 },
757            triggered_at: Utc::now(),
758            next_trigger_at: None,
759        };
760
761        let context = handler.create_schedule_context(&event).await;
762        assert!(context.is_some());
763        let ctx = context.unwrap();
764        assert_eq!(ctx.trigger_id, "every-1");
765        assert!(ctx.schedule_description.contains("每隔"));
766    }
767
768    /// **Validates: Requirement 8.2** - 创建触发结果
769    #[tokio::test]
770    async fn test_create_trigger_result() {
771        let handler = ScheduleTriggerHandler::new();
772        let trigger = create_cron_trigger("cron-1", "0 0 * * * *");
773        handler.register(trigger).await.unwrap();
774
775        let event = ScheduleTriggerEvent {
776            trigger_id: "cron-1".to_string(),
777            schedule_type: ScheduleType::Cron {
778                expr: "0 0 * * * *".to_string(),
779                timezone: None,
780            },
781            triggered_at: Utc::now(),
782            next_trigger_at: None,
783        };
784
785        let result = handler.create_trigger_result(&event).await;
786        assert!(result.is_some());
787
788        match result.unwrap() {
789            TriggerResult::Triggered { trigger, context } => {
790                assert_eq!(trigger.id, "cron-1");
791                assert_eq!(context.trigger_type, TriggerType::Schedule);
792                assert!(context.extra.contains_key("schedule_type"));
793            }
794            _ => panic!("Expected TriggerResult::Triggered"),
795        }
796    }
797
798    // ========== ScheduleType 描述测试 ==========
799
800    #[tokio::test]
801    async fn test_describe_cron_schedule() {
802        let handler = ScheduleTriggerHandler::new();
803        let schedule_type = ScheduleType::Cron {
804            expr: "0 0 * * * *".to_string(),
805            timezone: Some("Asia/Shanghai".to_string()),
806        };
807        let desc = handler.describe_schedule_type(&schedule_type);
808        assert!(desc.contains("Cron"));
809        assert!(desc.contains("Asia/Shanghai"));
810    }
811
812    #[tokio::test]
813    async fn test_describe_at_schedule() {
814        let handler = ScheduleTriggerHandler::new();
815        let at_ms = Utc::now().timestamp_millis();
816        let schedule_type = ScheduleType::At { at_ms };
817        let desc = handler.describe_schedule_type(&schedule_type);
818        assert!(desc.contains("一次性"));
819    }
820
821    #[tokio::test]
822    async fn test_describe_every_schedule() {
823        let handler = ScheduleTriggerHandler::new();
824        let schedule_type = ScheduleType::Every { every_ms: 3600000 };
825        let desc = handler.describe_schedule_type(&schedule_type);
826        assert!(desc.contains("每隔"));
827        assert!(desc.contains("小时"));
828    }
829
830    // ========================================================================
831    // Property 8: Schedule 配置类型支持 - 属性测试
832    // Feature: auto-reply-mechanism, Property 8: Schedule 配置类型支持
833    // **Validates: Requirements 8.3-8.5**
834    // ========================================================================
835
836    use proptest::prelude::*;
837
838    /// 测试配置
839    fn test_config() -> ProptestConfig {
840        ProptestConfig::with_cases(100)
841    }
842
843    // ========== 生成器定义 ==========
844
845    /// 生成有效的 Cron 表达式
846    /// **Validates: Requirement 8.3**
847    fn arb_valid_cron_expr() -> impl Strategy<Value = String> {
848        // 生成有效的 cron 表达式(6 字段格式:秒 分 时 日 月 周)
849        prop_oneof![
850            Just("0 0 * * * *".to_string()),    // 每小时
851            Just("0 */5 * * * *".to_string()),  // 每 5 分钟
852            Just("0 0 0 * * *".to_string()),    // 每天午夜
853            Just("0 0 12 * * *".to_string()),   // 每天中午
854            Just("0 30 9 * * 1-5".to_string()), // 工作日 9:30
855            Just("0 0 0 1 * *".to_string()),    // 每月 1 号
856            Just("0 0 0 * * 0".to_string()),    // 每周日
857            Just("0 0 */2 * * *".to_string()),  // 每 2 小时
858        ]
859    }
860
861    /// 生成有效的时区
862    fn arb_timezone() -> impl Strategy<Value = Option<String>> {
863        prop_oneof![
864            Just(None),
865            Just(Some("UTC".to_string())),
866            Just(Some("Asia/Shanghai".to_string())),
867            Just(Some("America/New_York".to_string())),
868            Just(Some("Europe/London".to_string())),
869        ]
870    }
871
872    /// 生成 Cron 调度类型
873    /// **Validates: Requirement 8.3**
874    fn arb_cron_schedule() -> impl Strategy<Value = ScheduleType> {
875        (arb_valid_cron_expr(), arb_timezone())
876            .prop_map(|(expr, timezone)| ScheduleType::Cron { expr, timezone })
877    }
878
879    /// 生成 At 调度类型(一次性定时)
880    /// **Validates: Requirement 8.4**
881    fn arb_at_schedule() -> impl Strategy<Value = ScheduleType> {
882        // 生成有效的时间戳(正数,未来时间)
883        (1i64..=i64::MAX / 2).prop_map(|at_ms| ScheduleType::At { at_ms })
884    }
885
886    /// 生成 Every 调度类型(固定间隔)
887    /// **Validates: Requirement 8.5**
888    fn arb_every_schedule() -> impl Strategy<Value = ScheduleType> {
889        // 生成有效的间隔(大于 0)
890        (1u64..=u64::MAX / 2).prop_map(|every_ms| ScheduleType::Every { every_ms })
891    }
892
893    /// 生成任意有效的 ScheduleType
894    /// **Validates: Requirements 8.3-8.5**
895    fn arb_schedule_type() -> impl Strategy<Value = ScheduleType> {
896        prop_oneof![arb_cron_schedule(), arb_at_schedule(), arb_every_schedule(),]
897    }
898
899    /// 生成 ScheduleTriggerConfig
900    fn arb_schedule_trigger_config() -> impl Strategy<Value = ScheduleTriggerConfig> {
901        arb_schedule_type().prop_map(|schedule_type| ScheduleTriggerConfig { schedule_type })
902    }
903
904    proptest! {
905        #![proptest_config(test_config())]
906
907        // ====================================================================
908        // Property 8.1: ScheduleType 序列化 Round-Trip
909        // **Validates: Requirements 8.3-8.5**
910        // ====================================================================
911
912        /// Property 8.1: ScheduleType 序列化后再反序列化应保持配置完整性
913        ///
914        /// *For any* ScheduleType 配置(Cron、At、Every),序列化和反序列化应保持配置完整性。
915        #[test]
916        fn prop_schedule_type_roundtrip(schedule_type in arb_schedule_type()) {
917            // Feature: auto-reply-mechanism, Property 8: Schedule 配置类型支持
918            // **Validates: Requirements 8.3-8.5**
919
920            // 序列化为 JSON
921            let json = serde_json::to_string(&schedule_type)
922                .expect("ScheduleType 应该能序列化为 JSON");
923
924            // 反序列化回 ScheduleType
925            let parsed: ScheduleType = serde_json::from_str(&json)
926                .expect("JSON 应该能反序列化回 ScheduleType");
927
928            // 验证 round-trip 一致性
929            prop_assert_eq!(
930                schedule_type,
931                parsed,
932                "ScheduleType round-trip 应保持一致"
933            );
934        }
935
936        // ====================================================================
937        // Property 8.2: ScheduleTriggerConfig 序列化 Round-Trip
938        // **Validates: Requirements 8.3-8.5**
939        // ====================================================================
940
941        /// Property 8.2: ScheduleTriggerConfig 序列化后再反序列化应保持配置完整性
942        #[test]
943        fn prop_schedule_trigger_config_roundtrip(
944            config in arb_schedule_trigger_config()
945        ) {
946            // Feature: auto-reply-mechanism, Property 8: Schedule 配置类型支持
947            // **Validates: Requirements 8.3-8.5**
948
949            // 序列化为 JSON
950            let json = serde_json::to_string(&config)
951                .expect("ScheduleTriggerConfig 应该能序列化为 JSON");
952
953            // 反序列化回 ScheduleTriggerConfig
954            let parsed: ScheduleTriggerConfig = serde_json::from_str(&json)
955                .expect("JSON 应该能反序列化回 ScheduleTriggerConfig");
956
957            // 验证 round-trip 一致性
958            prop_assert_eq!(
959                config,
960                parsed,
961                "ScheduleTriggerConfig round-trip 应保持一致"
962            );
963        }
964
965        // ====================================================================
966        // Property 8.3: Cron 调度类型序列化格式
967        // **Validates: Requirement 8.3**
968        // ====================================================================
969
970        /// Property 8.3: Cron 调度类型序列化应包含 kind 字段
971        #[test]
972        fn prop_cron_schedule_serialization_format(
973            schedule_type in arb_cron_schedule()
974        ) {
975            // Feature: auto-reply-mechanism, Property 8: Schedule 配置类型支持
976            // **Validates: Requirement 8.3**
977
978            let json = serde_json::to_string(&schedule_type)
979                .expect("Cron ScheduleType 应该能序列化");
980
981            // 验证 JSON 包含 kind: "cron"
982            prop_assert!(
983                json.contains("\"kind\":\"cron\""),
984                "Cron 调度类型 JSON 应包含 kind:cron,实际: {}",
985                json
986            );
987
988            // 验证 JSON 包含 expr 字段
989            prop_assert!(
990                json.contains("\"expr\""),
991                "Cron 调度类型 JSON 应包含 expr 字段,实际: {}",
992                json
993            );
994        }
995
996        // ====================================================================
997        // Property 8.4: At 调度类型序列化格式
998        // **Validates: Requirement 8.4**
999        // ====================================================================
1000
1001        /// Property 8.4: At 调度类型序列化应包含 kind 字段
1002        #[test]
1003        fn prop_at_schedule_serialization_format(schedule_type in arb_at_schedule()) {
1004            // Feature: auto-reply-mechanism, Property 8: Schedule 配置类型支持
1005            // **Validates: Requirement 8.4**
1006
1007            let json = serde_json::to_string(&schedule_type)
1008                .expect("At ScheduleType 应该能序列化");
1009
1010            // 验证 JSON 包含 kind: "at"
1011            prop_assert!(
1012                json.contains("\"kind\":\"at\""),
1013                "At 调度类型 JSON 应包含 kind:at,实际: {}",
1014                json
1015            );
1016
1017            // 验证 JSON 包含 at_ms 字段
1018            prop_assert!(
1019                json.contains("\"at_ms\""),
1020                "At 调度类型 JSON 应包含 at_ms 字段,实际: {}",
1021                json
1022            );
1023        }
1024
1025        // ====================================================================
1026        // Property 8.5: Every 调度类型序列化格式
1027        // **Validates: Requirement 8.5**
1028        // ====================================================================
1029
1030        /// Property 8.5: Every 调度类型序列化应包含 kind 字段
1031        #[test]
1032        fn prop_every_schedule_serialization_format(
1033            schedule_type in arb_every_schedule()
1034        ) {
1035            // Feature: auto-reply-mechanism, Property 8: Schedule 配置类型支持
1036            // **Validates: Requirement 8.5**
1037
1038            let json = serde_json::to_string(&schedule_type)
1039                .expect("Every ScheduleType 应该能序列化");
1040
1041            // 验证 JSON 包含 kind: "every"
1042            prop_assert!(
1043                json.contains("\"kind\":\"every\""),
1044                "Every 调度类型 JSON 应包含 kind:every,实际: {}",
1045                json
1046            );
1047
1048            // 验证 JSON 包含 every_ms 字段
1049            prop_assert!(
1050                json.contains("\"every_ms\""),
1051                "Every 调度类型 JSON 应包含 every_ms 字段,实际: {}",
1052                json
1053            );
1054        }
1055
1056        // ====================================================================
1057        // Property 8.6: 调度类型字段值保持一致
1058        // **Validates: Requirements 8.3-8.5**
1059        // ====================================================================
1060
1061        /// Property 8.6: Cron 调度类型字段值在 round-trip 后保持一致
1062        #[test]
1063        fn prop_cron_fields_preserved(
1064            expr in arb_valid_cron_expr(),
1065            timezone in arb_timezone()
1066        ) {
1067            // Feature: auto-reply-mechanism, Property 8: Schedule 配置类型支持
1068            // **Validates: Requirement 8.3**
1069
1070            let original = ScheduleType::Cron {
1071                expr: expr.clone(),
1072                timezone: timezone.clone(),
1073            };
1074
1075            let json = serde_json::to_string(&original).unwrap();
1076            let parsed: ScheduleType = serde_json::from_str(&json).unwrap();
1077
1078            match parsed {
1079                ScheduleType::Cron {
1080                    expr: parsed_expr,
1081                    timezone: parsed_tz,
1082                } => {
1083                    prop_assert_eq!(expr, parsed_expr, "expr 字段应保持一致");
1084                    prop_assert_eq!(timezone, parsed_tz, "timezone 字段应保持一致");
1085                }
1086                _ => prop_assert!(false, "解析后应为 Cron 类型"),
1087            }
1088        }
1089
1090        /// Property 8.7: At 调度类型字段值在 round-trip 后保持一致
1091        #[test]
1092        fn prop_at_fields_preserved(at_ms in 1i64..=i64::MAX / 2) {
1093            // Feature: auto-reply-mechanism, Property 8: Schedule 配置类型支持
1094            // **Validates: Requirement 8.4**
1095
1096            let original = ScheduleType::At { at_ms };
1097
1098            let json = serde_json::to_string(&original).unwrap();
1099            let parsed: ScheduleType = serde_json::from_str(&json).unwrap();
1100
1101            match parsed {
1102                ScheduleType::At { at_ms: parsed_ms } => {
1103                    prop_assert_eq!(at_ms, parsed_ms, "at_ms 字段应保持一致");
1104                }
1105                _ => prop_assert!(false, "解析后应为 At 类型"),
1106            }
1107        }
1108
1109        /// Property 8.8: Every 调度类型字段值在 round-trip 后保持一致
1110        #[test]
1111        fn prop_every_fields_preserved(every_ms in 1u64..=u64::MAX / 2) {
1112            // Feature: auto-reply-mechanism, Property 8: Schedule 配置类型支持
1113            // **Validates: Requirement 8.5**
1114
1115            let original = ScheduleType::Every { every_ms };
1116
1117            let json = serde_json::to_string(&original).unwrap();
1118            let parsed: ScheduleType = serde_json::from_str(&json).unwrap();
1119
1120            match parsed {
1121                ScheduleType::Every { every_ms: parsed_ms } => {
1122                    prop_assert_eq!(every_ms, parsed_ms, "every_ms 字段应保持一致");
1123                }
1124                _ => prop_assert!(false, "解析后应为 Every 类型"),
1125            }
1126        }
1127    }
1128}