Skip to main content

shell_tunnel/session/
context.rs

1//! Session execution bookkeeping.
2
3/// What has run in a session so far.
4///
5/// Bookkeeping only — it does not configure anything. A session holds no shell
6/// between calls (every execute spawns its own `cmd /c` / `sh -c`), so there is
7/// no working directory or environment for it to carry: both are decided per
8/// execute. This type once held a `cwd` and an `env` that nothing read, and a
9/// `StateProbe` helper for recovering them from a persistent shell that does
10/// not exist here.
11#[derive(Debug, Clone, Default)]
12pub struct SessionContext {
13    /// Last command executed.
14    last_command: Option<String>,
15    /// Exit code of last command.
16    last_exit_code: Option<i32>,
17    /// Command execution count.
18    execution_count: u64,
19}
20
21impl SessionContext {
22    /// Create a new empty session context.
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Get the last command executed.
28    pub fn last_command(&self) -> Option<&str> {
29        self.last_command.as_deref()
30    }
31
32    /// Get the exit code of the last command.
33    pub fn last_exit_code(&self) -> Option<i32> {
34        self.last_exit_code
35    }
36
37    /// Get the number of commands executed.
38    pub fn execution_count(&self) -> u64 {
39        self.execution_count
40    }
41
42    /// Record a command execution result.
43    pub fn record_execution(&mut self, command: impl Into<String>, exit_code: Option<i32>) {
44        self.last_command = Some(command.into());
45        self.last_exit_code = exit_code;
46        self.execution_count += 1;
47    }
48
49    /// Check if the last command succeeded.
50    pub fn last_succeeded(&self) -> bool {
51        self.last_exit_code == Some(0)
52    }
53
54    /// Check if the last command failed.
55    pub fn last_failed(&self) -> bool {
56        matches!(self.last_exit_code, Some(code) if code != 0)
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_context_new() {
66        let ctx = SessionContext::new();
67        assert!(ctx.last_command().is_none());
68        assert!(ctx.last_exit_code().is_none());
69        assert_eq!(ctx.execution_count(), 0);
70    }
71
72    #[test]
73    fn test_record_execution() {
74        let mut ctx = SessionContext::new();
75        ctx.record_execution("ls -la", Some(0));
76
77        assert_eq!(ctx.last_command(), Some("ls -la"));
78        assert_eq!(ctx.last_exit_code(), Some(0));
79        assert_eq!(ctx.execution_count(), 1);
80        assert!(ctx.last_succeeded());
81        assert!(!ctx.last_failed());
82    }
83
84    #[test]
85    fn test_execution_count_increments() {
86        let mut ctx = SessionContext::new();
87        ctx.record_execution("first", Some(0));
88        ctx.record_execution("second", Some(1));
89
90        assert_eq!(ctx.execution_count(), 2);
91        assert_eq!(ctx.last_command(), Some("second"));
92        assert!(ctx.last_failed());
93        assert!(!ctx.last_succeeded());
94    }
95
96    #[test]
97    fn a_command_that_never_reported_an_exit_code_neither_succeeded_nor_failed() {
98        let mut ctx = SessionContext::new();
99        ctx.record_execution("killed", None);
100
101        assert!(!ctx.last_succeeded());
102        assert!(!ctx.last_failed());
103    }
104}