ainl_mission/
state_machine.rs1use ainl_contracts::MissionState;
4use thiserror::Error;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct ValidTransition {
9 pub from: MissionState,
10 pub to: MissionState,
11}
12
13#[derive(Debug, Error, PartialEq, Eq)]
15pub enum StateMachineError {
16 #[error("terminal state {0:?} cannot transition")]
17 Terminal(MissionState),
18 #[error("invalid transition {from:?} -> {to:?}")]
19 InvalidTransition {
20 from: MissionState,
21 to: MissionState,
22 },
23}
24
25pub fn can_transition(from: MissionState, to: MissionState) -> bool {
27 if from == to {
28 return true;
29 }
30 match from {
31 MissionState::Completed | MissionState::Cancelled => false,
32 MissionState::AwaitingInput => {
33 matches!(to, MissionState::Initializing | MissionState::Cancelled)
34 }
35 MissionState::Initializing => {
36 matches!(to, MissionState::Running | MissionState::Cancelled)
37 }
38 MissionState::Running => matches!(
39 to,
40 MissionState::Paused
41 | MissionState::OrchestratorTurn
42 | MissionState::Completed
43 | MissionState::Cancelled
44 ),
45 MissionState::Paused => matches!(
46 to,
47 MissionState::Running
48 | MissionState::OrchestratorTurn
49 | MissionState::Cancelled
50 ),
51 MissionState::OrchestratorTurn => matches!(
52 to,
53 MissionState::Running | MissionState::Completed | MissionState::Cancelled
54 ),
55 }
56}
57
58pub fn transition(from: MissionState, to: MissionState) -> Result<MissionState, StateMachineError> {
60 if from == to {
61 return Ok(to);
62 }
63 if matches!(from, MissionState::Completed | MissionState::Cancelled) {
64 return Err(StateMachineError::Terminal(from));
65 }
66 if can_transition(from, to) {
67 Ok(to)
68 } else {
69 Err(StateMachineError::InvalidTransition { from, to })
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn awaiting_input_to_initializing() {
79 assert!(can_transition(
80 MissionState::AwaitingInput,
81 MissionState::Initializing
82 ));
83 assert_eq!(
84 transition(
85 MissionState::AwaitingInput,
86 MissionState::Initializing
87 )
88 .unwrap(),
89 MissionState::Initializing
90 );
91 }
92
93 #[test]
94 fn running_pause_orchestrator_complete() {
95 assert!(can_transition(MissionState::Running, MissionState::Paused));
96 assert!(can_transition(
97 MissionState::Running,
98 MissionState::OrchestratorTurn
99 ));
100 assert!(can_transition(MissionState::Running, MissionState::Completed));
101 }
102
103 #[test]
104 fn terminal_states_reject_moves() {
105 assert!(!can_transition(MissionState::Completed, MissionState::Running));
106 assert_eq!(
107 transition(MissionState::Completed, MissionState::Running),
108 Err(StateMachineError::Terminal(MissionState::Completed))
109 );
110 }
111
112 #[test]
113 fn invalid_skip_initializing() {
114 assert_eq!(
115 transition(MissionState::AwaitingInput, MissionState::Running),
116 Err(StateMachineError::InvalidTransition {
117 from: MissionState::AwaitingInput,
118 to: MissionState::Running,
119 })
120 );
121 }
122
123 #[test]
124 fn orchestrator_turn_returns_to_running() {
125 assert!(can_transition(
126 MissionState::OrchestratorTurn,
127 MissionState::Running
128 ));
129 }
130}