1use orcs_event::Signal;
7use orcs_types::Principal;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum InputCommand {
12 Approve {
14 approval_id: Option<String>,
16 },
17
18 Reject {
20 approval_id: Option<String>,
22 reason: Option<String>,
24 },
25
26 Pause,
28
29 Resume,
31
32 Steer {
34 message: String,
36 },
37
38 Quit,
40
41 Veto,
43
44 Empty,
48
49 ComponentMessage {
55 target: String,
57 message: String,
59 },
60
61 Unknown {
63 input: String,
65 },
66}
67
68impl InputCommand {
69 #[must_use]
78 pub fn to_signal(
79 &self,
80 principal: Principal,
81 default_approval_id: Option<&str>,
82 ) -> Option<Signal> {
83 match self {
84 Self::Approve { approval_id } => {
85 let id = approval_id.as_deref().or(default_approval_id)?;
86 Some(Signal::approve(id, principal))
87 }
88 Self::Reject {
89 approval_id,
90 reason,
91 } => {
92 let id = approval_id.as_deref().or(default_approval_id)?;
93 Some(Signal::reject(id, reason.clone(), principal))
94 }
95 Self::Veto => Some(Signal::veto(principal)),
96 Self::Pause | Self::Resume | Self::Steer { .. } => {
97 None
99 }
100 Self::Quit | Self::Empty | Self::Unknown { .. } | Self::ComponentMessage { .. } => None,
101 }
102 }
103
104 #[must_use]
106 pub fn is_approval(&self) -> bool {
107 matches!(self, Self::Approve { .. })
108 }
109
110 #[must_use]
112 pub fn is_rejection(&self) -> bool {
113 matches!(self, Self::Reject { .. })
114 }
115
116 #[must_use]
118 pub fn is_hil_response(&self) -> bool {
119 self.is_approval() || self.is_rejection()
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::io::InputParser;
127 use orcs_event::SignalKind;
128 use orcs_types::PrincipalId;
129
130 #[test]
131 fn parse_approve_simple() {
132 let cmd = InputParser.parse("y");
133 assert!(matches!(cmd, InputCommand::Approve { approval_id: None }));
134
135 let cmd = InputParser.parse("yes");
136 assert!(matches!(cmd, InputCommand::Approve { approval_id: None }));
137
138 let cmd = InputParser.parse("approve");
139 assert!(matches!(cmd, InputCommand::Approve { approval_id: None }));
140 }
141
142 #[test]
143 fn parse_approve_with_id() {
144 let cmd = InputParser.parse("y req-123");
145 assert!(matches!(
146 cmd,
147 InputCommand::Approve {
148 approval_id: Some(ref id)
149 } if id == "req-123"
150 ));
151 }
152
153 #[test]
154 fn parse_reject_simple() {
155 let cmd = InputParser.parse("n");
156 assert!(matches!(
157 cmd,
158 InputCommand::Reject {
159 approval_id: None,
160 reason: None
161 }
162 ));
163 }
164
165 #[test]
166 fn parse_reject_with_id() {
167 let cmd = InputParser.parse("n req-456");
168 assert!(matches!(
169 cmd,
170 InputCommand::Reject {
171 approval_id: Some(ref id),
172 reason: None
173 } if id == "req-456"
174 ));
175 }
176
177 #[test]
178 fn parse_reject_with_reason() {
179 let cmd = InputParser.parse("n req-789 too dangerous");
180 assert!(matches!(
181 cmd,
182 InputCommand::Reject {
183 approval_id: Some(ref id),
184 reason: Some(ref r)
185 } if id == "req-789" && r == "too dangerous"
186 ));
187 }
188
189 #[test]
190 fn parse_pause_resume() {
191 assert!(matches!(InputParser.parse("p"), InputCommand::Pause));
192 assert!(matches!(InputParser.parse("pause"), InputCommand::Pause));
193 assert!(matches!(InputParser.parse("r"), InputCommand::Resume));
194 assert!(matches!(InputParser.parse("resume"), InputCommand::Resume));
195 }
196
197 #[test]
198 fn parse_steer() {
199 let cmd = InputParser.parse("s focus on error handling");
200 assert!(matches!(
201 cmd,
202 InputCommand::Steer { ref message } if message == "focus on error handling"
203 ));
204 }
205
206 #[test]
207 fn parse_steer_empty_is_unknown() {
208 let cmd = InputParser.parse("s");
209 assert!(matches!(cmd, InputCommand::Unknown { .. }));
210
211 let cmd = InputParser.parse("steer");
212 assert!(matches!(cmd, InputCommand::Unknown { .. }));
213 }
214
215 #[test]
216 fn parse_quit() {
217 assert!(matches!(InputParser.parse("q"), InputCommand::Quit));
218 assert!(matches!(InputParser.parse("quit"), InputCommand::Quit));
219 assert!(matches!(InputParser.parse("exit"), InputCommand::Quit));
220 }
221
222 #[test]
223 fn parse_veto() {
224 assert!(matches!(InputParser.parse("veto"), InputCommand::Veto));
225 assert!(matches!(InputParser.parse("stop"), InputCommand::Veto));
226 assert!(matches!(InputParser.parse("abort"), InputCommand::Veto));
227 }
228
229 #[test]
230 fn parse_unknown() {
231 let cmd = InputParser.parse("foobar");
232 assert!(matches!(cmd, InputCommand::Unknown { .. }));
233 }
234
235 #[test]
236 fn parse_empty() {
237 let cmd = InputParser.parse("");
238 assert!(matches!(cmd, InputCommand::Empty));
239
240 let cmd = InputParser.parse(" ");
241 assert!(matches!(cmd, InputCommand::Empty));
242 }
243
244 #[test]
245 fn to_signal_approve() {
246 let cmd = InputCommand::Approve {
247 approval_id: Some("req-123".to_string()),
248 };
249 let principal = Principal::User(PrincipalId::new());
250 let signal = cmd.to_signal(principal, None);
251
252 assert!(signal.is_some());
253 let signal = signal.expect("approve command with id should produce signal");
254 assert!(signal.is_approve());
255 }
256
257 #[test]
258 fn to_signal_approve_with_default() {
259 let cmd = InputCommand::Approve { approval_id: None };
260 let principal = Principal::User(PrincipalId::new());
261 let signal = cmd.to_signal(principal, Some("default-id"));
262
263 assert!(signal.is_some());
264 let signal = signal.expect("approve command with default id should produce signal");
265 assert!(signal.is_approve());
266 if let SignalKind::Approve { approval_id } = &signal.kind {
267 assert_eq!(approval_id, "default-id");
268 }
269 }
270
271 #[test]
272 fn to_signal_approve_no_id() {
273 let cmd = InputCommand::Approve { approval_id: None };
274 let principal = Principal::User(PrincipalId::new());
275 let signal = cmd.to_signal(principal, None);
276
277 assert!(signal.is_none());
279 }
280
281 #[test]
282 fn to_signal_reject() {
283 let cmd = InputCommand::Reject {
284 approval_id: Some("req-456".to_string()),
285 reason: Some("not allowed".to_string()),
286 };
287 let principal = Principal::User(PrincipalId::new());
288 let signal = cmd.to_signal(principal, None);
289
290 assert!(signal.is_some());
291 let signal = signal.expect("reject command with id should produce signal");
292 assert!(signal.is_reject());
293 }
294
295 #[test]
296 fn to_signal_veto() {
297 let cmd = InputCommand::Veto;
298 let principal = Principal::User(PrincipalId::new());
299 let signal = cmd.to_signal(principal, None);
300
301 assert!(signal.is_some());
302 let signal = signal.expect("veto command should produce signal");
303 assert!(signal.is_veto());
304 assert!(signal.is_global());
305 }
306
307 #[test]
308 fn to_signal_quit_returns_none() {
309 let cmd = InputCommand::Quit;
310 let principal = Principal::User(PrincipalId::new());
311 let signal = cmd.to_signal(principal, None);
312
313 assert!(signal.is_none());
314 }
315
316 #[test]
317 fn command_helpers() {
318 assert!(InputCommand::Approve { approval_id: None }.is_approval());
319 assert!(!InputCommand::Approve { approval_id: None }.is_rejection());
320 assert!(InputCommand::Approve { approval_id: None }.is_hil_response());
321
322 assert!(InputCommand::Reject {
323 approval_id: None,
324 reason: None
325 }
326 .is_rejection());
327 assert!(InputCommand::Reject {
328 approval_id: None,
329 reason: None
330 }
331 .is_hil_response());
332 }
333
334 #[test]
335 fn to_signal_component_message_returns_none() {
336 let cmd = InputCommand::ComponentMessage {
337 target: "shell".to_string(),
338 message: "ls".to_string(),
339 };
340 let principal = Principal::User(PrincipalId::new());
341 let signal = cmd.to_signal(principal, None);
342 assert!(signal.is_none());
343 }
344
345 #[test]
346 fn case_insensitive() {
347 assert!(matches!(
348 InputParser.parse("Y"),
349 InputCommand::Approve { .. }
350 ));
351 assert!(matches!(
352 InputParser.parse("YES"),
353 InputCommand::Approve { .. }
354 ));
355 assert!(matches!(
356 InputParser.parse("N"),
357 InputCommand::Reject { .. }
358 ));
359 assert!(matches!(InputParser.parse("QUIT"), InputCommand::Quit));
360 }
361}