Skip to main content

ainl_mission/
state_machine.rs

1//! Pure [`MissionState`](ainl_contracts::MissionState) transition rules.
2
3use ainl_contracts::MissionState;
4use thiserror::Error;
5
6/// A validated state transition.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub struct ValidTransition {
9    pub from: MissionState,
10    pub to: MissionState,
11}
12
13/// State machine violation.
14#[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
25/// Returns true when `from` may move to `to`.
26pub 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
58/// Apply a transition, returning the target state or an error.
59pub 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}