1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ScheduleTriggerEvent {
39 pub trigger_id: String,
41 pub schedule_type: ScheduleType,
43 pub triggered_at: DateTime<Utc>,
45 pub next_trigger_at: Option<DateTime<Utc>>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct ScheduleContext {
56 pub trigger_id: String,
58 pub trigger_name: String,
60 pub schedule_description: String,
62 pub triggered_at: DateTime<Utc>,
64 pub is_first_trigger: bool,
66 pub last_triggered_at: Option<DateTime<Utc>>,
68 #[serde(default)]
70 pub metadata: HashMap<String, serde_json::Value>,
71}
72
73#[derive(Debug, Clone)]
75struct RegisteredSchedule {
76 trigger: AutoReplyTrigger,
78 schedule_config: ScheduleTriggerConfig,
80 next_trigger_at: Option<DateTime<Utc>>,
82 last_triggered_at: Option<DateTime<Utc>>,
84 has_triggered: bool,
86}
87
88pub struct ScheduleTriggerHandler {
108 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 pub fn new() -> Self {
123 Self {
124 schedules: Arc::new(RwLock::new(HashMap::new())),
125 }
126 }
127
128 pub async fn register(&self, trigger: AutoReplyTrigger) -> Result<(), String> {
139 if trigger.trigger_type != TriggerType::Schedule {
141 return Err(format!("触发器 {} 不是 Schedule 类型", trigger.id));
142 }
143
144 let schedule_config = match &trigger.config {
146 TriggerConfig::Schedule(config) => config.clone(),
147 _ => {
148 return Err(format!("触发器 {} 配置类型不匹配", trigger.id));
149 }
150 };
151
152 self.validate_schedule_type(&schedule_config.schedule_type)?;
154
155 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 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 fn validate_schedule_type(&self, schedule_type: &ScheduleType) -> Result<(), String> {
195 match schedule_type {
196 ScheduleType::Cron { expr, timezone } => {
197 if expr.is_empty() {
199 return Err("Cron 表达式不能为空".to_string());
200 }
201 if let Some(tz) = timezone {
203 if tz.parse::<chrono_tz::Tz>().is_err() {
204 return Err(format!("无效的时区: {}", tz));
205 }
206 }
207 if cron::Schedule::from_str(expr).is_err() {
209 return Err(format!("无效的 Cron 表达式: {}", expr));
210 }
211 Ok(())
212 }
213 ScheduleType::At { at_ms } => {
214 if *at_ms <= 0 {
216 return Err("At 调度时间戳必须为正数".to_string());
217 }
218 Ok(())
219 }
220 ScheduleType::Every { every_ms } => {
221 if *every_ms == 0 {
223 return Err("Every 间隔必须大于 0".to_string());
224 }
225 Ok(())
226 }
227 }
228 }
229
230 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 self.next_cron_trigger(expr, timezone.as_deref(), now)
242 }
243 ScheduleType::At { at_ms } => {
244 let at_time = DateTime::from_timestamp_millis(*at_ms)?;
246 if at_time > now {
247 Some(at_time)
248 } else {
249 None }
251 }
252 ScheduleType::Every { every_ms } => {
253 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 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 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 if !schedule.trigger.enabled {
300 continue;
301 }
302
303 if let Some(next_at) = schedule.next_trigger_at {
305 if next_at <= now {
306 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, };
313
314 schedule.last_triggered_at = Some(now);
316 schedule.has_triggered = true;
317
318 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 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 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 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 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 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 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 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 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 pub async fn count(&self) -> usize {
472 let schedules = self.schedules.read().await;
473 schedules.len()
474 }
475}
476
477fn 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
497use std::str::FromStr;
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503
504 #[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 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 #[tokio::test]
599 async fn test_register_cron_trigger() {
600 let handler = ScheduleTriggerHandler::new();
601 let trigger = create_cron_trigger("cron-1", "0 0 * * * *"); 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 #[tokio::test]
611 async fn test_register_at_trigger() {
612 let handler = ScheduleTriggerHandler::new();
613 let future_ms = Utc::now().timestamp_millis() + 3600000; 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 #[tokio::test]
624 async fn test_register_every_trigger() {
625 let handler = ScheduleTriggerHandler::new();
626 let trigger = create_every_trigger("every-1", 60000); 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 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 #[tokio::test]
719 async fn test_check_and_fire_expired_at_trigger() {
720 let handler = ScheduleTriggerHandler::new();
721 let past_ms = Utc::now().timestamp_millis() - 1000; let trigger = create_at_trigger("at-past", past_ms);
724
725 {
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 #[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 #[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 #[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 use proptest::prelude::*;
837
838 fn test_config() -> ProptestConfig {
840 ProptestConfig::with_cases(100)
841 }
842
843 fn arb_valid_cron_expr() -> impl Strategy<Value = String> {
848 prop_oneof![
850 Just("0 0 * * * *".to_string()), Just("0 */5 * * * *".to_string()), Just("0 0 0 * * *".to_string()), Just("0 0 12 * * *".to_string()), Just("0 30 9 * * 1-5".to_string()), Just("0 0 0 1 * *".to_string()), Just("0 0 0 * * 0".to_string()), Just("0 0 */2 * * *".to_string()), ]
859 }
860
861 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 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 fn arb_at_schedule() -> impl Strategy<Value = ScheduleType> {
882 (1i64..=i64::MAX / 2).prop_map(|at_ms| ScheduleType::At { at_ms })
884 }
885
886 fn arb_every_schedule() -> impl Strategy<Value = ScheduleType> {
889 (1u64..=u64::MAX / 2).prop_map(|every_ms| ScheduleType::Every { every_ms })
891 }
892
893 fn arb_schedule_type() -> impl Strategy<Value = ScheduleType> {
896 prop_oneof![arb_cron_schedule(), arb_at_schedule(), arb_every_schedule(),]
897 }
898
899 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 #[test]
916 fn prop_schedule_type_roundtrip(schedule_type in arb_schedule_type()) {
917 let json = serde_json::to_string(&schedule_type)
922 .expect("ScheduleType 应该能序列化为 JSON");
923
924 let parsed: ScheduleType = serde_json::from_str(&json)
926 .expect("JSON 应该能反序列化回 ScheduleType");
927
928 prop_assert_eq!(
930 schedule_type,
931 parsed,
932 "ScheduleType round-trip 应保持一致"
933 );
934 }
935
936 #[test]
943 fn prop_schedule_trigger_config_roundtrip(
944 config in arb_schedule_trigger_config()
945 ) {
946 let json = serde_json::to_string(&config)
951 .expect("ScheduleTriggerConfig 应该能序列化为 JSON");
952
953 let parsed: ScheduleTriggerConfig = serde_json::from_str(&json)
955 .expect("JSON 应该能反序列化回 ScheduleTriggerConfig");
956
957 prop_assert_eq!(
959 config,
960 parsed,
961 "ScheduleTriggerConfig round-trip 应保持一致"
962 );
963 }
964
965 #[test]
972 fn prop_cron_schedule_serialization_format(
973 schedule_type in arb_cron_schedule()
974 ) {
975 let json = serde_json::to_string(&schedule_type)
979 .expect("Cron ScheduleType 应该能序列化");
980
981 prop_assert!(
983 json.contains("\"kind\":\"cron\""),
984 "Cron 调度类型 JSON 应包含 kind:cron,实际: {}",
985 json
986 );
987
988 prop_assert!(
990 json.contains("\"expr\""),
991 "Cron 调度类型 JSON 应包含 expr 字段,实际: {}",
992 json
993 );
994 }
995
996 #[test]
1003 fn prop_at_schedule_serialization_format(schedule_type in arb_at_schedule()) {
1004 let json = serde_json::to_string(&schedule_type)
1008 .expect("At ScheduleType 应该能序列化");
1009
1010 prop_assert!(
1012 json.contains("\"kind\":\"at\""),
1013 "At 调度类型 JSON 应包含 kind:at,实际: {}",
1014 json
1015 );
1016
1017 prop_assert!(
1019 json.contains("\"at_ms\""),
1020 "At 调度类型 JSON 应包含 at_ms 字段,实际: {}",
1021 json
1022 );
1023 }
1024
1025 #[test]
1032 fn prop_every_schedule_serialization_format(
1033 schedule_type in arb_every_schedule()
1034 ) {
1035 let json = serde_json::to_string(&schedule_type)
1039 .expect("Every ScheduleType 应该能序列化");
1040
1041 prop_assert!(
1043 json.contains("\"kind\":\"every\""),
1044 "Every 调度类型 JSON 应包含 kind:every,实际: {}",
1045 json
1046 );
1047
1048 prop_assert!(
1050 json.contains("\"every_ms\""),
1051 "Every 调度类型 JSON 应包含 every_ms 字段,实际: {}",
1052 json
1053 );
1054 }
1055
1056 #[test]
1063 fn prop_cron_fields_preserved(
1064 expr in arb_valid_cron_expr(),
1065 timezone in arb_timezone()
1066 ) {
1067 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 #[test]
1092 fn prop_at_fields_preserved(at_ms in 1i64..=i64::MAX / 2) {
1093 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 #[test]
1111 fn prop_every_fields_preserved(every_ms in 1u64..=u64::MAX / 2) {
1112 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}