Skip to main content

synaps_cli/tools/shell/
config.rs

1//! Shell session configuration — parsed from ~/.synaps-cli/config shell.* keys.
2
3use std::time::Duration;
4
5/// Configuration for interactive shell sessions.
6#[derive(Debug, Clone)]
7pub struct ShellConfig {
8    pub max_sessions: usize,
9    pub idle_timeout: Duration,
10    pub readiness_timeout_ms: u64,
11    pub max_readiness_timeout_ms: u64,
12    pub default_rows: u16,
13    pub default_cols: u16,
14    pub prompt_patterns: Vec<String>,
15    pub readiness_strategy: String,
16    pub max_output: usize,
17}
18
19impl Default for ShellConfig {
20    fn default() -> Self {
21        Self {
22            max_sessions: 5,
23            idle_timeout: Duration::from_secs(600),
24            readiness_timeout_ms: 300,
25            max_readiness_timeout_ms: 10_000,
26            default_rows: 24,
27            default_cols: 80,
28            prompt_patterns: default_prompt_patterns(),
29            readiness_strategy: "hybrid".to_string(),
30            max_output: 30000,
31        }
32    }
33}
34
35fn default_prompt_patterns() -> Vec<String> {
36    vec![
37        r"[$#%»] $".into(),
38        r"[$#%»]\s*$".into(),
39        r"\(gdb\)\s*$".into(),
40        r">>>\s*$".into(),
41        r"\.\.\.\:\s*$".into(),
42        r"In \[\d+\]:\s*$".into(),
43        r"irb.*>\s*$".into(),
44        r"mysql>\s*$".into(),
45        r"postgres[=#]>\s*$".into(),
46        r"Password:\s*$".into(),
47        r"\[Y/n\]\s*$".into(),
48        r"\(yes/no.*\)\?\s*$".into(),
49        r"% $".into(),
50        r"% \s*$".into(),
51        r"root@[\w.-]+:[/~].*# $".into(),
52        r"[\w.-]+@[\w.-]+:[/~].*\$ $".into(),
53        r"Enter passphrase.*:\s*$".into(),
54        r"Token:\s*$".into(),
55        r"Verification code:\s*$".into(),
56    ]
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use serial_test::serial;
63    use crate::core::config::load_config;
64
65    #[test]
66    fn test_shell_config_default() {
67        let config = ShellConfig::default();
68        assert_eq!(config.max_sessions, 5);
69        assert_eq!(config.idle_timeout, Duration::from_secs(600));
70        assert_eq!(config.readiness_timeout_ms, 300);
71        assert_eq!(config.max_readiness_timeout_ms, 10_000);
72        assert_eq!(config.default_rows, 24);
73        assert_eq!(config.default_cols, 80);
74        assert_eq!(config.prompt_patterns.len(), 19);
75    }
76
77    #[test]
78    #[serial]
79    fn test_shell_config_from_file() {
80        // These tests must run sequentially since they mutate HOME env var.
81        // Combined into one test to avoid parallel races.
82
83        // --- Subtest 1: Full config parsing ---
84        {
85            let test_dir = std::path::PathBuf::from("/tmp/synaps-shell-test-1/.synaps-cli");
86            let _ = std::fs::create_dir_all(&test_dir);
87            let config_path = test_dir.join("config");
88
89            let config_content = "shell.max_sessions = 10\nshell.idle_timeout = 1200\nshell.readiness_timeout_ms = 500\nshell.max_readiness_timeout_ms = 20000\nshell.default_rows = 30\nshell.default_cols = 120\n";
90            std::fs::write(&config_path, config_content).unwrap();
91
92            let original_home = std::env::var("HOME").ok();
93            let original_base_dir = std::env::var("SYNAPS_BASE_DIR").ok();
94            std::env::remove_var("SYNAPS_BASE_DIR");
95            std::env::set_var("HOME", "/tmp/synaps-shell-test-1");
96            let config = load_config();
97            if let Some(home) = original_home {
98                std::env::set_var("HOME", home);
99            }
100            if let Some(base_dir) = original_base_dir {
101                std::env::set_var("SYNAPS_BASE_DIR", base_dir);
102            }
103            let _ = std::fs::remove_dir_all("/tmp/synaps-shell-test-1");
104
105            assert_eq!(config.shell.max_sessions, 10);
106            assert_eq!(config.shell.idle_timeout, Duration::from_secs(1200));
107            assert_eq!(config.shell.readiness_timeout_ms, 500);
108            assert_eq!(config.shell.max_readiness_timeout_ms, 20000);
109            assert_eq!(config.shell.default_rows, 30);
110            assert_eq!(config.shell.default_cols, 120);
111        }
112
113        // --- Subtest 2: Partial config — missing keys use defaults ---
114        {
115            let test_dir = std::path::PathBuf::from("/tmp/synaps-shell-test-2/.synaps-cli");
116            let _ = std::fs::create_dir_all(&test_dir);
117            let config_path = test_dir.join("config");
118
119            let config_content = "shell.max_sessions = 3\nshell.default_rows = 40\n";
120            std::fs::write(&config_path, config_content).unwrap();
121
122            let original_home = std::env::var("HOME").ok();
123            let original_base_dir = std::env::var("SYNAPS_BASE_DIR").ok();
124            std::env::remove_var("SYNAPS_BASE_DIR");
125            std::env::set_var("HOME", "/tmp/synaps-shell-test-2");
126            let config = load_config();
127            if let Some(home) = original_home {
128                std::env::set_var("HOME", home);
129            }
130            if let Some(base_dir) = original_base_dir {
131                std::env::set_var("SYNAPS_BASE_DIR", base_dir);
132            }
133            let _ = std::fs::remove_dir_all("/tmp/synaps-shell-test-2");
134
135            assert_eq!(config.shell.max_sessions, 3);
136            assert_eq!(config.shell.default_rows, 40);
137            assert_eq!(config.shell.idle_timeout, Duration::from_secs(600));
138            assert_eq!(config.shell.readiness_timeout_ms, 300);
139        }
140
141        // --- Subtest 3: Invalid values → defaults ---
142        {
143            let test_dir = std::path::PathBuf::from("/tmp/synaps-shell-test-3/.synaps-cli");
144            let _ = std::fs::create_dir_all(&test_dir);
145            let config_path = test_dir.join("config");
146
147            let config_content = "shell.max_sessions = not_a_number\nshell.idle_timeout = invalid\n";
148            std::fs::write(&config_path, config_content).unwrap();
149
150            let original_home = std::env::var("HOME").ok();
151            let original_base_dir = std::env::var("SYNAPS_BASE_DIR").ok();
152            std::env::remove_var("SYNAPS_BASE_DIR");
153            std::env::set_var("HOME", "/tmp/synaps-shell-test-3");
154            let config = load_config();
155            if let Some(home) = original_home {
156                std::env::set_var("HOME", home);
157            }
158            if let Some(base_dir) = original_base_dir {
159                std::env::set_var("SYNAPS_BASE_DIR", base_dir);
160            }
161            let _ = std::fs::remove_dir_all("/tmp/synaps-shell-test-3");
162
163            assert_eq!(config.shell.max_sessions, 5);
164            assert_eq!(config.shell.idle_timeout, Duration::from_secs(600));
165        }
166    }
167}