返回 CodeWhale
automation.rs
根目录 / crates / tui / src / commands / groups / utility / automation.rs
1 //! Operator controls for durable scheduled automations.
2
3 use crate::commands::CommandResult;
4 use crate::commands::traits::{CommandInfo, RegisterCommand};
5 use crate::localization::{Locale, MessageId, tr};
6 use crate::tui::app::{App, AppAction, AutomationAction};
7
8 pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
9 name: "automation",
10 aliases: &["automations", "scheduled"],
11 usage: "/automation [list|show <id>|pause <id>|resume <id>|delete <id> [--confirm <token>]|run <id>]",
12 description_id: MessageId::CmdAutomationDescription,
13 };
14
15 pub(in crate::commands) struct AutomationCmd;
16
17 impl RegisterCommand for AutomationCmd {
18 fn info() -> &'static CommandInfo {
19 &COMMAND_INFO
20 }
21
22 fn execute(app: &mut App, arg: Option<&str>) -> CommandResult {
23 automation(app.ui_locale, arg)
24 }
25 }
26
27 fn automation(locale: Locale, args: Option<&str>) -> CommandResult {
28 let raw = args.unwrap_or("").trim();
29 if raw.is_empty() || raw.eq_ignore_ascii_case("list") {
30 return action(AutomationAction::List);
31 }
32
33 let mut parts = raw.split_whitespace();
34 let verb = parts.next().unwrap_or("").to_ascii_lowercase();
35
36 match verb.as_str() {
37 "show" | "status" => single_id(locale, &mut parts, AutomationAction::Show),
38 "pause" => single_id(locale, &mut parts, AutomationAction::Pause),
39 "resume" => single_id(locale, &mut parts, AutomationAction::Resume),
40 "delete" | "remove" | "rm" => delete(locale, &mut parts),
41 "run" | "trigger" => single_id(locale, &mut parts, AutomationAction::Run),
42 _ => usage_error(locale),
43 }
44 }
45
46 fn single_id<'a>(
47 locale: Locale,
48 parts: &mut impl Iterator<Item = &'a str>,
49 make_action: fn(String) -> AutomationAction,
50 ) -> CommandResult {
51 let Some(id) = parts.next() else {
52 return usage_error(locale);
53 };
54 if parts.next().is_some() {
55 return usage_error(locale);
56 }
57 action(make_action(id.to_string()))
58 }
59
60 fn delete<'a>(locale: Locale, parts: &mut impl Iterator<Item = &'a str>) -> CommandResult {
61 let Some(id) = parts.next() else {
62 return usage_error(locale);
63 };
64 let confirmation = match (parts.next(), parts.next(), parts.next()) {
65 (None, None, None) => None,
66 (Some(flag), Some(token), None) if flag.eq_ignore_ascii_case("--confirm") => {
67 Some(token.to_string())
68 }
69 _ => return usage_error(locale),
70 };
71 action(AutomationAction::Delete {
72 id: id.to_string(),
73 confirmation,
74 })
75 }
76
77 fn usage_error(locale: Locale) -> CommandResult {
78 CommandResult::error(tr(locale, MessageId::AutomationUsage).into_owned())
79 }
80
81 fn action(action: AutomationAction) -> CommandResult {
82 CommandResult::action(AppAction::Automation(action))
83 }
84
85 #[cfg(test)]
86 mod tests {
87 use super::*;
88
89 fn parsed(args: Option<&str>) -> Option<AutomationAction> {
90 match automation(Locale::En, args).action {
91 Some(AppAction::Automation(action)) => Some(action),
92 _ => None,
93 }
94 }
95
96 #[test]
97 fn parses_list_show_and_mutations() {
98 assert_eq!(parsed(None), Some(AutomationAction::List));
99 assert_eq!(parsed(Some("list")), Some(AutomationAction::List));
100 assert_eq!(
101 parsed(Some("show auto_1")),
102 Some(AutomationAction::Show("auto_1".to_string()))
103 );
104 assert_eq!(
105 parsed(Some("pause auto_1")),
106 Some(AutomationAction::Pause("auto_1".to_string()))
107 );
108 assert_eq!(
109 parsed(Some("resume auto_1")),
110 Some(AutomationAction::Resume("auto_1".to_string()))
111 );
112 assert_eq!(
113 parsed(Some("delete auto_1")),
114 Some(AutomationAction::Delete {
115 id: "auto_1".to_string(),
116 confirmation: None,
117 })
118 );
119 assert_eq!(
120 parsed(Some("run auto_1")),
121 Some(AutomationAction::Run("auto_1".to_string()))
122 );
123 }
124
125 #[test]
126 fn accepts_operator_aliases() {
127 assert_eq!(
128 parsed(Some("status auto_1")),
129 Some(AutomationAction::Show("auto_1".to_string()))
130 );
131 assert_eq!(
132 parsed(Some("rm auto_1")),
133 Some(AutomationAction::Delete {
134 id: "auto_1".to_string(),
135 confirmation: None,
136 })
137 );
138 assert_eq!(
139 parsed(Some("trigger auto_1")),
140 Some(AutomationAction::Run("auto_1".to_string()))
141 );
142 }
143
144 #[test]
145 fn validates_missing_ids_and_unknown_actions() {
146 for verb in ["show", "pause", "resume", "delete", "run", "unknown"] {
147 let result = automation(Locale::En, Some(verb));
148 assert!(result.message.is_some(), "{verb} should show usage");
149 assert!(result.action.is_none());
150 }
151 }
152
153 #[test]
154 fn delete_confirmation_is_explicit_and_exact() {
155 assert_eq!(
156 parsed(Some("delete auto_1 --confirm receipt")),
157 Some(AutomationAction::Delete {
158 id: "auto_1".to_string(),
159 confirmation: Some("receipt".to_string()),
160 })
161 );
162 for invalid in [
163 "delete auto_1 --confirm",
164 "delete auto_1 receipt",
165 "delete auto_1 --confirm receipt extra",
166 ] {
167 let result = automation(Locale::En, Some(invalid));
168 assert!(result.is_error, "{invalid} should be rejected");
169 assert!(result.action.is_none());
170 }
171 }
172 }
173
173 lines RUST