1use std::fmt;
7
8use chrono::Utc;
9use ironflow_store::entities::RunStatus;
10use serde::{Deserialize, Serialize};
11
12use super::{Transition, TransitionError};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum RunEvent {
29 PickedUp,
31 AllStepsCompleted,
33 StepFailed,
35 StepFailedRetryable,
37 RetryStarted,
39 MaxRetriesExceeded,
41 CancelRequested,
43 ApprovalRequested,
45 Approved,
47 Rejected,
49}
50
51impl fmt::Display for RunEvent {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 RunEvent::PickedUp => f.write_str("picked_up"),
55 RunEvent::AllStepsCompleted => f.write_str("all_steps_completed"),
56 RunEvent::StepFailed => f.write_str("step_failed"),
57 RunEvent::StepFailedRetryable => f.write_str("step_failed_retryable"),
58 RunEvent::RetryStarted => f.write_str("retry_started"),
59 RunEvent::MaxRetriesExceeded => f.write_str("max_retries_exceeded"),
60 RunEvent::CancelRequested => f.write_str("cancel_requested"),
61 RunEvent::ApprovalRequested => f.write_str("approval_requested"),
62 RunEvent::Approved => f.write_str("approved"),
63 RunEvent::Rejected => f.write_str("rejected"),
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
108pub struct RunFsm {
109 state: RunStatus,
110 history: Vec<Transition<RunStatus, RunEvent>>,
111}
112
113impl RunFsm {
114 pub fn new() -> Self {
126 Self {
127 state: RunStatus::Pending,
128 history: Vec::new(),
129 }
130 }
131
132 pub fn from_state(state: RunStatus) -> Self {
144 Self {
145 state,
146 history: Vec::new(),
147 }
148 }
149
150 pub fn state(&self) -> RunStatus {
152 self.state
153 }
154
155 pub fn history(&self) -> &[Transition<RunStatus, RunEvent>] {
157 &self.history
158 }
159
160 pub fn is_terminal(&self) -> bool {
162 self.state.is_terminal()
163 }
164
165 pub fn apply(
186 &mut self,
187 event: RunEvent,
188 ) -> Result<RunStatus, TransitionError<RunStatus, RunEvent>> {
189 let next = next_state(self.state, event).ok_or(TransitionError {
190 from: self.state,
191 event,
192 })?;
193
194 let transition = Transition {
195 from: self.state,
196 to: next,
197 event,
198 at: Utc::now(),
199 };
200
201 self.history.push(transition);
202 self.state = next;
203 Ok(next)
204 }
205
206 pub fn can_apply(&self, event: RunEvent) -> bool {
218 next_state(self.state, event).is_some()
219 }
220}
221
222impl Default for RunFsm {
223 fn default() -> Self {
224 Self::new()
225 }
226}
227
228fn next_state(from: RunStatus, event: RunEvent) -> Option<RunStatus> {
231 match (from, event) {
232 (RunStatus::Pending, RunEvent::PickedUp) => Some(RunStatus::Running),
234 (RunStatus::Pending, RunEvent::CancelRequested) => Some(RunStatus::Cancelled),
235
236 (RunStatus::Running, RunEvent::AllStepsCompleted) => Some(RunStatus::Completed),
238 (RunStatus::Running, RunEvent::StepFailed) => Some(RunStatus::Failed),
239 (RunStatus::Running, RunEvent::StepFailedRetryable) => Some(RunStatus::Retrying),
240 (RunStatus::Running, RunEvent::CancelRequested) => Some(RunStatus::Cancelled),
241
242 (RunStatus::Retrying, RunEvent::RetryStarted) => Some(RunStatus::Running),
244 (RunStatus::Retrying, RunEvent::MaxRetriesExceeded) => Some(RunStatus::Failed),
245 (RunStatus::Retrying, RunEvent::CancelRequested) => Some(RunStatus::Cancelled),
246
247 (RunStatus::Running, RunEvent::ApprovalRequested) => Some(RunStatus::AwaitingApproval),
249 (RunStatus::AwaitingApproval, RunEvent::Approved) => Some(RunStatus::Running),
250 (RunStatus::AwaitingApproval, RunEvent::Rejected) => Some(RunStatus::Failed),
251 (RunStatus::AwaitingApproval, RunEvent::CancelRequested) => Some(RunStatus::Cancelled),
252
253 _ => None,
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
265 fn pending_to_running() {
266 let mut fsm = RunFsm::new();
267 let result = fsm.apply(RunEvent::PickedUp);
268 assert!(result.is_ok());
269 assert_eq!(fsm.state(), RunStatus::Running);
270 }
271
272 #[test]
273 fn full_success_path() {
274 let mut fsm = RunFsm::new();
275 fsm.apply(RunEvent::PickedUp).unwrap();
276 fsm.apply(RunEvent::AllStepsCompleted).unwrap();
277 assert_eq!(fsm.state(), RunStatus::Completed);
278 assert!(fsm.is_terminal());
279 assert_eq!(fsm.history().len(), 2);
280 }
281
282 #[test]
283 fn full_failure_path() {
284 let mut fsm = RunFsm::new();
285 fsm.apply(RunEvent::PickedUp).unwrap();
286 fsm.apply(RunEvent::StepFailed).unwrap();
287 assert_eq!(fsm.state(), RunStatus::Failed);
288 assert!(fsm.is_terminal());
289 }
290
291 #[test]
292 fn retry_then_success() {
293 let mut fsm = RunFsm::new();
294 fsm.apply(RunEvent::PickedUp).unwrap();
295 fsm.apply(RunEvent::StepFailedRetryable).unwrap();
296 assert_eq!(fsm.state(), RunStatus::Retrying);
297
298 fsm.apply(RunEvent::RetryStarted).unwrap();
299 assert_eq!(fsm.state(), RunStatus::Running);
300
301 fsm.apply(RunEvent::AllStepsCompleted).unwrap();
302 assert_eq!(fsm.state(), RunStatus::Completed);
303 assert_eq!(fsm.history().len(), 4);
304 }
305
306 #[test]
307 fn retry_then_max_retries_exceeded() {
308 let mut fsm = RunFsm::new();
309 fsm.apply(RunEvent::PickedUp).unwrap();
310 fsm.apply(RunEvent::StepFailedRetryable).unwrap();
311 fsm.apply(RunEvent::MaxRetriesExceeded).unwrap();
312 assert_eq!(fsm.state(), RunStatus::Failed);
313 }
314
315 #[test]
316 fn cancel_from_pending() {
317 let mut fsm = RunFsm::new();
318 fsm.apply(RunEvent::CancelRequested).unwrap();
319 assert_eq!(fsm.state(), RunStatus::Cancelled);
320 assert!(fsm.is_terminal());
321 }
322
323 #[test]
324 fn cancel_from_running() {
325 let mut fsm = RunFsm::new();
326 fsm.apply(RunEvent::PickedUp).unwrap();
327 fsm.apply(RunEvent::CancelRequested).unwrap();
328 assert_eq!(fsm.state(), RunStatus::Cancelled);
329 }
330
331 #[test]
332 fn cancel_from_retrying() {
333 let mut fsm = RunFsm::new();
334 fsm.apply(RunEvent::PickedUp).unwrap();
335 fsm.apply(RunEvent::StepFailedRetryable).unwrap();
336 fsm.apply(RunEvent::CancelRequested).unwrap();
337 assert_eq!(fsm.state(), RunStatus::Cancelled);
338 }
339
340 #[test]
343 fn cannot_complete_from_pending() {
344 let mut fsm = RunFsm::new();
345 let result = fsm.apply(RunEvent::AllStepsCompleted);
346 assert!(result.is_err());
347 assert_eq!(fsm.state(), RunStatus::Pending);
348 }
349
350 #[test]
351 fn cannot_pick_up_running() {
352 let mut fsm = RunFsm::new();
353 fsm.apply(RunEvent::PickedUp).unwrap();
354 let result = fsm.apply(RunEvent::PickedUp);
355 assert!(result.is_err());
356 }
357
358 #[test]
359 fn cannot_transition_from_terminal() {
360 let mut fsm = RunFsm::new();
361 fsm.apply(RunEvent::PickedUp).unwrap();
362 fsm.apply(RunEvent::AllStepsCompleted).unwrap();
363
364 assert!(fsm.apply(RunEvent::PickedUp).is_err());
365 assert!(fsm.apply(RunEvent::CancelRequested).is_err());
366 assert!(fsm.apply(RunEvent::StepFailed).is_err());
367 }
368
369 #[test]
372 fn can_apply_checks_without_mutation() {
373 let fsm = RunFsm::new();
374 assert!(fsm.can_apply(RunEvent::PickedUp));
375 assert!(fsm.can_apply(RunEvent::CancelRequested));
376 assert!(!fsm.can_apply(RunEvent::AllStepsCompleted));
377 assert!(!fsm.can_apply(RunEvent::StepFailed));
378 assert_eq!(fsm.state(), RunStatus::Pending);
379 }
380
381 #[test]
384 fn from_state_resumes_at_given_state() {
385 let mut fsm = RunFsm::from_state(RunStatus::Running);
386 assert_eq!(fsm.state(), RunStatus::Running);
387 assert!(fsm.history().is_empty());
388
389 fsm.apply(RunEvent::AllStepsCompleted).unwrap();
390 assert_eq!(fsm.state(), RunStatus::Completed);
391 }
392
393 #[test]
396 fn history_records_transitions() {
397 let mut fsm = RunFsm::new();
398 fsm.apply(RunEvent::PickedUp).unwrap();
399 fsm.apply(RunEvent::StepFailedRetryable).unwrap();
400 fsm.apply(RunEvent::RetryStarted).unwrap();
401
402 let history = fsm.history();
403 assert_eq!(history.len(), 3);
404
405 assert_eq!(history[0].from, RunStatus::Pending);
406 assert_eq!(history[0].to, RunStatus::Running);
407 assert_eq!(history[0].event, RunEvent::PickedUp);
408
409 assert_eq!(history[1].from, RunStatus::Running);
410 assert_eq!(history[1].to, RunStatus::Retrying);
411 assert_eq!(history[1].event, RunEvent::StepFailedRetryable);
412
413 assert_eq!(history[2].from, RunStatus::Retrying);
414 assert_eq!(history[2].to, RunStatus::Running);
415 assert_eq!(history[2].event, RunEvent::RetryStarted);
416 }
417
418 #[test]
421 fn running_to_awaiting_approval() {
422 let mut fsm = RunFsm::new();
423 fsm.apply(RunEvent::PickedUp).unwrap();
424 fsm.apply(RunEvent::ApprovalRequested).unwrap();
425 assert_eq!(fsm.state(), RunStatus::AwaitingApproval);
426 assert!(!fsm.is_terminal());
427 }
428
429 #[test]
430 fn awaiting_approval_approved_resumes_running() {
431 let mut fsm = RunFsm::new();
432 fsm.apply(RunEvent::PickedUp).unwrap();
433 fsm.apply(RunEvent::ApprovalRequested).unwrap();
434 fsm.apply(RunEvent::Approved).unwrap();
435 assert_eq!(fsm.state(), RunStatus::Running);
436 }
437
438 #[test]
439 fn awaiting_approval_rejected_fails() {
440 let mut fsm = RunFsm::new();
441 fsm.apply(RunEvent::PickedUp).unwrap();
442 fsm.apply(RunEvent::ApprovalRequested).unwrap();
443 fsm.apply(RunEvent::Rejected).unwrap();
444 assert_eq!(fsm.state(), RunStatus::Failed);
445 assert!(fsm.is_terminal());
446 }
447
448 #[test]
449 fn awaiting_approval_cancel() {
450 let mut fsm = RunFsm::new();
451 fsm.apply(RunEvent::PickedUp).unwrap();
452 fsm.apply(RunEvent::ApprovalRequested).unwrap();
453 fsm.apply(RunEvent::CancelRequested).unwrap();
454 assert_eq!(fsm.state(), RunStatus::Cancelled);
455 assert!(fsm.is_terminal());
456 }
457
458 #[test]
459 fn cannot_approve_from_pending() {
460 let mut fsm = RunFsm::new();
461 assert!(fsm.apply(RunEvent::Approved).is_err());
462 }
463
464 #[test]
465 fn approval_then_complete() {
466 let mut fsm = RunFsm::new();
467 fsm.apply(RunEvent::PickedUp).unwrap();
468 fsm.apply(RunEvent::ApprovalRequested).unwrap();
469 fsm.apply(RunEvent::Approved).unwrap();
470 fsm.apply(RunEvent::AllStepsCompleted).unwrap();
471 assert_eq!(fsm.state(), RunStatus::Completed);
472 assert_eq!(fsm.history().len(), 4);
473 }
474
475 #[test]
478 fn transition_error_display() {
479 let mut fsm = RunFsm::new();
480 let err = fsm.apply(RunEvent::AllStepsCompleted).unwrap_err();
481 let msg = err.to_string();
482 assert!(msg.contains("all_steps_completed"));
483 assert!(msg.contains("Pending"));
484 }
485}