1use chrono::Local;
2use serde_json::Value;
3
4use crate::blocks;
5use crate::blocks::{
6 Card, CollapsiblePanel, Column, ColumnSet, ColumnWidth, HeaderBlock, Markdown, TextAlign,
7 TextSize, TextTag,
8};
9
10pub type CardContent = serde_json::Value;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum LanguageCode {
23 Zh,
25 En,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum SeverityLevel {
39 Info,
40 Warning,
41 Error,
42 Critical,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum ColorTheme {
52 Blue,
54 Green,
56 Red,
58 Orange,
60 Wathet,
62 Purple,
64 Grey,
66}
67
68impl ColorTheme {
69 pub fn as_str(&self) -> &'static str {
70 match self {
71 ColorTheme::Blue => "blue",
72 ColorTheme::Green => "green",
73 ColorTheme::Red => "red",
74 ColorTheme::Orange => "orange",
75 ColorTheme::Wathet => "wathet",
76 ColorTheme::Purple => "purple",
77 ColorTheme::Grey => "grey",
78 }
79 }
80}
81
82impl std::fmt::Display for ColorTheme {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 write!(f, "{}", self.as_str())
85 }
86}
87
88impl std::str::FromStr for ColorTheme {
89 type Err = String;
90
91 fn from_str(s: &str) -> Result<Self, Self::Err> {
92 match s.to_lowercase().as_str() {
93 "blue" => Ok(ColorTheme::Blue),
94 "green" => Ok(ColorTheme::Green),
95 "red" => Ok(ColorTheme::Red),
96 "orange" => Ok(ColorTheme::Orange),
97 "wathet" => Ok(ColorTheme::Wathet),
98 "purple" => Ok(ColorTheme::Purple),
99 "grey" | "gray" => Ok(ColorTheme::Grey),
100 _ => Err(format!("unknown color: {s}")),
101 }
102 }
103}
104
105impl SeverityLevel {
106 pub fn as_str(&self) -> &'static str {
107 match self {
108 SeverityLevel::Info => "info",
109 SeverityLevel::Warning => "warning",
110 SeverityLevel::Error => "error",
111 SeverityLevel::Critical => "critical",
112 }
113 }
114
115 pub fn color(&self) -> &'static str {
116 match self {
117 SeverityLevel::Info => "blue",
118 SeverityLevel::Warning => "orange",
119 SeverityLevel::Error | SeverityLevel::Critical => "red",
120 }
121 }
122
123 pub fn icon(&self) -> &'static str {
124 match self {
125 SeverityLevel::Info => ":InfoCircle:",
126 SeverityLevel::Warning => ":WarningTriangle:",
127 SeverityLevel::Error => ":CrossMark:",
128 SeverityLevel::Critical => ":Fire:",
129 }
130 }
131}
132
133impl std::fmt::Display for SeverityLevel {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 write!(f, "{}", self.as_str())
136 }
137}
138
139impl std::str::FromStr for SeverityLevel {
140 type Err = String;
141
142 fn from_str(s: &str) -> Result<Self, Self::Err> {
143 match s.to_lowercase().as_str() {
144 "info" => Ok(SeverityLevel::Info),
145 "warning" => Ok(SeverityLevel::Warning),
146 "error" => Ok(SeverityLevel::Error),
147 "critical" => Ok(SeverityLevel::Critical),
148 _ => Err(format!("unknown severity: {s}")),
149 }
150 }
151}
152
153impl std::fmt::Display for LanguageCode {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 match self {
156 LanguageCode::Zh => write!(f, "zh"),
157 LanguageCode::En => write!(f, "en"),
158 }
159 }
160}
161
162impl std::str::FromStr for LanguageCode {
163 type Err = String;
164
165 fn from_str(s: &str) -> Result<Self, Self::Err> {
166 match s {
167 "zh" => Ok(LanguageCode::Zh),
168 "en" => Ok(LanguageCode::En),
169 _ => Err(format!("unknown language: {s}")),
170 }
171 }
172}
173
174pub fn get_translation(key: &str, lang: LanguageCode) -> String {
175 let s = match lang {
176 LanguageCode::Zh => zh_translation(key),
177 LanguageCode::En => en_translation(key),
178 };
179 s.map(|s| s.to_owned()).unwrap_or_else(|| key.to_owned())
180}
181
182fn zh_translation(key: &str) -> Option<&'static str> {
183 Some(match key {
184 "task_name" => "作业名称",
185 "start_time" => "开始时间",
186 "completion_time" => "完成时间",
187 "task_description" => "作业描述",
188 "estimated_duration" => "预计用时",
189 "execution_duration" => "执行时长",
190 "execution_status" => "执行状态",
191 "result_storage" => "结果存储",
192 "storage_prefix" => "存储前缀",
193 "result_overview" => "结果概览",
194 "running_overview" => "运行概览",
195 "state_overview" => "状态概览",
196 "metadata_overview" => "元数据概览",
197 "running" => "正在运行",
198 "completed" => "已成功完成",
199 "failed" => "失败",
200 "success" => "已完成",
201 "failure" => "失败",
202 "task_notification" => "作业运行情况通知",
203 "task_completion_notification" => "作业完成情况通知",
204 "task_failure_notification" => "作业失败情况通知",
205 "no_description" => "*No description provided*",
206 "timestamp" => "时间",
207 "unknown_task" => "未知任务",
208 "return_code" => "返回值",
209 "group" => "归属组别",
210 "network_submission_started" => "网络提交已开始",
211 "network_submission_complete" => "网络提交已完成",
212 "network_submission_failed" => "网络提交失败",
213 "network_set_name" => "网络集名称",
214 "network_type" => "网络类型",
215 "expected_count" => "预期数量",
216 "submitted_count" => "提交总数",
217 "submitted" => "已提交",
218 "config_uploaded" => "配置已上传",
219 "config_name" => "配置名称",
220 "files_uploaded" => "已上传文件数",
221 "description" => "配置描述",
222 "uploaded_files" => "已上传文件的标签",
223 "task_submission_started" => "任务提交已开始",
224 "task_submission_complete" => "任务提交已完成",
225 "task_submission_failed" => "任务提交失败",
226 "task_set_name" => "任务集名称",
227 "iterations" => "迭代次数",
228 "duration" => "持续时间",
229 "submission_overview" => "提交概览",
230 "status" => "状态",
231 "successfully_completed" => "已成功完成",
232 "submitted_before_failure" => "失败前已提交",
233 "error_details" => "错误详情",
234 "task_set_complete" => "任务集已完成",
235 "task_set_failed" => "任务集失败",
236 "task_set_progress" => "任务集进度",
237 "task_set_count" => "任务集数量",
238 "result_collection_started" => "结果收集已开始",
239 "result_collection_complete" => "结果收集已完成",
240 "task_sets" => "任务集",
241 "rows" => "行数",
242 "columns" => "列数",
243 "comparison_complete" => "比较已完成",
244 "comparison_name" => "比较名称",
245 "task_sets_compared" => "已比较任务集",
246 "common_networks" => "公共网络数",
247 "result_rows" => "结果行数",
248 "result_columns" => "结果列数",
249 "comparison_results" => "比较结果",
250 "total_items" => "总项目数",
251 "summary" => "摘要",
252 "items" => "项目",
253 _ => return None,
254 })
255}
256
257fn en_translation(key: &str) -> Option<&'static str> {
258 Some(match key {
259 "task_name" => "Job Name",
260 "start_time" => "Start Time",
261 "completion_time" => "Completion Time",
262 "task_description" => "Job Description",
263 "estimated_duration" => "Estimated Duration",
264 "execution_duration" => "Execution Duration",
265 "execution_status" => "Execution Status",
266 "result_storage" => "Result Storage",
267 "storage_prefix" => "Storage Prefix",
268 "result_overview" => "Result Overview",
269 "running_overview" => "Running Overview",
270 "state_overview" => "State Overview",
271 "metadata_overview" => "Metadata Overview",
272 "running" => "Running Now",
273 "completed" => "Successfully Completed",
274 "failed" => "Failed",
275 "success" => "Completed",
276 "failure" => "Failed",
277 "task_notification" => "Job Status Notification",
278 "task_completion_notification" => "Job Completion Notification",
279 "task_failure_notification" => "Job Failure Notification",
280 "no_description" => "*No description provided*",
281 "timestamp" => "Timestamp",
282 "unknown_task" => "Unknown Task",
283 "return_code" => "Return Status",
284 "group" => "Group",
285 "network_submission_started" => "Network Submission Started",
286 "network_submission_complete" => "Network Submission Complete",
287 "network_submission_failed" => "Network Submission Failed",
288 "network_set_name" => "Network Set Name",
289 "network_type" => "Network Type",
290 "expected_count" => "Expected Count",
291 "submitted_count" => "Total Count Submitted",
292 "submitted" => "Submitted",
293 "config_uploaded" => "Configuration Uploaded",
294 "config_name" => "Config Name",
295 "files_uploaded" => "Files Uploaded",
296 "description" => "Config Description",
297 "uploaded_files" => "Labels of Uploaded Files",
298 "task_submission_started" => "Task Submission Started",
299 "task_submission_complete" => "Task Submission Complete",
300 "task_submission_failed" => "Task Submission Failed",
301 "task_set_name" => "Task Set Name",
302 "iterations" => "Iterations",
303 "duration" => "Duration",
304 "submission_overview" => "Submission Overview",
305 "status" => "Status",
306 "successfully_completed" => "Successfully Completed",
307 "submitted_before_failure" => "Submitted Before Failure",
308 "error_details" => "Error Details",
309 "task_set_complete" => "Task Set Complete",
310 "task_set_failed" => "Task Set Failed",
311 "task_set_progress" => "Task Set Progress",
312 "task_set_count" => "Task Set Count",
313 "result_collection_started" => "Result Collection Started",
314 "result_collection_complete" => "Result Collection Complete",
315 "task_sets" => "Task Sets",
316 "rows" => "Rows",
317 "columns" => "Columns",
318 "comparison_complete" => "Comparison Complete",
319 "comparison_name" => "Comparison Name",
320 "task_sets_compared" => "Task Sets Compared",
321 "common_networks" => "Common Networks",
322 "result_rows" => "Result Rows",
323 "result_columns" => "Result Columns",
324 "comparison_results" => "Comparison Results",
325 "total_items" => "Total Items",
326 "summary" => "Summary",
327 "items" => "Items",
328 _ => return None,
329 })
330}
331
332pub trait LarkTemplate {
340 fn generate(&self) -> CardContent;
342}
343
344pub struct LegacyTaskTemplate {
350 pub task_name: String,
351 pub status: Option<i32>,
352 pub group: String,
353 pub prefix: String,
354 pub task_summary: String,
355 pub language: LanguageCode,
356}
357
358impl LegacyTaskTemplate {
359 fn t(&self, key: &str) -> String {
360 get_translation(key, self.language)
361 }
362}
363
364impl LarkTemplate for LegacyTaskTemplate {
365 fn generate(&self) -> CardContent {
366 let task_time = Local::now().format("%Y-%m-%d %H:%M").to_string();
367 let task_status = match self.status {
368 Some(s) if s != 0 => format!(
369 "<font color='red'> :CrossMark: {}: {} {s}</font>",
370 self.t("failed"),
371 self.t("return_code")
372 ),
373 _ => format!(
374 "<font color='green'> :CheckMark: {}</font>",
375 self.t("completed")
376 ),
377 };
378 blocks::template_reference(
379 "AAqz08XD5HCzP",
380 "1.0.3",
381 serde_json::json!({
382 "task_name": self.task_name,
383 "task_time": task_time,
384 "attachment_group": self.group,
385 "attachment_prefix": self.prefix,
386 "task_summary": self.task_summary,
387 "task_status": task_status,
388 }),
389 )
390 .into()
391 }
392}
393
394pub struct StartTaskTemplate {
400 pub task_name: String,
401 pub desc: Option<String>,
402 pub group: Option<String>,
403 pub prefix: Option<String>,
404 pub msg: Option<String>,
405 pub estimated_duration: Option<String>,
406 pub language: LanguageCode,
407}
408
409impl StartTaskTemplate {
410 fn t(&self, key: &str) -> String {
411 get_translation(key, self.language)
412 }
413}
414
415impl LarkTemplate for StartTaskTemplate {
416 fn generate(&self) -> CardContent {
417 let task_time = Local::now().format("%Y-%m-%d %H:%M").to_string();
418 let no_desc = get_translation("no_description", self.language);
419 let desc = self.desc.as_deref().unwrap_or(&no_desc);
420 let duration_text = self
421 .estimated_duration
422 .as_ref()
423 .map(|d| format!("\n**{}:** {d}", self.t("estimated_duration")))
424 .unwrap_or_default();
425 let task_status = format!(
426 "<font color='wathet-400'> :StatusInFlight: {}</font>",
427 self.t("running")
428 );
429 let main_text = format!(
430 "**{}:** {}\n**{}:** {}\n**{}:** {}{}\n**{}:** {}",
431 self.t("task_name"),
432 self.task_name,
433 self.t("start_time"),
434 task_time,
435 self.t("task_description"),
436 desc,
437 duration_text,
438 self.t("execution_status"),
439 task_status,
440 );
441
442 let mut elements = vec![blocks::markdown(&main_text).into()];
443
444 if self.group.is_some() || self.prefix.is_some() {
445 elements.push(storage_columns(
446 &self.t("result_storage"),
447 self.group.as_deref().unwrap_or(""),
448 &self.t("storage_prefix"),
449 self.prefix.as_deref().unwrap_or(""),
450 ));
451 }
452
453 if let Some(msg) = &self.msg {
454 elements.push(overview_panel(&self.t("running_overview"), msg));
455 }
456
457 let hdr: Value = HeaderBlock {
458 title: self.t("task_notification"),
459 template: "wathet".into(),
460 subtitle: Some("".into()),
461 text_tag_list: Some(vec![
462 TextTag {
463 text: self.t("running"),
464 color: "wathet".into(),
465 }
466 .into(),
467 ]),
468 padding: Some("12px 8px 12px 8px".into()),
469 }
470 .into();
471 Card {
472 elements,
473 header: hdr,
474 config: Some(blocks::config_textsize_normal_v2()),
475 ..Default::default()
476 }
477 .into()
478 }
479}
480
481pub struct ReportTaskResultTemplate {
487 pub task_name: String,
488 pub status: i32,
489 pub group: Option<String>,
490 pub prefix: Option<String>,
491 pub desc: Option<String>,
492 pub msg: Option<String>,
493 pub duration: Option<String>,
494 pub title: Option<String>,
495 pub language: LanguageCode,
496}
497
498impl ReportTaskResultTemplate {
499 fn t(&self, key: &str) -> String {
500 get_translation(key, self.language)
501 }
502}
503
504impl LarkTemplate for ReportTaskResultTemplate {
505 fn generate(&self) -> CardContent {
506 let task_time = Local::now().format("%Y-%m-%d %H:%M").to_string();
507 let task_status = format!(
508 "<font color='green'> :CheckMark: {}</font>",
509 self.t("completed")
510 );
511 let desc_text = self
512 .desc
513 .as_ref()
514 .map(|d| format!("\n**{}:** {d}", self.t("task_description")))
515 .unwrap_or_default();
516 let dur_text = self
517 .duration
518 .as_ref()
519 .map(|d| format!("\n**{}:** {d}", self.t("execution_duration")))
520 .unwrap_or_default();
521 let main_text = format!(
522 "**{}:** {}\n**{}:** {}{}{}\n**{}:** {}",
523 self.t("task_name"),
524 self.task_name,
525 self.t("completion_time"),
526 task_time,
527 desc_text,
528 dur_text,
529 self.t("execution_status"),
530 task_status,
531 );
532
533 let mut elements = vec![blocks::markdown(&main_text).into()];
534 if self.group.is_some() || self.prefix.is_some() {
535 elements.push(storage_columns(
536 &self.t("group"),
537 self.group.as_deref().unwrap_or(""),
538 &self.t("storage_prefix"),
539 self.prefix.as_deref().unwrap_or(""),
540 ));
541 }
542 if let Some(msg) = &self.msg {
543 elements.push(overview_panel(&self.t("result_overview"), msg));
544 }
545
546 let default_title = self.t("task_completion_notification");
547 let card_title = self.title.as_deref().unwrap_or(&default_title);
548 let hdr: Value = HeaderBlock {
549 title: card_title.into(),
550 template: "green".into(),
551 subtitle: Some("".into()),
552 text_tag_list: Some(vec![
553 TextTag {
554 text: self.t("success"),
555 color: "green".into(),
556 }
557 .into(),
558 ]),
559 padding: Some("12px 8px 12px 8px".into()),
560 }
561 .into();
562 Card {
563 elements,
564 header: hdr,
565 config: Some(blocks::config_textsize_normal_v2()),
566 ..Default::default()
567 }
568 .into()
569 }
570}
571
572pub struct ReportFailureTaskTemplate {
578 pub task_name: String,
579 pub status: i32,
581 pub group: Option<String>,
582 pub prefix: Option<String>,
583 pub desc: Option<String>,
584 pub msg: Option<String>,
585 pub duration: Option<String>,
586 pub title: Option<String>,
587 pub language: LanguageCode,
588}
589
590impl ReportFailureTaskTemplate {
591 fn t(&self, key: &str) -> String {
592 get_translation(key, self.language)
593 }
594}
595
596impl LarkTemplate for ReportFailureTaskTemplate {
597 fn generate(&self) -> CardContent {
598 let task_time = Local::now().format("%Y-%m-%d %H:%M").to_string();
599 let status = self.status;
600 let task_status = format!(
601 "<font color='red'> :CrossMark: {}: {status}</font>",
602 self.t("failed")
603 );
604 let desc_text = self
605 .desc
606 .as_ref()
607 .map(|d| format!("\n**{}:** {d}", self.t("task_description")))
608 .unwrap_or_default();
609 let dur_text = self
610 .duration
611 .as_ref()
612 .map(|d| format!("\n**{}:** {d}", self.t("execution_duration")))
613 .unwrap_or_default();
614 let main_text = format!(
615 "**{}:** {}\n**{}:** {}{}{}\n**{}:** {}",
616 self.t("task_name"),
617 self.task_name,
618 self.t("completion_time"),
619 task_time,
620 desc_text,
621 dur_text,
622 self.t("execution_status"),
623 task_status,
624 );
625
626 let mut elements = vec![blocks::markdown(&main_text).into()];
627 if self.group.is_some() || self.prefix.is_some() {
628 elements.push(storage_columns(
629 &self.t("group"),
630 self.group.as_deref().unwrap_or(""),
631 &self.t("storage_prefix"),
632 self.prefix.as_deref().unwrap_or(""),
633 ));
634 }
635 if let Some(msg) = &self.msg {
636 elements.push(overview_panel(&self.t("result_overview"), msg));
637 }
638
639 let default_title = self.t("task_failure_notification");
640 let card_title = self.title.as_deref().unwrap_or(&default_title);
641 let hdr: Value = HeaderBlock {
642 title: card_title.into(),
643 template: "red".into(),
644 subtitle: Some("".into()),
645 text_tag_list: Some(vec![
646 TextTag {
647 text: self.t("failure"),
648 color: "red".into(),
649 }
650 .into(),
651 ]),
652 padding: Some("12px 8px 12px 8px".into()),
653 }
654 .into();
655 Card {
656 elements,
657 header: hdr,
658 config: Some(blocks::config_textsize_normal_v2()),
659 ..Default::default()
660 }
661 .into()
662 }
663}
664
665pub struct SimpleMessageTemplate {
667 pub title: String,
668 pub content: String,
669 pub color: ColorTheme,
670 pub language: LanguageCode,
671}
672
673impl LarkTemplate for SimpleMessageTemplate {
674 fn generate(&self) -> CardContent {
675 let hdr: Value = HeaderBlock {
676 title: self.title.clone(),
677 template: self.color.as_str().into(),
678 ..Default::default()
679 }
680 .into();
681 Card {
682 elements: vec![blocks::markdown(&self.content).into()],
683 header: hdr,
684 ..Default::default()
685 }
686 .into()
687 }
688}
689
690pub struct AlertTemplate {
695 pub title: String,
696 pub message: String,
697 pub severity: SeverityLevel,
698 pub timestamp: String,
700 pub language: LanguageCode,
701}
702
703impl AlertTemplate {
704 fn t(&self, key: &str) -> String {
705 get_translation(key, self.language)
706 }
707}
708
709impl LarkTemplate for AlertTemplate {
710 fn generate(&self) -> CardContent {
711 let color = self.severity.color();
712 let icon = self.severity.icon();
713 let severity_str = self.severity.as_str().to_uppercase();
714 let body_text = format!(
715 "{icon} **{}**\n\n**{}:** {}",
716 self.message,
717 self.t("timestamp"),
718 self.timestamp,
719 );
720 let hdr: Value = HeaderBlock {
721 title: self.title.clone(),
722 template: color.into(),
723 subtitle: Some(severity_str.clone()),
724 text_tag_list: Some(vec![
725 TextTag {
726 text: severity_str,
727 color: color.into(),
728 }
729 .into(),
730 ]),
731 ..Default::default()
732 }
733 .into();
734 Card {
735 elements: vec![blocks::markdown(&body_text).into()],
736 header: hdr,
737 ..Default::default()
738 }
739 .into()
740 }
741}
742
743pub struct RawContentTemplate {
748 pub content: CardContent,
749 pub language: LanguageCode,
750}
751
752impl LarkTemplate for RawContentTemplate {
753 fn generate(&self) -> CardContent {
754 self.content.clone()
755 }
756}
757
758pub struct GenericCardTemplate {
762 pub(crate) content: CardContent,
763}
764
765impl LarkTemplate for GenericCardTemplate {
766 fn generate(&self) -> CardContent {
767 self.content.clone()
768 }
769}
770
771pub(crate) fn storage_columns(
772 group_label: &str,
773 group_value: &str,
774 prefix_label: &str,
775 prefix_value: &str,
776) -> Value {
777 let col1: Value = Column {
778 elements: vec![
779 Markdown {
780 content: format!("**{group_label}**\n{group_value}"),
781 text_align: TextAlign::Center,
782 text_size: TextSize::NormalV2,
783 margin: "0px 4px 0px 4px".into(),
784 }
785 .into(),
786 ],
787 width: ColumnWidth::Auto,
788 ..Default::default()
789 }
790 .into();
791 let col2: Value = Column {
792 elements: vec![
793 Markdown {
794 content: format!("**{prefix_label}**\n{prefix_value}"),
795 text_align: TextAlign::Center,
796 text_size: TextSize::NormalV2,
797 ..Default::default()
798 }
799 .into(),
800 ],
801 width: ColumnWidth::Weighted,
802 weight: Some(1),
803 ..Default::default()
804 }
805 .into();
806 ColumnSet {
807 columns: vec![col1, col2],
808 ..Default::default()
809 }
810 .into()
811}
812
813pub(crate) fn overview_panel(title: &str, content: &str) -> Value {
814 CollapsiblePanel {
815 title_markdown: format!("**<font color='grey-800'>{title}</font>**"),
816 elements: vec![
817 Markdown {
818 content: content.into(),
819 text_size: TextSize::NormalV2,
820 ..Default::default()
821 }
822 .into(),
823 ],
824 expanded: false,
825 ..Default::default()
826 }
827 .into()
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833
834 #[test]
835 fn test_translation_zh() {
836 assert_eq!(get_translation("task_name", LanguageCode::Zh), "作业名称");
837 assert_eq!(get_translation("running", LanguageCode::Zh), "正在运行");
838 }
839
840 #[test]
841 fn test_translation_en() {
842 assert_eq!(get_translation("task_name", LanguageCode::En), "Job Name");
843 assert_eq!(get_translation("running", LanguageCode::En), "Running Now");
844 }
845
846 #[test]
847 fn test_translation_fallback() {
848 let result = get_translation("nonexistent_key_xyz", LanguageCode::Zh);
849 assert_eq!(result, "nonexistent_key_xyz");
850 }
851
852 #[test]
853 fn test_simple_message_generate() {
854 let t = SimpleMessageTemplate {
855 title: "Hello".to_owned(),
856 content: "World".to_owned(),
857 color: ColorTheme::Blue,
858 language: LanguageCode::Zh,
859 };
860 let card = t.generate();
861 assert_eq!(card["schema"], "2.0");
862 assert_eq!(card["header"]["title"]["content"], "Hello");
863 assert_eq!(card["header"]["template"], "blue");
864 assert_eq!(card["body"]["elements"][0]["content"], "World");
865 assert!(card.get("config").is_none());
866 }
867
868 #[test]
869 fn test_alert_template_warning() {
870 let t = AlertTemplate {
871 title: "Alert!".to_owned(),
872 message: "High CPU".to_owned(),
873 severity: SeverityLevel::Warning,
874 timestamp: "2026-01-01 00:00:00".to_owned(),
875 language: LanguageCode::En,
876 };
877 let card = t.generate();
878 assert_eq!(card["header"]["template"], "orange");
879 let body_content = card["body"]["elements"][0]["content"].as_str().unwrap();
880 assert!(body_content.contains("High CPU"));
881 }
882
883 #[test]
884 fn test_raw_content_passthrough() {
885 let content = serde_json::json!({"schema": "2.0", "custom": true});
886 let t = RawContentTemplate {
887 content: content.clone(),
888 language: LanguageCode::Zh,
889 };
890 assert_eq!(t.generate(), content);
891 }
892
893 #[test]
894 fn test_legacy_task_template() {
895 let t = LegacyTaskTemplate {
896 task_name: "my-task".to_owned(),
897 status: Some(0),
898 group: "g1".to_owned(),
899 prefix: "p/".to_owned(),
900 task_summary: "summary".to_owned(),
901 language: LanguageCode::Zh,
902 };
903 let card = t.generate();
904 assert_eq!(card["type"], "template");
905 assert_eq!(card["data"]["template_id"], "AAqz08XD5HCzP");
906 assert_eq!(card["data"]["template_variable"]["task_name"], "my-task");
907 assert!(
908 card["data"]["template_variable"]["task_status"]
909 .as_str()
910 .unwrap()
911 .contains("CheckMark")
912 );
913 }
914
915 #[test]
916 fn test_start_task_template_structure() {
917 let t = StartTaskTemplate {
918 task_name: "build".to_owned(),
919 desc: None,
920 group: None,
921 prefix: None,
922 msg: None,
923 estimated_duration: None,
924 language: LanguageCode::Zh,
925 };
926 let card = t.generate();
927 assert_eq!(card["schema"], "2.0");
928 assert_eq!(card["header"]["template"], "wathet");
929 assert!(card.get("config").is_some());
930 }
931
932 #[test]
933 fn test_report_task_result_green() {
934 let t = ReportTaskResultTemplate {
935 task_name: "build".to_owned(),
936 status: 0,
937 group: None,
938 prefix: None,
939 desc: None,
940 msg: None,
941 duration: None,
942 title: None,
943 language: LanguageCode::Zh,
944 };
945 let card = t.generate();
946 assert_eq!(card["header"]["template"], "green");
947 }
948
949 #[test]
950 fn test_report_failure_task_red() {
951 let t = ReportFailureTaskTemplate {
952 task_name: "build".to_owned(),
953 status: 1,
954 group: None,
955 prefix: None,
956 desc: None,
957 msg: None,
958 duration: None,
959 title: None,
960 language: LanguageCode::Zh,
961 };
962 let card = t.generate();
963 assert_eq!(card["header"]["template"], "red");
964 }
965
966 #[test]
967 fn test_report_failure_body_contains_status_code() {
968 let t = ReportFailureTaskTemplate {
969 task_name: "myjob".to_owned(),
970 status: 42,
971 group: None,
972 prefix: None,
973 desc: None,
974 msg: None,
975 duration: None,
976 title: None,
977 language: LanguageCode::Zh,
978 };
979 let card = t.generate();
980 let body_text = card["body"]["elements"][0]["content"].as_str().unwrap();
981 assert!(
982 body_text.contains("42"),
983 "body should contain the return code"
984 );
985 assert!(body_text.contains("myjob"), "body should contain task name");
986 }
987
988 #[test]
989 fn test_start_task_template_body_element_count_no_group() {
990 let t = StartTaskTemplate {
991 task_name: "t".to_owned(),
992 desc: None,
993 group: None,
994 prefix: None,
995 msg: None,
996 estimated_duration: None,
997 language: LanguageCode::Zh,
998 };
999 let card = t.generate();
1000 let elements = card["body"]["elements"].as_array().unwrap();
1001 assert_eq!(
1002 elements.len(),
1003 1,
1004 "only markdown block when no group/prefix/msg"
1005 );
1006 }
1007
1008 #[test]
1009 fn test_start_task_template_with_group_prefix_has_column_set() {
1010 let t = StartTaskTemplate {
1011 task_name: "t".to_owned(),
1012 desc: None,
1013 group: Some("grp".to_owned()),
1014 prefix: Some("pfx/".to_owned()),
1015 msg: None,
1016 estimated_duration: None,
1017 language: LanguageCode::Zh,
1018 };
1019 let card = t.generate();
1020 let elements = card["body"]["elements"].as_array().unwrap();
1021 assert_eq!(
1022 elements.len(),
1023 2,
1024 "markdown + column_set when group/prefix set"
1025 );
1026 assert_eq!(elements[1]["tag"], "column_set");
1027 }
1028
1029 #[test]
1030 fn test_start_task_template_with_group_prefix_and_msg() {
1031 let t = StartTaskTemplate {
1032 task_name: "t".to_owned(),
1033 desc: None,
1034 group: Some("grp".to_owned()),
1035 prefix: Some("pfx/".to_owned()),
1036 msg: Some("running details".to_owned()),
1037 estimated_duration: None,
1038 language: LanguageCode::Zh,
1039 };
1040 let card = t.generate();
1041 let elements = card["body"]["elements"].as_array().unwrap();
1042 assert_eq!(elements.len(), 3, "markdown + column_set + collapsible");
1043 assert_eq!(elements[2]["tag"], "collapsible_panel");
1044 }
1045
1046 #[test]
1047 fn test_language_switching_body_text() {
1048 let zh_t = SimpleMessageTemplate {
1049 title: "T".to_owned(),
1050 content: "C".to_owned(),
1051 color: ColorTheme::Blue,
1052 language: LanguageCode::Zh,
1053 };
1054 let en_t = SimpleMessageTemplate {
1055 title: "T".to_owned(),
1056 content: "C".to_owned(),
1057 color: ColorTheme::Blue,
1058 language: LanguageCode::En,
1059 };
1060 let zh_card = zh_t.generate();
1062 let en_card = en_t.generate();
1063 assert_eq!(zh_card["header"]["title"]["content"], "T");
1064 assert_eq!(en_card["header"]["title"]["content"], "T");
1065 assert_eq!(zh_card["schema"], en_card["schema"]);
1067 }
1068
1069 #[test]
1070 fn test_start_task_language_switching_zh_en() {
1071 let make = |lang| StartTaskTemplate {
1072 task_name: "job".to_owned(),
1073 desc: None,
1074 group: None,
1075 prefix: None,
1076 msg: None,
1077 estimated_duration: None,
1078 language: lang,
1079 };
1080 let zh_body = make(LanguageCode::Zh).generate();
1081 let en_body = make(LanguageCode::En).generate();
1082 let zh_text = zh_body["body"]["elements"][0]["content"].as_str().unwrap();
1083 let en_text = en_body["body"]["elements"][0]["content"].as_str().unwrap();
1084 assert_ne!(zh_text, en_text, "zh and en body text should differ");
1086 assert!(zh_text.contains("job"), "both should contain the task name");
1087 assert!(en_text.contains("job"));
1088 }
1089
1090 #[test]
1091 fn test_report_task_result_body_contains_task_name() {
1092 let t = ReportTaskResultTemplate {
1093 task_name: "myjob".to_owned(),
1094 status: 0,
1095 group: Some("g".to_owned()),
1096 prefix: Some("p/".to_owned()),
1097 desc: None,
1098 msg: None,
1099 duration: None,
1100 title: None,
1101 language: LanguageCode::Zh,
1102 };
1103 let card = t.generate();
1104 let elements = card["body"]["elements"].as_array().unwrap();
1105 assert_eq!(
1106 elements.len(),
1107 2,
1108 "markdown + column_set when group/prefix set"
1109 );
1110 let body_text = elements[0]["content"].as_str().unwrap();
1111 assert!(body_text.contains("myjob"));
1112 assert_eq!(elements[1]["tag"], "column_set");
1113 }
1114
1115 #[test]
1116 fn test_generic_card_template_passthrough() {
1117 use crate::builder::CardBuilder;
1118 let card_val = CardBuilder::new()
1119 .header("My Card", None, None, None)
1120 .markdown(
1121 "hello",
1122 crate::blocks::TextAlign::Left,
1123 crate::blocks::TextSize::Normal,
1124 )
1125 .build()
1126 .generate();
1127 assert_eq!(card_val["schema"], "2.0");
1128 assert_eq!(card_val["header"]["title"]["content"], "My Card");
1129 assert_eq!(card_val["body"]["elements"][0]["content"], "hello");
1130 }
1131}