Skip to main content

shell_tunnel/session/
state.rs

1//! Session state machine.
2
3/// Represents the lifecycle state of a shell session.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum SessionState {
6    /// Session has been created but not yet activated.
7    #[default]
8    Created,
9    /// Session is actively processing commands.
10    Active,
11    /// Session is idle, waiting for commands.
12    Idle,
13    /// Session has been terminated and cannot be reused.
14    Terminated,
15}
16
17impl SessionState {
18    /// Check if transition to target state is valid.
19    ///
20    /// Valid transitions:
21    /// - Created -> Active
22    /// - Created -> Idle    (session created and immediately ready for commands)
23    /// - Active -> Idle
24    /// - Active -> Terminated
25    /// - Idle -> Active
26    /// - Idle -> Terminated
27    ///
28    /// Note: `Created -> Idle` exists because a freshly created session holds no
29    /// persistent PTY (each command spawns its own), so it is ready to accept
30    /// commands the moment it is created. This keeps the create→execute flow
31    /// working out-of-the-box without the consumer having to drive an explicit
32    /// activation step (thin-API principle).
33    pub fn can_transition_to(&self, target: SessionState) -> bool {
34        use SessionState::*;
35        matches!(
36            (*self, target),
37            (Created, Active)
38                | (Created, Idle)
39                | (Active, Idle)
40                | (Active, Terminated)
41                | (Idle, Active)
42                | (Idle, Terminated)
43        )
44    }
45
46    /// Attempt to transition to a new state.
47    ///
48    /// Returns `Ok(())` if the transition is valid, or an error otherwise.
49    pub fn transition_to(&mut self, target: SessionState) -> crate::Result<()> {
50        if self.can_transition_to(target) {
51            *self = target;
52            Ok(())
53        } else {
54            Err(crate::error::ShellTunnelError::InvalidStateTransition {
55                from: *self,
56                to: target,
57            })
58        }
59    }
60
61    /// Check if this is a terminal state (no further transitions possible).
62    pub fn is_terminal(&self) -> bool {
63        matches!(self, SessionState::Terminated)
64    }
65
66    /// Check if session can accept commands.
67    pub fn can_execute(&self) -> bool {
68        matches!(self, SessionState::Active | SessionState::Idle)
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn test_valid_transitions() {
78        // Created -> Active
79        let mut state = SessionState::Created;
80        assert!(state.transition_to(SessionState::Active).is_ok());
81        assert_eq!(state, SessionState::Active);
82
83        // Active -> Idle
84        assert!(state.transition_to(SessionState::Idle).is_ok());
85        assert_eq!(state, SessionState::Idle);
86
87        // Idle -> Active (resume)
88        assert!(state.transition_to(SessionState::Active).is_ok());
89        assert_eq!(state, SessionState::Active);
90
91        // Active -> Terminated
92        assert!(state.transition_to(SessionState::Terminated).is_ok());
93        assert_eq!(state, SessionState::Terminated);
94    }
95
96    #[test]
97    fn test_created_to_idle_is_valid() {
98        // A freshly created session is immediately ready for commands.
99        let mut state = SessionState::Created;
100        assert!(state.transition_to(SessionState::Idle).is_ok());
101        assert_eq!(state, SessionState::Idle);
102    }
103
104    #[test]
105    fn test_invalid_idle_to_created() {
106        // Going back to Created is never valid.
107        let mut state = SessionState::Idle;
108        assert!(state.transition_to(SessionState::Created).is_err());
109        // State should remain unchanged
110        assert_eq!(state, SessionState::Idle);
111    }
112
113    #[test]
114    fn test_invalid_from_terminated() {
115        let mut state = SessionState::Terminated;
116        assert!(state.transition_to(SessionState::Active).is_err());
117        assert!(state.transition_to(SessionState::Idle).is_err());
118        assert!(state.transition_to(SessionState::Created).is_err());
119    }
120
121    #[test]
122    fn test_is_terminal() {
123        assert!(!SessionState::Created.is_terminal());
124        assert!(!SessionState::Active.is_terminal());
125        assert!(!SessionState::Idle.is_terminal());
126        assert!(SessionState::Terminated.is_terminal());
127    }
128
129    #[test]
130    fn test_can_execute() {
131        assert!(!SessionState::Created.can_execute());
132        assert!(SessionState::Active.can_execute());
133        assert!(SessionState::Idle.can_execute());
134        assert!(!SessionState::Terminated.can_execute());
135    }
136
137    #[test]
138    fn test_default() {
139        let state = SessionState::default();
140        assert_eq!(state, SessionState::Created);
141    }
142}