Skip to main content

meerkat_runtime/
runtime_state.rs

1//! §22 RuntimeState — the runtime's own state machine with validated transitions.
2//!
3//! 7 states with strict transition rules from the spec.
4
5use serde::{Deserialize, Serialize};
6
7/// The state of a runtime instance.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10#[non_exhaustive]
11pub enum RuntimeState {
12    /// Initializing (first state after creation).
13    Initializing,
14    /// Idle — no run in progress, ready to accept input.
15    Idle,
16    /// A run is in progress.
17    Running,
18    /// Recovering from a crash or error.
19    Recovering,
20    /// Retired — no longer accepting new input, draining existing.
21    Retired,
22    /// Permanently stopped (terminal).
23    Stopped,
24    /// Destroyed (terminal).
25    Destroyed,
26}
27
28impl RuntimeState {
29    /// Check if this is a terminal state.
30    pub fn is_terminal(&self) -> bool {
31        matches!(self, Self::Stopped | Self::Destroyed)
32    }
33
34    /// Check if the runtime can accept new input in this state.
35    pub fn can_accept_input(&self) -> bool {
36        matches!(self, Self::Idle | Self::Running)
37    }
38
39    /// Check if the runtime can process queued inputs in this state.
40    ///
41    /// Retired runtimes can still drain their queue but cannot accept new input.
42    pub fn can_process_queue(&self) -> bool {
43        matches!(self, Self::Idle | Self::Retired)
44    }
45
46    /// Validate a transition from this state to another (§22 table).
47    pub fn can_transition_to(&self, next: &RuntimeState) -> bool {
48        use RuntimeState::{Destroyed, Idle, Initializing, Recovering, Retired, Running, Stopped};
49        matches!(
50            (self, next),
51            // Initializing → Idle, Stopped, Destroyed
52            (Initializing, Idle | Stopped | Destroyed)
53            // Idle → Running, Retired, Recovering, Stopped, Destroyed
54            | (Idle, Running | Retired | Recovering | Stopped | Destroyed)
55            // Running → Idle, Recovering, Retired, Stopped, Destroyed
56            | (Running, Idle | Recovering | Retired | Stopped | Destroyed)
57            // Recovering → Idle, Running, Stopped, Destroyed
58            | (Recovering, Idle | Running | Stopped | Destroyed)
59            // Retired → Running (drain), Stopped, Destroyed
60            | (Retired, Running | Stopped | Destroyed)
61        )
62    }
63
64    /// Attempt to transition, returning an error if invalid.
65    pub fn transition(&mut self, next: RuntimeState) -> Result<(), RuntimeStateTransitionError> {
66        if self.can_transition_to(&next) {
67            *self = next;
68            Ok(())
69        } else {
70            Err(RuntimeStateTransitionError {
71                from: *self,
72                to: next,
73            })
74        }
75    }
76}
77
78impl std::fmt::Display for RuntimeState {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        match self {
81            Self::Initializing => write!(f, "initializing"),
82            Self::Idle => write!(f, "idle"),
83            Self::Running => write!(f, "running"),
84            Self::Recovering => write!(f, "recovering"),
85            Self::Retired => write!(f, "retired"),
86            Self::Stopped => write!(f, "stopped"),
87            Self::Destroyed => write!(f, "destroyed"),
88        }
89    }
90}
91
92/// Error when an invalid runtime state transition is attempted.
93#[derive(Debug, Clone, thiserror::Error)]
94#[error("Invalid runtime state transition: {from} -> {to}")]
95pub struct RuntimeStateTransitionError {
96    pub from: RuntimeState,
97    pub to: RuntimeState,
98}
99
100#[cfg(test)]
101#[allow(clippy::unwrap_used)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn terminal_states() {
107        assert!(RuntimeState::Stopped.is_terminal());
108        assert!(RuntimeState::Destroyed.is_terminal());
109
110        assert!(!RuntimeState::Initializing.is_terminal());
111        assert!(!RuntimeState::Idle.is_terminal());
112        assert!(!RuntimeState::Running.is_terminal());
113        assert!(!RuntimeState::Recovering.is_terminal());
114        assert!(!RuntimeState::Retired.is_terminal());
115    }
116
117    #[test]
118    fn can_accept_input() {
119        assert!(RuntimeState::Idle.can_accept_input());
120        assert!(RuntimeState::Running.can_accept_input());
121
122        assert!(!RuntimeState::Initializing.can_accept_input());
123        assert!(!RuntimeState::Recovering.can_accept_input());
124        assert!(!RuntimeState::Retired.can_accept_input());
125        assert!(!RuntimeState::Stopped.can_accept_input());
126        assert!(!RuntimeState::Destroyed.can_accept_input());
127    }
128
129    #[test]
130    fn can_process_queue() {
131        assert!(RuntimeState::Idle.can_process_queue());
132        assert!(RuntimeState::Retired.can_process_queue());
133
134        assert!(!RuntimeState::Initializing.can_process_queue());
135        assert!(!RuntimeState::Running.can_process_queue());
136        assert!(!RuntimeState::Recovering.can_process_queue());
137        assert!(!RuntimeState::Stopped.can_process_queue());
138        assert!(!RuntimeState::Destroyed.can_process_queue());
139    }
140
141    // §22 transition table — exhaustive valid transitions
142    #[test]
143    fn initializing_valid_transitions() {
144        let s = RuntimeState::Initializing;
145        assert!(s.can_transition_to(&RuntimeState::Idle));
146        assert!(s.can_transition_to(&RuntimeState::Stopped));
147        assert!(s.can_transition_to(&RuntimeState::Destroyed));
148
149        // Invalid
150        assert!(!s.can_transition_to(&RuntimeState::Running));
151        assert!(!s.can_transition_to(&RuntimeState::Recovering));
152        assert!(!s.can_transition_to(&RuntimeState::Retired));
153        assert!(!s.can_transition_to(&RuntimeState::Initializing));
154    }
155
156    #[test]
157    fn idle_valid_transitions() {
158        let s = RuntimeState::Idle;
159        assert!(s.can_transition_to(&RuntimeState::Running));
160        assert!(s.can_transition_to(&RuntimeState::Retired));
161        assert!(s.can_transition_to(&RuntimeState::Recovering));
162        assert!(s.can_transition_to(&RuntimeState::Stopped));
163        assert!(s.can_transition_to(&RuntimeState::Destroyed));
164
165        // Invalid
166        assert!(!s.can_transition_to(&RuntimeState::Initializing));
167        assert!(!s.can_transition_to(&RuntimeState::Idle));
168    }
169
170    #[test]
171    fn running_valid_transitions() {
172        let s = RuntimeState::Running;
173        assert!(s.can_transition_to(&RuntimeState::Idle));
174        assert!(s.can_transition_to(&RuntimeState::Recovering));
175        assert!(s.can_transition_to(&RuntimeState::Retired));
176        assert!(s.can_transition_to(&RuntimeState::Stopped));
177        assert!(s.can_transition_to(&RuntimeState::Destroyed));
178
179        // Invalid
180        assert!(!s.can_transition_to(&RuntimeState::Initializing));
181        assert!(!s.can_transition_to(&RuntimeState::Running));
182    }
183
184    #[test]
185    fn recovering_valid_transitions() {
186        let s = RuntimeState::Recovering;
187        assert!(s.can_transition_to(&RuntimeState::Idle));
188        assert!(s.can_transition_to(&RuntimeState::Running));
189        assert!(s.can_transition_to(&RuntimeState::Stopped));
190        assert!(s.can_transition_to(&RuntimeState::Destroyed));
191
192        // Invalid
193        assert!(!s.can_transition_to(&RuntimeState::Initializing));
194        assert!(!s.can_transition_to(&RuntimeState::Recovering));
195        assert!(!s.can_transition_to(&RuntimeState::Retired));
196    }
197
198    #[test]
199    fn retired_valid_transitions() {
200        let s = RuntimeState::Retired;
201        assert!(s.can_transition_to(&RuntimeState::Running));
202        assert!(s.can_transition_to(&RuntimeState::Stopped));
203        assert!(s.can_transition_to(&RuntimeState::Destroyed));
204
205        // Invalid
206        assert!(!s.can_transition_to(&RuntimeState::Initializing));
207        assert!(!s.can_transition_to(&RuntimeState::Idle));
208        assert!(!s.can_transition_to(&RuntimeState::Recovering));
209        assert!(!s.can_transition_to(&RuntimeState::Retired));
210    }
211
212    #[test]
213    fn stopped_is_terminal() {
214        let s = RuntimeState::Stopped;
215        assert!(!s.can_transition_to(&RuntimeState::Initializing));
216        assert!(!s.can_transition_to(&RuntimeState::Idle));
217        assert!(!s.can_transition_to(&RuntimeState::Running));
218        assert!(!s.can_transition_to(&RuntimeState::Recovering));
219        assert!(!s.can_transition_to(&RuntimeState::Retired));
220        assert!(!s.can_transition_to(&RuntimeState::Stopped));
221        assert!(!s.can_transition_to(&RuntimeState::Destroyed));
222    }
223
224    #[test]
225    fn destroyed_is_terminal() {
226        let s = RuntimeState::Destroyed;
227        assert!(!s.can_transition_to(&RuntimeState::Initializing));
228        assert!(!s.can_transition_to(&RuntimeState::Idle));
229        assert!(!s.can_transition_to(&RuntimeState::Running));
230        assert!(!s.can_transition_to(&RuntimeState::Recovering));
231        assert!(!s.can_transition_to(&RuntimeState::Retired));
232        assert!(!s.can_transition_to(&RuntimeState::Stopped));
233        assert!(!s.can_transition_to(&RuntimeState::Destroyed));
234    }
235
236    #[test]
237    fn transition_success() {
238        let mut state = RuntimeState::Initializing;
239        assert!(state.transition(RuntimeState::Idle).is_ok());
240        assert_eq!(state, RuntimeState::Idle);
241
242        assert!(state.transition(RuntimeState::Running).is_ok());
243        assert_eq!(state, RuntimeState::Running);
244
245        assert!(state.transition(RuntimeState::Idle).is_ok());
246        assert_eq!(state, RuntimeState::Idle);
247    }
248
249    #[test]
250    fn transition_failure() {
251        let mut state = RuntimeState::Stopped;
252        let result = state.transition(RuntimeState::Idle);
253        assert!(result.is_err());
254        assert_eq!(state, RuntimeState::Stopped); // unchanged
255    }
256
257    #[test]
258    fn serde_roundtrip_all_states() {
259        for state in [
260            RuntimeState::Initializing,
261            RuntimeState::Idle,
262            RuntimeState::Running,
263            RuntimeState::Recovering,
264            RuntimeState::Retired,
265            RuntimeState::Stopped,
266            RuntimeState::Destroyed,
267        ] {
268            let json = serde_json::to_value(state).unwrap();
269            let parsed: RuntimeState = serde_json::from_value(json).unwrap();
270            assert_eq!(state, parsed);
271        }
272    }
273
274    #[test]
275    fn display() {
276        assert_eq!(RuntimeState::Idle.to_string(), "idle");
277        assert_eq!(RuntimeState::Running.to_string(), "running");
278        assert_eq!(RuntimeState::Destroyed.to_string(), "destroyed");
279    }
280}