Skip to main content

rust_expect/session/
builder.rs

1//! Session builder for constructing sessions with custom configuration.
2//!
3//! This module provides a builder pattern for creating sessions with
4//! customized configuration options.
5
6use std::path::PathBuf;
7use std::time::Duration;
8
9use crate::config::{
10    BufferConfig, EncodingConfig, LineEnding, LoggingConfig, SessionConfig, TimeoutConfig,
11};
12
13/// Builder for creating session configurations.
14#[derive(Debug, Clone)]
15pub struct SessionBuilder {
16    config: SessionConfig,
17}
18
19impl SessionBuilder {
20    /// Create a new session builder with default configuration.
21    #[must_use]
22    pub fn new() -> Self {
23        Self {
24            config: SessionConfig::default(),
25        }
26    }
27
28    /// Set the command to execute.
29    #[must_use]
30    pub fn command(mut self, command: impl Into<String>) -> Self {
31        self.config.command = command.into();
32        self
33    }
34
35    /// Set the command arguments.
36    #[must_use]
37    pub fn args<I, S>(mut self, args: I) -> Self
38    where
39        I: IntoIterator<Item = S>,
40        S: Into<String>,
41    {
42        self.config.args = args.into_iter().map(Into::into).collect();
43        self
44    }
45
46    /// Add a single argument.
47    #[must_use]
48    pub fn arg(mut self, arg: impl Into<String>) -> Self {
49        self.config.args.push(arg.into());
50        self
51    }
52
53    /// Set environment variables.
54    #[must_use]
55    pub fn envs<I, K, V>(mut self, envs: I) -> Self
56    where
57        I: IntoIterator<Item = (K, V)>,
58        K: Into<String>,
59        V: Into<String>,
60    {
61        self.config.env = envs
62            .into_iter()
63            .map(|(k, v)| (k.into(), v.into()))
64            .collect();
65        self
66    }
67
68    /// Set a single environment variable.
69    #[must_use]
70    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
71        self.config.env.insert(key.into(), value.into());
72        self
73    }
74
75    /// Set the working directory.
76    #[must_use]
77    pub fn working_directory(mut self, path: impl Into<PathBuf>) -> Self {
78        self.config.working_dir = Some(path.into());
79        self
80    }
81
82    /// Set the terminal dimensions (width, height).
83    #[must_use]
84    pub const fn dimensions(mut self, cols: u16, rows: u16) -> Self {
85        self.config.dimensions = (cols, rows);
86        self
87    }
88
89    /// Set the default timeout.
90    #[must_use]
91    pub const fn timeout(mut self, timeout: Duration) -> Self {
92        self.config.timeout.default = timeout;
93        self
94    }
95
96    /// Set the timeout configuration.
97    #[must_use]
98    pub const fn timeout_config(mut self, config: TimeoutConfig) -> Self {
99        self.config.timeout = config;
100        self
101    }
102
103    /// Set the buffer max size.
104    #[must_use]
105    pub const fn buffer_max_size(mut self, max_size: usize) -> Self {
106        self.config.buffer.max_size = max_size;
107        self
108    }
109
110    /// Set the buffer configuration.
111    #[must_use]
112    pub const fn buffer_config(mut self, config: BufferConfig) -> Self {
113        self.config.buffer = config;
114        self
115    }
116
117    /// Set the line ending style.
118    #[must_use]
119    pub const fn line_ending(mut self, line_ending: LineEnding) -> Self {
120        self.config.line_ending = line_ending;
121        self
122    }
123
124    /// Use Unix line endings (LF).
125    #[must_use]
126    pub const fn unix_line_endings(self) -> Self {
127        self.line_ending(LineEnding::Lf)
128    }
129
130    /// Use the Windows line ending for *sending*: CR.
131    ///
132    /// This is deliberately `Cr` and not `CrLf`. Like its
133    /// [`unix_line_endings`](Self::unix_line_endings) sibling — which sets `Lf`, the
134    /// Unix ENTER — this selects the terminator the platform's terminal sends for the
135    /// ENTER key, not the platform's text-file convention. `\r` is what Windows
136    /// Terminal sends to `ConPTY` for ENTER.
137    ///
138    /// `CrLf` also submits a line today, but only because conhost swallows the
139    /// trailing LF; against a child with `ENABLE_LINE_INPUT` disabled an LF that did
140    /// arrive would submit a second ENTER. To normalize *text* to CRLF, use
141    /// [`LineEndingStyle`](crate::encoding::LineEndingStyle) instead — that is a
142    /// separate concern from the send terminator.
143    #[must_use]
144    pub const fn windows_line_endings(self) -> Self {
145        self.line_ending(LineEnding::Cr)
146    }
147
148    /// Set the encoding configuration.
149    #[must_use]
150    pub const fn encoding(mut self, config: EncodingConfig) -> Self {
151        self.config.encoding = config;
152        self
153    }
154
155    /// Set the logging configuration.
156    #[must_use]
157    pub fn logging(mut self, config: LoggingConfig) -> Self {
158        self.config.logging = config;
159        self
160    }
161
162    /// Enable logging to a file.
163    #[must_use]
164    pub fn log_to_file(mut self, path: impl Into<PathBuf>) -> Self {
165        self.config.logging.log_file = Some(path.into());
166        self
167    }
168
169    /// Build the session configuration.
170    #[must_use]
171    pub fn build(self) -> SessionConfig {
172        self.config
173    }
174}
175
176impl Default for SessionBuilder {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182impl From<SessionBuilder> for SessionConfig {
183    fn from(builder: SessionBuilder) -> Self {
184        builder.build()
185    }
186}
187
188/// Quick session configuration for common use cases.
189pub struct QuickSession;
190
191impl QuickSession {
192    /// Create a session config for a shell command.
193    #[must_use]
194    pub fn shell() -> SessionConfig {
195        SessionBuilder::new().command(Self::default_shell()).build()
196    }
197
198    /// Create a session config for bash.
199    #[must_use]
200    pub fn bash() -> SessionConfig {
201        SessionBuilder::new()
202            .command("/bin/bash")
203            .arg("--norc")
204            .arg("--noprofile")
205            .build()
206    }
207
208    /// Create a session config for a custom command.
209    #[must_use]
210    pub fn command(cmd: impl Into<String>) -> SessionConfig {
211        SessionBuilder::new().command(cmd).build()
212    }
213
214    /// Create a session config for SSH.
215    #[must_use]
216    pub fn ssh(host: &str) -> SessionConfig {
217        SessionBuilder::new()
218            .command("ssh")
219            .arg(host)
220            .timeout(Duration::from_secs(30))
221            .build()
222    }
223
224    /// Create a session config for SSH with user.
225    #[must_use]
226    pub fn ssh_user(user: &str, host: &str) -> SessionConfig {
227        SessionBuilder::new()
228            .command("ssh")
229            .arg(format!("{user}@{host}"))
230            .timeout(Duration::from_secs(30))
231            .build()
232    }
233
234    /// Create a session config for telnet.
235    #[must_use]
236    pub fn telnet(host: &str, port: u16) -> SessionConfig {
237        SessionBuilder::new()
238            .command("telnet")
239            .arg(host)
240            .arg(port.to_string())
241            .timeout(Duration::from_secs(30))
242            .build()
243    }
244
245    /// Create a session config for Python.
246    #[must_use]
247    pub fn python() -> SessionConfig {
248        SessionBuilder::new()
249            .command(if cfg!(windows) { "python" } else { "python3" })
250            .arg("-i")
251            .build()
252    }
253
254    /// Create a session config for Windows Command Prompt.
255    ///
256    /// This configures a cmd.exe session with Windows-style line endings.
257    #[must_use]
258    pub fn cmd() -> SessionConfig {
259        SessionBuilder::new()
260            .command("cmd.exe")
261            .windows_line_endings()
262            .build()
263    }
264
265    /// Create a session config for `PowerShell`.
266    ///
267    /// Works with both Windows `PowerShell` (`powershell.exe`) and
268    /// `PowerShell` Core (`pwsh.exe`). Defaults to `powershell.exe` on Windows,
269    /// `pwsh` on other platforms.
270    #[must_use]
271    pub fn powershell() -> SessionConfig {
272        let command = if cfg!(windows) {
273            "powershell.exe"
274        } else {
275            "pwsh"
276        };
277        SessionBuilder::new()
278            .command(command)
279            .arg("-NoLogo")
280            .arg("-NoProfile")
281            .build()
282    }
283
284    /// Create a session config for zsh.
285    #[must_use]
286    pub fn zsh() -> SessionConfig {
287        SessionBuilder::new()
288            .command("/bin/zsh")
289            .arg("--no-rcs")
290            .build()
291    }
292
293    /// Create a session config for fish shell.
294    #[must_use]
295    pub fn fish() -> SessionConfig {
296        SessionBuilder::new()
297            .command("fish")
298            .arg("--no-config")
299            .build()
300    }
301
302    /// Create a session config for a REPL.
303    #[must_use]
304    pub fn repl(cmd: impl Into<String>) -> SessionConfig {
305        SessionBuilder::new().command(cmd).build()
306    }
307
308    /// Create a session config for Node.js REPL.
309    #[must_use]
310    pub fn node() -> SessionConfig {
311        SessionBuilder::new().command("node").build()
312    }
313
314    /// Create a session config for Ruby IRB.
315    #[must_use]
316    pub fn ruby() -> SessionConfig {
317        SessionBuilder::new()
318            .command("irb")
319            .arg("--simple-prompt")
320            .build()
321    }
322
323    /// Create a session config for `MySQL` client.
324    #[must_use]
325    pub fn mysql(host: &str, user: &str, database: &str) -> SessionConfig {
326        SessionBuilder::new()
327            .command("mysql")
328            .arg("-h")
329            .arg(host)
330            .arg("-u")
331            .arg(user)
332            .arg(database)
333            .timeout(Duration::from_secs(30))
334            .build()
335    }
336
337    /// Create a session config for `MySQL` client with password prompt.
338    #[must_use]
339    pub fn mysql_password(host: &str, user: &str, database: &str) -> SessionConfig {
340        SessionBuilder::new()
341            .command("mysql")
342            .arg("-h")
343            .arg(host)
344            .arg("-u")
345            .arg(user)
346            .arg("-p")
347            .arg(database)
348            .timeout(Duration::from_secs(30))
349            .build()
350    }
351
352    /// Create a session config for `PostgreSQL` client.
353    #[must_use]
354    pub fn psql(host: &str, user: &str, database: &str) -> SessionConfig {
355        SessionBuilder::new()
356            .command("psql")
357            .arg("-h")
358            .arg(host)
359            .arg("-U")
360            .arg(user)
361            .arg(database)
362            .timeout(Duration::from_secs(30))
363            .build()
364    }
365
366    /// Create a session config for Docker exec into a container.
367    #[must_use]
368    pub fn docker_exec(container: &str) -> SessionConfig {
369        SessionBuilder::new()
370            .command("docker")
371            .arg("exec")
372            .arg("-it")
373            .arg(container)
374            .arg("/bin/sh")
375            .build()
376    }
377
378    /// Create a session config for Docker exec with a specific shell.
379    #[must_use]
380    pub fn docker_exec_shell(container: &str, shell: &str) -> SessionConfig {
381        SessionBuilder::new()
382            .command("docker")
383            .arg("exec")
384            .arg("-it")
385            .arg(container)
386            .arg(shell)
387            .build()
388    }
389
390    /// Create a session config for Docker run with interactive shell.
391    #[must_use]
392    pub fn docker_run(image: &str) -> SessionConfig {
393        SessionBuilder::new()
394            .command("docker")
395            .arg("run")
396            .arg("-it")
397            .arg("--rm")
398            .arg(image)
399            .build()
400    }
401
402    /// Create a session config for Redis CLI.
403    #[must_use]
404    pub fn redis_cli(host: &str) -> SessionConfig {
405        SessionBuilder::new()
406            .command("redis-cli")
407            .arg("-h")
408            .arg(host)
409            .build()
410    }
411
412    /// Create a session config for `MongoDB` shell.
413    #[must_use]
414    pub fn mongosh(uri: &str) -> SessionConfig {
415        SessionBuilder::new()
416            .command("mongosh")
417            .arg(uri)
418            .timeout(Duration::from_secs(30))
419            .build()
420    }
421
422    /// Create a session config for `SQLite`.
423    #[must_use]
424    pub fn sqlite(database: &str) -> SessionConfig {
425        SessionBuilder::new()
426            .command("sqlite3")
427            .arg(database)
428            .build()
429    }
430
431    /// Create a session config for GDB debugger.
432    #[must_use]
433    pub fn gdb(program: &str) -> SessionConfig {
434        SessionBuilder::new().command("gdb").arg(program).build()
435    }
436
437    /// Create a session config for LLDB debugger.
438    #[must_use]
439    pub fn lldb(program: &str) -> SessionConfig {
440        SessionBuilder::new().command("lldb").arg(program).build()
441    }
442
443    /// Create a session config for Lua REPL.
444    #[must_use]
445    pub fn lua() -> SessionConfig {
446        SessionBuilder::new().command("lua").arg("-i").build()
447    }
448
449    /// Create a session config for Perl debugger.
450    #[must_use]
451    pub fn perl() -> SessionConfig {
452        SessionBuilder::new().command("perl").arg("-de0").build()
453    }
454
455    /// Create a session config for R REPL.
456    #[must_use]
457    pub fn r() -> SessionConfig {
458        SessionBuilder::new()
459            .command("R")
460            .arg("--no-save")
461            .arg("--no-restore")
462            .build()
463    }
464
465    /// Create a session config for Julia REPL.
466    #[must_use]
467    pub fn julia() -> SessionConfig {
468        SessionBuilder::new().command("julia").build()
469    }
470
471    /// Create a session config for Scala REPL.
472    #[must_use]
473    pub fn scala() -> SessionConfig {
474        SessionBuilder::new().command("scala").build()
475    }
476
477    /// Create a session config for Elixir `IEx`.
478    #[must_use]
479    pub fn iex() -> SessionConfig {
480        SessionBuilder::new().command("iex").build()
481    }
482
483    /// Create a session config for Clojure REPL.
484    #[must_use]
485    pub fn clojure() -> SessionConfig {
486        SessionBuilder::new().command("clj").build()
487    }
488
489    /// Create a session config for Haskell `GHCi`.
490    #[must_use]
491    pub fn ghci() -> SessionConfig {
492        SessionBuilder::new().command("ghci").build()
493    }
494
495    /// Create a session config for OCaml REPL.
496    #[must_use]
497    pub fn ocaml() -> SessionConfig {
498        SessionBuilder::new().command("ocaml").build()
499    }
500
501    /// Create a session config for kubectl exec into a pod.
502    #[must_use]
503    pub fn kubectl_exec(pod: &str) -> SessionConfig {
504        SessionBuilder::new()
505            .command("kubectl")
506            .arg("exec")
507            .arg("-it")
508            .arg(pod)
509            .arg("--")
510            .arg("/bin/sh")
511            .build()
512    }
513
514    /// Create a session config for kubectl exec with namespace.
515    #[must_use]
516    pub fn kubectl_exec_ns(namespace: &str, pod: &str, shell: &str) -> SessionConfig {
517        SessionBuilder::new()
518            .command("kubectl")
519            .arg("exec")
520            .arg("-it")
521            .arg("-n")
522            .arg(namespace)
523            .arg(pod)
524            .arg("--")
525            .arg(shell)
526            .build()
527    }
528
529    /// Create a session config for screen attach.
530    #[must_use]
531    pub fn screen_attach(session_name: &str) -> SessionConfig {
532        SessionBuilder::new()
533            .command("screen")
534            .arg("-r")
535            .arg(session_name)
536            .build()
537    }
538
539    /// Create a session config for tmux attach.
540    #[must_use]
541    pub fn tmux_attach(session_name: &str) -> SessionConfig {
542        SessionBuilder::new()
543            .command("tmux")
544            .arg("attach")
545            .arg("-t")
546            .arg(session_name)
547            .build()
548    }
549
550    /// Create a session config for SSH with a specific port.
551    #[must_use]
552    pub fn ssh_port(host: &str, port: u16) -> SessionConfig {
553        SessionBuilder::new()
554            .command("ssh")
555            .arg("-p")
556            .arg(port.to_string())
557            .arg(host)
558            .timeout(Duration::from_secs(30))
559            .build()
560    }
561
562    /// Create a session config for SSH with user and port.
563    #[must_use]
564    pub fn ssh_full(user: &str, host: &str, port: u16) -> SessionConfig {
565        SessionBuilder::new()
566            .command("ssh")
567            .arg("-p")
568            .arg(port.to_string())
569            .arg(format!("{user}@{host}"))
570            .timeout(Duration::from_secs(30))
571            .build()
572    }
573
574    /// Create a session config for SSH with a specific identity file.
575    #[must_use]
576    pub fn ssh_key(user: &str, host: &str, key_file: &str) -> SessionConfig {
577        SessionBuilder::new()
578            .command("ssh")
579            .arg("-i")
580            .arg(key_file)
581            .arg(format!("{user}@{host}"))
582            .timeout(Duration::from_secs(30))
583            .build()
584    }
585
586    /// Create a session config for Vagrant SSH.
587    #[must_use]
588    pub fn vagrant_ssh() -> SessionConfig {
589        SessionBuilder::new()
590            .command("vagrant")
591            .arg("ssh")
592            .timeout(Duration::from_secs(30))
593            .build()
594    }
595
596    /// Create a session config for Vagrant SSH to a specific machine.
597    #[must_use]
598    pub fn vagrant_ssh_machine(machine: &str) -> SessionConfig {
599        SessionBuilder::new()
600            .command("vagrant")
601            .arg("ssh")
602            .arg(machine)
603            .timeout(Duration::from_secs(30))
604            .build()
605    }
606
607    /// Create a session config for SFTP.
608    #[must_use]
609    pub fn sftp(host: &str) -> SessionConfig {
610        SessionBuilder::new()
611            .command("sftp")
612            .arg(host)
613            .timeout(Duration::from_secs(30))
614            .build()
615    }
616
617    /// Create a session config for SFTP with user.
618    #[must_use]
619    pub fn sftp_user(user: &str, host: &str) -> SessionConfig {
620        SessionBuilder::new()
621            .command("sftp")
622            .arg(format!("{user}@{host}"))
623            .timeout(Duration::from_secs(30))
624            .build()
625    }
626
627    /// Create a session config for FTP.
628    #[must_use]
629    pub fn ftp(host: &str) -> SessionConfig {
630        SessionBuilder::new()
631            .command("ftp")
632            .arg(host)
633            .timeout(Duration::from_secs(30))
634            .build()
635    }
636
637    /// Create a session config for netcat interactive mode.
638    #[must_use]
639    pub fn netcat(host: &str, port: u16) -> SessionConfig {
640        SessionBuilder::new()
641            .command("nc")
642            .arg(host)
643            .arg(port.to_string())
644            .build()
645    }
646
647    /// Create a session config for socat interactive mode.
648    #[must_use]
649    pub fn socat(address: &str) -> SessionConfig {
650        SessionBuilder::new()
651            .command("socat")
652            .arg("-")
653            .arg(address)
654            .build()
655    }
656
657    /// Create a session config for minicom serial terminal.
658    #[must_use]
659    pub fn minicom(device: &str) -> SessionConfig {
660        SessionBuilder::new()
661            .command("minicom")
662            .arg("-D")
663            .arg(device)
664            .build()
665    }
666
667    /// Create a session config for screen serial terminal.
668    #[must_use]
669    pub fn screen_serial(device: &str, baud_rate: u32) -> SessionConfig {
670        SessionBuilder::new()
671            .command("screen")
672            .arg(device)
673            .arg(baud_rate.to_string())
674            .build()
675    }
676
677    /// Create a session config for picocom serial terminal.
678    #[must_use]
679    pub fn picocom(device: &str, baud_rate: u32) -> SessionConfig {
680        SessionBuilder::new()
681            .command("picocom")
682            .arg("-b")
683            .arg(baud_rate.to_string())
684            .arg(device)
685            .build()
686    }
687
688    /// Create a session config for AWS SSM session.
689    #[must_use]
690    pub fn aws_ssm(instance_id: &str) -> SessionConfig {
691        SessionBuilder::new()
692            .command("aws")
693            .arg("ssm")
694            .arg("start-session")
695            .arg("--target")
696            .arg(instance_id)
697            .timeout(Duration::from_secs(60))
698            .build()
699    }
700
701    /// Create a session config for Azure VM serial console.
702    #[must_use]
703    pub fn az_serial_console(resource_group: &str, vm_name: &str) -> SessionConfig {
704        SessionBuilder::new()
705            .command("az")
706            .arg("serial-console")
707            .arg("connect")
708            .arg("--resource-group")
709            .arg(resource_group)
710            .arg("--name")
711            .arg(vm_name)
712            .timeout(Duration::from_secs(60))
713            .build()
714    }
715
716    /// Create a session config for GCP SSH.
717    #[must_use]
718    pub fn gcloud_ssh(instance: &str, zone: &str) -> SessionConfig {
719        SessionBuilder::new()
720            .command("gcloud")
721            .arg("compute")
722            .arg("ssh")
723            .arg(instance)
724            .arg("--zone")
725            .arg(zone)
726            .timeout(Duration::from_secs(60))
727            .build()
728    }
729
730    /// Create a session config for Rust REPL (evcxr).
731    #[must_use]
732    pub fn evcxr() -> SessionConfig {
733        SessionBuilder::new().command("evcxr").build()
734    }
735
736    /// Create a session config for Go playground.
737    #[must_use]
738    pub fn gore() -> SessionConfig {
739        SessionBuilder::new().command("gore").build()
740    }
741
742    /// Create a session config for PHP interactive mode.
743    #[must_use]
744    pub fn php() -> SessionConfig {
745        SessionBuilder::new().command("php").arg("-a").build()
746    }
747
748    /// Create a session config for Swift REPL.
749    #[must_use]
750    pub fn swift() -> SessionConfig {
751        SessionBuilder::new().command("swift").build()
752    }
753
754    /// Create a session config for Kotlin REPL.
755    #[must_use]
756    pub fn kotlin() -> SessionConfig {
757        SessionBuilder::new().command("kotlin").build()
758    }
759
760    /// Create a session config for Groovy console.
761    #[must_use]
762    pub fn groovysh() -> SessionConfig {
763        SessionBuilder::new().command("groovysh").build()
764    }
765
766    /// Create a session config for TypeScript REPL (ts-node).
767    #[must_use]
768    pub fn ts_node() -> SessionConfig {
769        SessionBuilder::new().command("ts-node").build()
770    }
771
772    /// Create a session config for Deno REPL.
773    #[must_use]
774    pub fn deno() -> SessionConfig {
775        SessionBuilder::new().command("deno").build()
776    }
777
778    /// Create a session config for Bun REPL.
779    #[must_use]
780    pub fn bun() -> SessionConfig {
781        SessionBuilder::new().command("bun").arg("repl").build()
782    }
783
784    /// Get the default shell for the current platform.
785    #[must_use]
786    pub fn default_shell() -> String {
787        std::env::var("SHELL").unwrap_or_else(|_| {
788            if cfg!(windows) {
789                "cmd.exe".to_string()
790            } else {
791                "/bin/sh".to_string()
792            }
793        })
794    }
795}
796
797#[cfg(test)]
798mod tests {
799    use super::*;
800
801    #[test]
802    fn builder_basic() {
803        let config = SessionBuilder::new()
804            .command("/bin/bash")
805            .arg("-c")
806            .arg("echo hello")
807            .build();
808
809        assert_eq!(config.command, "/bin/bash");
810        assert_eq!(config.args, vec!["-c", "echo hello"]);
811    }
812
813    #[test]
814    fn builder_env() {
815        let config = SessionBuilder::new()
816            .command("test")
817            .env("FOO", "bar")
818            .env("BAZ", "qux")
819            .build();
820
821        assert_eq!(config.env.get("FOO"), Some(&"bar".to_string()));
822        assert_eq!(config.env.get("BAZ"), Some(&"qux".to_string()));
823    }
824
825    #[test]
826    fn builder_timeout() {
827        let config = SessionBuilder::new()
828            .command("test")
829            .timeout(Duration::from_secs(60))
830            .build();
831
832        assert_eq!(config.timeout.default, Duration::from_secs(60));
833    }
834
835    #[test]
836    fn quick_session_bash() {
837        let config = QuickSession::bash();
838        assert_eq!(config.command, "/bin/bash");
839        assert!(config.args.contains(&"--norc".to_string()));
840    }
841
842    #[test]
843    fn quick_session_ssh() {
844        let config = QuickSession::ssh_user("admin", "example.com");
845        assert_eq!(config.command, "ssh");
846        assert!(config.args.contains(&"admin@example.com".to_string()));
847    }
848
849    #[test]
850    fn quick_session_cmd() {
851        let config = QuickSession::cmd();
852        assert_eq!(config.command, "cmd.exe");
853        assert_eq!(config.line_ending, LineEnding::Cr);
854    }
855
856    #[test]
857    fn quick_session_powershell() {
858        let config = QuickSession::powershell();
859        #[cfg(windows)]
860        assert_eq!(config.command, "powershell.exe");
861        #[cfg(not(windows))]
862        assert_eq!(config.command, "pwsh");
863        assert!(config.args.contains(&"-NoLogo".to_string()));
864        assert!(config.args.contains(&"-NoProfile".to_string()));
865    }
866
867    #[test]
868    fn quick_session_zsh() {
869        let config = QuickSession::zsh();
870        assert_eq!(config.command, "/bin/zsh");
871        assert!(config.args.contains(&"--no-rcs".to_string()));
872    }
873
874    #[test]
875    fn quick_session_fish() {
876        let config = QuickSession::fish();
877        assert_eq!(config.command, "fish");
878        assert!(config.args.contains(&"--no-config".to_string()));
879    }
880
881    #[test]
882    fn quick_session_python() {
883        let config = QuickSession::python();
884        #[cfg(windows)]
885        assert_eq!(config.command, "python");
886        #[cfg(not(windows))]
887        assert_eq!(config.command, "python3");
888        assert!(config.args.contains(&"-i".to_string()));
889    }
890
891    #[test]
892    fn quick_session_node() {
893        let config = QuickSession::node();
894        assert_eq!(config.command, "node");
895    }
896
897    #[test]
898    fn quick_session_ruby() {
899        let config = QuickSession::ruby();
900        assert_eq!(config.command, "irb");
901        assert!(config.args.contains(&"--simple-prompt".to_string()));
902    }
903
904    #[test]
905    fn quick_session_mysql() {
906        let config = QuickSession::mysql("localhost", "root", "testdb");
907        assert_eq!(config.command, "mysql");
908        assert!(config.args.contains(&"-h".to_string()));
909        assert!(config.args.contains(&"localhost".to_string()));
910        assert!(config.args.contains(&"-u".to_string()));
911        assert!(config.args.contains(&"root".to_string()));
912        assert!(config.args.contains(&"testdb".to_string()));
913    }
914
915    #[test]
916    fn quick_session_psql() {
917        let config = QuickSession::psql("localhost", "postgres", "mydb");
918        assert_eq!(config.command, "psql");
919        assert!(config.args.contains(&"-h".to_string()));
920        assert!(config.args.contains(&"-U".to_string()));
921        assert!(config.args.contains(&"postgres".to_string()));
922    }
923
924    #[test]
925    fn quick_session_docker_exec() {
926        let config = QuickSession::docker_exec("my-container");
927        assert_eq!(config.command, "docker");
928        assert!(config.args.contains(&"exec".to_string()));
929        assert!(config.args.contains(&"-it".to_string()));
930        assert!(config.args.contains(&"my-container".to_string()));
931        assert!(config.args.contains(&"/bin/sh".to_string()));
932    }
933
934    #[test]
935    fn quick_session_docker_run() {
936        let config = QuickSession::docker_run("ubuntu:latest");
937        assert_eq!(config.command, "docker");
938        assert!(config.args.contains(&"run".to_string()));
939        assert!(config.args.contains(&"-it".to_string()));
940        assert!(config.args.contains(&"--rm".to_string()));
941        assert!(config.args.contains(&"ubuntu:latest".to_string()));
942    }
943
944    #[test]
945    fn quick_session_redis() {
946        let config = QuickSession::redis_cli("redis.example.com");
947        assert_eq!(config.command, "redis-cli");
948        assert!(config.args.contains(&"-h".to_string()));
949        assert!(config.args.contains(&"redis.example.com".to_string()));
950    }
951
952    #[test]
953    fn quick_session_sqlite() {
954        let config = QuickSession::sqlite("test.db");
955        assert_eq!(config.command, "sqlite3");
956        assert!(config.args.contains(&"test.db".to_string()));
957    }
958
959    #[test]
960    fn quick_session_gdb() {
961        let config = QuickSession::gdb("./my_program");
962        assert_eq!(config.command, "gdb");
963        assert!(config.args.contains(&"./my_program".to_string()));
964    }
965
966    #[test]
967    fn quick_session_kubectl() {
968        let config = QuickSession::kubectl_exec("my-pod");
969        assert_eq!(config.command, "kubectl");
970        assert!(config.args.contains(&"exec".to_string()));
971        assert!(config.args.contains(&"-it".to_string()));
972        assert!(config.args.contains(&"my-pod".to_string()));
973        assert!(config.args.contains(&"--".to_string()));
974        assert!(config.args.contains(&"/bin/sh".to_string()));
975    }
976
977    #[test]
978    fn quick_session_kubectl_ns() {
979        let config = QuickSession::kubectl_exec_ns("production", "api-pod", "/bin/bash");
980        assert_eq!(config.command, "kubectl");
981        assert!(config.args.contains(&"-n".to_string()));
982        assert!(config.args.contains(&"production".to_string()));
983        assert!(config.args.contains(&"api-pod".to_string()));
984        assert!(config.args.contains(&"/bin/bash".to_string()));
985    }
986
987    #[test]
988    fn quick_session_repls() {
989        // Test various REPL helpers
990        assert_eq!(QuickSession::lua().command, "lua");
991        assert_eq!(QuickSession::julia().command, "julia");
992        assert_eq!(QuickSession::scala().command, "scala");
993        assert_eq!(QuickSession::iex().command, "iex");
994        assert_eq!(QuickSession::clojure().command, "clj");
995        assert_eq!(QuickSession::ghci().command, "ghci");
996        assert_eq!(QuickSession::ocaml().command, "ocaml");
997        assert_eq!(QuickSession::r().command, "R");
998    }
999
1000    #[test]
1001    fn quick_session_tmux_screen() {
1002        let config = QuickSession::tmux_attach("mysession");
1003        assert_eq!(config.command, "tmux");
1004        assert!(config.args.contains(&"attach".to_string()));
1005        assert!(config.args.contains(&"-t".to_string()));
1006        assert!(config.args.contains(&"mysession".to_string()));
1007
1008        let config = QuickSession::screen_attach("myscreen");
1009        assert_eq!(config.command, "screen");
1010        assert!(config.args.contains(&"-r".to_string()));
1011        assert!(config.args.contains(&"myscreen".to_string()));
1012    }
1013
1014    #[test]
1015    fn quick_session_ssh_variants() {
1016        // ssh_port
1017        let config = QuickSession::ssh_port("example.com", 2222);
1018        assert_eq!(config.command, "ssh");
1019        assert!(config.args.contains(&"-p".to_string()));
1020        assert!(config.args.contains(&"2222".to_string()));
1021        assert!(config.args.contains(&"example.com".to_string()));
1022
1023        // ssh_full
1024        let config = QuickSession::ssh_full("admin", "server.com", 2222);
1025        assert_eq!(config.command, "ssh");
1026        assert!(config.args.contains(&"-p".to_string()));
1027        assert!(config.args.contains(&"2222".to_string()));
1028        assert!(config.args.contains(&"admin@server.com".to_string()));
1029
1030        // ssh_key
1031        let config = QuickSession::ssh_key("root", "host.com", "/path/to/key");
1032        assert_eq!(config.command, "ssh");
1033        assert!(config.args.contains(&"-i".to_string()));
1034        assert!(config.args.contains(&"/path/to/key".to_string()));
1035        assert!(config.args.contains(&"root@host.com".to_string()));
1036    }
1037
1038    #[test]
1039    fn quick_session_vagrant() {
1040        let config = QuickSession::vagrant_ssh();
1041        assert_eq!(config.command, "vagrant");
1042        assert!(config.args.contains(&"ssh".to_string()));
1043
1044        let config = QuickSession::vagrant_ssh_machine("web");
1045        assert_eq!(config.command, "vagrant");
1046        assert!(config.args.contains(&"ssh".to_string()));
1047        assert!(config.args.contains(&"web".to_string()));
1048    }
1049
1050    #[test]
1051    fn quick_session_file_transfer() {
1052        let config = QuickSession::sftp("server.com");
1053        assert_eq!(config.command, "sftp");
1054        assert!(config.args.contains(&"server.com".to_string()));
1055
1056        let config = QuickSession::sftp_user("admin", "server.com");
1057        assert_eq!(config.command, "sftp");
1058        assert!(config.args.contains(&"admin@server.com".to_string()));
1059
1060        let config = QuickSession::ftp("ftp.example.com");
1061        assert_eq!(config.command, "ftp");
1062        assert!(config.args.contains(&"ftp.example.com".to_string()));
1063    }
1064
1065    #[test]
1066    fn quick_session_network_tools() {
1067        let config = QuickSession::netcat("localhost", 8080);
1068        assert_eq!(config.command, "nc");
1069        assert!(config.args.contains(&"localhost".to_string()));
1070        assert!(config.args.contains(&"8080".to_string()));
1071
1072        let config = QuickSession::socat("TCP:server:1234");
1073        assert_eq!(config.command, "socat");
1074        assert!(config.args.contains(&"-".to_string()));
1075        assert!(config.args.contains(&"TCP:server:1234".to_string()));
1076    }
1077
1078    #[test]
1079    fn quick_session_serial_terminals() {
1080        let config = QuickSession::minicom("/dev/ttyUSB0");
1081        assert_eq!(config.command, "minicom");
1082        assert!(config.args.contains(&"-D".to_string()));
1083        assert!(config.args.contains(&"/dev/ttyUSB0".to_string()));
1084
1085        let config = QuickSession::screen_serial("/dev/ttyACM0", 115_200);
1086        assert_eq!(config.command, "screen");
1087        assert!(config.args.contains(&"/dev/ttyACM0".to_string()));
1088        assert!(config.args.contains(&"115200".to_string()));
1089
1090        let config = QuickSession::picocom("/dev/ttyS0", 9600);
1091        assert_eq!(config.command, "picocom");
1092        assert!(config.args.contains(&"-b".to_string()));
1093        assert!(config.args.contains(&"9600".to_string()));
1094        assert!(config.args.contains(&"/dev/ttyS0".to_string()));
1095    }
1096
1097    #[test]
1098    fn quick_session_cloud_providers() {
1099        let config = QuickSession::aws_ssm("i-1234567890abcdef0");
1100        assert_eq!(config.command, "aws");
1101        assert!(config.args.contains(&"ssm".to_string()));
1102        assert!(config.args.contains(&"start-session".to_string()));
1103        assert!(config.args.contains(&"--target".to_string()));
1104        assert!(config.args.contains(&"i-1234567890abcdef0".to_string()));
1105
1106        let config = QuickSession::az_serial_console("my-rg", "my-vm");
1107        assert_eq!(config.command, "az");
1108        assert!(config.args.contains(&"serial-console".to_string()));
1109        assert!(config.args.contains(&"connect".to_string()));
1110        assert!(config.args.contains(&"--resource-group".to_string()));
1111        assert!(config.args.contains(&"my-rg".to_string()));
1112        assert!(config.args.contains(&"--name".to_string()));
1113        assert!(config.args.contains(&"my-vm".to_string()));
1114
1115        let config = QuickSession::gcloud_ssh("instance-1", "us-central1-a");
1116        assert_eq!(config.command, "gcloud");
1117        assert!(config.args.contains(&"compute".to_string()));
1118        assert!(config.args.contains(&"ssh".to_string()));
1119        assert!(config.args.contains(&"instance-1".to_string()));
1120        assert!(config.args.contains(&"--zone".to_string()));
1121        assert!(config.args.contains(&"us-central1-a".to_string()));
1122    }
1123
1124    #[test]
1125    fn quick_session_additional_repls() {
1126        // Rust REPL
1127        assert_eq!(QuickSession::evcxr().command, "evcxr");
1128
1129        // Go REPL
1130        assert_eq!(QuickSession::gore().command, "gore");
1131
1132        // PHP
1133        let config = QuickSession::php();
1134        assert_eq!(config.command, "php");
1135        assert!(config.args.contains(&"-a".to_string()));
1136
1137        // Swift
1138        assert_eq!(QuickSession::swift().command, "swift");
1139
1140        // Kotlin
1141        assert_eq!(QuickSession::kotlin().command, "kotlin");
1142
1143        // Groovy
1144        assert_eq!(QuickSession::groovysh().command, "groovysh");
1145
1146        // TypeScript
1147        assert_eq!(QuickSession::ts_node().command, "ts-node");
1148
1149        // Deno
1150        assert_eq!(QuickSession::deno().command, "deno");
1151
1152        // Bun
1153        let config = QuickSession::bun();
1154        assert_eq!(config.command, "bun");
1155        assert!(config.args.contains(&"repl".to_string()));
1156    }
1157}