Skip to main content

bashkit/
lib.rs

1//! Bashkit - Virtual bash interpreter for multi-tenant environments
2//!
3//! Virtual bash interpreter for AI agents, CI/CD pipelines, and code sandboxes.
4//! Written in Rust.
5//!
6//! # Features
7//!
8//! - **POSIX compliant** - Substantial IEEE 1003.1-2024 Shell Command Language compliance
9//! - **Sandboxed, in-process execution** - No real filesystem access by default
10//! - **Virtual filesystem** - [`InMemoryFs`], [`OverlayFs`], [`MountableFs`]
11//! - **Resource limits** - Command count, loop iterations, function depth
12//! - **Network allowlist** - Control HTTP access per-domain
13//! - **Custom builtins** - Extend with domain-specific commands
14//! - **Async-first** - Built on tokio
15//! - **Experimental: Git** - Virtual git operations on the VFS (`git` feature)
16//! - **Experimental: Python** - Embedded Python via [Monty](https://github.com/pydantic/monty) (`python` feature)
17//!
18//! # Built-in Commands (100+)
19//!
20//! | Category | Commands |
21//! |----------|----------|
22//! | Core | `echo`, `printf`, `cat`, `nl`, `read` |
23//! | Navigation | `cd`, `pwd`, `ls`, `find`, `pushd`, `popd`, `dirs` |
24//! | Flow control | `true`, `false`, `exit`, `return`, `break`, `continue`, `test`, `[` |
25//! | Variables | `export`, `set`, `unset`, `local`, `shift`, `source`, `.`, `eval`, `readonly`, `times`, `declare`, `typeset`, `let` |
26//! | Shell | `bash`, `sh` (virtual re-invocation), `:`, `trap`, `caller`, `getopts`, `shopt` |
27//! | Text processing | `grep`, `sed`, `awk`, `jq`, `head`, `tail`, `sort`, `uniq`, `cut`, `tr`, `wc`, `paste`, `column`, `diff`, `comm`, `strings`, `tac`, `rev`, `seq`, `expr` |
28//! | File operations | `mkdir`, `mktemp`, `rm`, `cp`, `mv`, `touch`, `chmod`, `chown`, `ln`, `rmdir`, `realpath` |
29//! | File inspection | `file`, `stat`, `less` |
30//! | Archives | `tar`, `gzip`, `gunzip` |
31//! | Byte tools | `od`, `xxd`, `hexdump` |
32//! | Utilities | `sleep`, `date`, `basename`, `dirname`, `timeout`, `wait`, `watch`, `yes`, `kill` |
33//! | Disk | `df`, `du` |
34//! | Pipeline | `xargs`, `tee` |
35//! | System info | `whoami`, `hostname`, `uname`, `id`, `env`, `printenv`, `history` |
36//! | Network | `curl`, `wget` (requires [`NetworkAllowlist`])
37//! | Experimental | `python`, `python3` (requires `python` feature), `git` (requires `git` feature)
38//!
39//! # Shell Features
40//!
41//! - Variables and parameter expansion (`$VAR`, `${VAR:-default}`, `${#VAR}`)
42//! - Command substitution (`$(cmd)`)
43//! - Arithmetic expansion (`$((1 + 2))`)
44//! - Pipelines and redirections (`|`, `>`, `>>`, `<`, `<<<`, `2>&1`)
45//! - Control flow (`if`/`elif`/`else`, `for`, `while`, `case`)
46//! - Functions (POSIX and bash-style)
47//! - Arrays (`arr=(a b c)`, `${arr[@]}`, `${#arr[@]}`)
48//! - Glob expansion (`*`, `?`)
49//! - Here documents (`<<EOF`)
50//!
51//! - [`compatibility_scorecard`] - Full compatibility status
52//!
53//! # Quick Start
54//!
55//! ```rust
56//! use bashkit::Bash;
57//!
58//! # #[tokio::main]
59//! # async fn main() -> bashkit::Result<()> {
60//! let mut bash = Bash::new();
61//! let result = bash.exec("echo 'Hello, World!'").await?;
62//! assert_eq!(result.stdout, "Hello, World!\n");
63//! assert_eq!(result.exit_code, 0);
64//! # Ok(())
65//! # }
66//! ```
67//!
68//! # Basic Usage
69//!
70//! ## Simple Commands
71//!
72//! ```rust
73//! use bashkit::Bash;
74//!
75//! # #[tokio::main]
76//! # async fn main() -> bashkit::Result<()> {
77//! let mut bash = Bash::new();
78//!
79//! // Echo with variables
80//! let result = bash.exec("NAME=World; echo \"Hello, $NAME!\"").await?;
81//! assert_eq!(result.stdout, "Hello, World!\n");
82//!
83//! // Pipelines
84//! let result = bash.exec("echo -e 'apple\\nbanana\\ncherry' | grep a").await?;
85//! assert_eq!(result.stdout, "apple\nbanana\n");
86//!
87//! // Arithmetic
88//! let result = bash.exec("echo $((2 + 2 * 3))").await?;
89//! assert_eq!(result.stdout, "8\n");
90//! # Ok(())
91//! # }
92//! ```
93//!
94//! ## Control Flow
95//!
96//! ```rust
97//! use bashkit::Bash;
98//!
99//! # #[tokio::main]
100//! # async fn main() -> bashkit::Result<()> {
101//! let mut bash = Bash::new();
102//!
103//! // For loops
104//! let result = bash.exec("for i in 1 2 3; do echo $i; done").await?;
105//! assert_eq!(result.stdout, "1\n2\n3\n");
106//!
107//! // If statements
108//! let result = bash.exec("if [ 5 -gt 3 ]; then echo bigger; fi").await?;
109//! assert_eq!(result.stdout, "bigger\n");
110//!
111//! // Functions
112//! let result = bash.exec("greet() { echo \"Hello, $1!\"; }; greet World").await?;
113//! assert_eq!(result.stdout, "Hello, World!\n");
114//! # Ok(())
115//! # }
116//! ```
117//!
118//! ## File Operations
119//!
120//! All file operations happen in the virtual filesystem:
121//!
122//! ```rust
123//! use bashkit::Bash;
124//!
125//! # #[tokio::main]
126//! # async fn main() -> bashkit::Result<()> {
127//! let mut bash = Bash::new();
128//!
129//! // Create and read files
130//! bash.exec("echo 'Hello' > /tmp/test.txt").await?;
131//! bash.exec("echo 'World' >> /tmp/test.txt").await?;
132//!
133//! let result = bash.exec("cat /tmp/test.txt").await?;
134//! assert_eq!(result.stdout, "Hello\nWorld\n");
135//!
136//! // Directory operations
137//! bash.exec("mkdir -p /data/nested/dir").await?;
138//! bash.exec("echo 'content' > /data/nested/dir/file.txt").await?;
139//! # Ok(())
140//! # }
141//! ```
142//!
143//! # Configuration with Builder
144//!
145//! Use [`Bash::builder()`] for advanced configuration:
146//!
147//! ```rust
148//! use bashkit::{Bash, ExecutionLimits};
149//!
150//! # #[tokio::main]
151//! # async fn main() -> bashkit::Result<()> {
152//! let mut bash = Bash::builder()
153//!     .env("API_KEY", "secret123")
154//!     .username("deploy")
155//!     .hostname("prod-server")
156//!     .limits(ExecutionLimits::new().max_commands(100))
157//!     .build();
158//!
159//! let result = bash.exec("whoami && hostname").await?;
160//! assert_eq!(result.stdout, "deploy\nprod-server\n");
161//! # Ok(())
162//! # }
163//! ```
164//!
165//! # Custom Builtins
166//!
167//! Register custom commands to extend Bashkit with domain-specific functionality:
168//!
169//! ```rust
170//! use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, async_trait};
171//!
172//! struct Greet;
173//!
174//! #[async_trait]
175//! impl Builtin for Greet {
176//!     async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
177//!         let name = ctx.args.first().map(|s| s.as_str()).unwrap_or("World");
178//!         Ok(ExecResult::ok(format!("Hello, {}!\n", name)))
179//!     }
180//! }
181//!
182//! # #[tokio::main]
183//! # async fn main() -> bashkit::Result<()> {
184//! let mut bash = Bash::builder()
185//!     .builtin("greet", Box::new(Greet))
186//!     .build();
187//!
188//! let result = bash.exec("greet Alice").await?;
189//! assert_eq!(result.stdout, "Hello, Alice!\n");
190//! # Ok(())
191//! # }
192//! ```
193//!
194//! Custom builtins have access to:
195//! - Command arguments (`ctx.args`)
196//! - Environment variables (`ctx.env`)
197//! - Shell variables (`ctx.variables`)
198//! - Virtual filesystem (`ctx.fs`)
199//! - Pipeline stdin (`ctx.stdin`)
200//!
201//! See [`BashBuilder::builtin`] for more details.
202//!
203//! # Virtual Filesystem
204//!
205//! Bashkit provides three filesystem implementations:
206//!
207//! - [`InMemoryFs`]: Simple in-memory filesystem (default)
208//! - [`OverlayFs`]: Copy-on-write overlay for layered storage
209//! - [`MountableFs`]: Mount multiple filesystems at different paths
210//!
211//! See the `fs` module documentation for details and examples.
212//!
213//! # Direct Filesystem Access
214//!
215//! Access the filesystem directly via [`Bash::fs()`]:
216//!
217//! ```rust
218//! use bashkit::{Bash, FileSystem};
219//! use std::path::Path;
220//!
221//! # #[tokio::main]
222//! # async fn main() -> bashkit::Result<()> {
223//! let mut bash = Bash::new();
224//! let fs = bash.fs();
225//!
226//! // Pre-populate files before running scripts
227//! fs.mkdir(Path::new("/config"), false).await?;
228//! fs.write_file(Path::new("/config/app.conf"), b"debug=true").await?;
229//!
230//! // Run a script that reads the config
231//! let result = bash.exec("cat /config/app.conf").await?;
232//! assert_eq!(result.stdout, "debug=true");
233//!
234//! // Read script output directly
235//! bash.exec("echo 'result' > /output.txt").await?;
236//! let output = fs.read_file(Path::new("/output.txt")).await?;
237//! assert_eq!(output, b"result\n");
238//! # Ok(())
239//! # }
240//! ```
241//!
242//! # HTTP Access (curl/wget)
243//!
244//! Enable the `http_client` feature and configure an allowlist for network access:
245//!
246//! ```rust,ignore
247//! use bashkit::{Bash, NetworkAllowlist};
248//!
249//! let mut bash = Bash::builder()
250//!     .network(NetworkAllowlist::new()
251//!         .allow("https://httpbin.org"))
252//!     .build();
253//!
254//! // curl and wget now work for allowed URLs
255//! let result = bash.exec("curl -s https://httpbin.org/get").await?;
256//! assert!(result.stdout.contains("httpbin.org"));
257//! ```
258//!
259//! Security features:
260//! - URL allowlist enforcement (no access without explicit configuration)
261//! - 10MB response size limit (prevents memory exhaustion)
262//! - 30 second timeout (prevents hanging)
263//! - No automatic redirects (prevents allowlist bypass)
264//! - Zip bomb protection for compressed responses
265//!
266//! See [`NetworkAllowlist`] for allowlist configuration options.
267//!
268//! # Experimental: Git Support
269//!
270//! Enable the `git` feature for virtual git operations. All git data lives in
271//! the virtual filesystem.
272//!
273//! ```toml
274//! [dependencies]
275//! bashkit = { version = "0.1", features = ["git"] }
276//! ```
277//!
278//! ```rust,ignore
279//! use bashkit::{Bash, GitConfig};
280//!
281//! let mut bash = Bash::builder()
282//!     .git(GitConfig::new()
283//!         .author("Deploy Bot", "deploy@example.com"))
284//!     .build();
285//!
286//! bash.exec("git init").await?;
287//! bash.exec("echo 'hello' > file.txt").await?;
288//! bash.exec("git add file.txt").await?;
289//! bash.exec("git commit -m 'initial'").await?;
290//! bash.exec("git log").await?;
291//! ```
292//!
293//! Supported: `init`, `config`, `add`, `commit`, `status`, `log`, `branch`,
294//! `checkout`, `diff`, `reset`, `remote`, `clone`/`push`/`pull`/`fetch` (virtual mode).
295//!
296//! See [`GitConfig`] for configuration options.
297//!
298//! # Experimental: Python Support
299//!
300//! Enable the `python` feature to embed the [Monty](https://github.com/pydantic/monty)
301//! Python interpreter (pure Rust, Python 3.12). Python `pathlib.Path` operations are
302//! bridged to the virtual filesystem.
303//!
304//! ```toml
305//! [dependencies]
306//! bashkit = { version = "0.1", features = ["python"] }
307//! ```
308//!
309//! ```rust,ignore
310//! use bashkit::Bash;
311//!
312//! let mut bash = Bash::builder().python().build();
313//!
314//! // Inline code
315//! bash.exec("python3 -c \"print(2 ** 10)\"").await?;
316//!
317//! // VFS bridging — files shared between bash and Python
318//! bash.exec("echo 'data' > /tmp/shared.txt").await?;
319//! bash.exec(r#"python3 -c "
320//! from pathlib import Path
321//! print(Path('/tmp/shared.txt').read_text().strip())
322//! ""#).await?;
323//! ```
324//!
325//! Limitations: no `open()` (use `pathlib.Path`), no network, no classes,
326//! no third-party imports.
327//!
328//! See `PythonLimits` for resource limit configuration.
329//!
330//! See the `python_guide` module docs (requires `python` feature).
331//!
332//! # Examples
333//!
334//! See the `examples/` directory for complete working examples:
335//!
336//! - `basic.rs` - Getting started with Bashkit
337//! - `custom_fs.rs` - Using different filesystem implementations
338//! - `custom_filesystem_impl.rs` - Implementing the [`FileSystem`] trait
339//! - `resource_limits.rs` - Setting execution limits
340//! - `virtual_identity.rs` - Customizing username/hostname
341//! - `text_processing.rs` - Using grep, sed, awk, and jq
342//! - `agent_tool.rs` - LLM agent integration
343//! - `git_workflow.rs` - Git operations on the virtual filesystem
344//! - `python_scripts.rs` - Embedded Python with VFS bridging
345//! - `python_external_functions.rs` - Python callbacks into host functions
346//!
347//! # Guides
348//!
349//! - [`custom_builtins_guide`] - Creating custom builtins
350//! - `python_guide` - Embedded Python (Monty) guide (requires `python` feature)
351//! - [`compatibility_scorecard`] - Feature parity tracking
352//! - `logging_guide` - Structured logging with security (requires `logging` feature)
353//!
354//! # Resources
355//!
356//! - [`threat_model`] - Security threats and mitigations
357//!
358//! # Ecosystem
359//!
360//! Bashkit is part of the [Everruns](https://everruns.com) ecosystem.
361
362// Stricter panic prevention - prefer proper error handling over unwrap()
363#![warn(clippy::unwrap_used)]
364
365mod builtins;
366mod error;
367mod fs;
368mod git;
369mod interpreter;
370mod limits;
371#[cfg(feature = "logging")]
372mod logging_impl;
373mod network;
374/// Parser module - exposed for fuzzing and testing
375pub mod parser;
376/// Scripted tool: compose ToolDef+callback pairs into a single Tool via bash scripts.
377/// Requires the `scripted_tool` feature.
378#[cfg(feature = "scripted_tool")]
379pub mod scripted_tool;
380/// Tool contract for LLM integration
381pub mod tool;
382
383pub use async_trait::async_trait;
384pub use builtins::{Builtin, Context as BuiltinContext};
385pub use error::{Error, Result};
386pub use fs::{
387    verify_filesystem_requirements, DirEntry, FileSystem, FileType, FsBackend, FsLimitExceeded,
388    FsLimits, FsUsage, InMemoryFs, Metadata, MountableFs, OverlayFs, PosixFs, VfsSnapshot,
389};
390pub use git::GitConfig;
391pub use interpreter::{ControlFlow, ExecResult, OutputCallback, ShellState};
392pub use limits::{ExecutionCounters, ExecutionLimits, LimitExceeded};
393pub use network::NetworkAllowlist;
394pub use tool::{BashTool, BashToolBuilder, Tool, ToolRequest, ToolResponse, ToolStatus, VERSION};
395
396#[cfg(feature = "scripted_tool")]
397pub use scripted_tool::{ScriptedTool, ScriptedToolBuilder, ToolArgs, ToolCallback, ToolDef};
398
399#[cfg(feature = "http_client")]
400pub use network::HttpClient;
401
402#[cfg(feature = "git")]
403pub use git::GitClient;
404
405#[cfg(feature = "python")]
406pub use builtins::{PythonExternalFnHandler, PythonExternalFns, PythonLimits};
407// Re-export monty types needed by external handler consumers.
408// **Unstable:** These types come from monty (git-pinned, not on crates.io).
409// They may change in breaking ways between bashkit releases.
410#[cfg(feature = "python")]
411pub use monty::{ExcType, ExternalResult, MontyException, MontyObject};
412
413/// Logging utilities module
414///
415/// Provides structured logging with security features including sensitive data redaction.
416/// Only available when the `logging` feature is enabled.
417#[cfg(feature = "logging")]
418pub mod logging {
419    pub use crate::logging_impl::{format_script_for_log, sanitize_for_log, LogConfig};
420}
421
422#[cfg(feature = "logging")]
423pub use logging::LogConfig;
424
425use interpreter::Interpreter;
426use parser::Parser;
427use std::collections::HashMap;
428use std::path::PathBuf;
429use std::sync::Arc;
430
431/// Main entry point for Bashkit.
432///
433/// Provides a virtual bash interpreter with an in-memory virtual filesystem.
434pub struct Bash {
435    fs: Arc<dyn FileSystem>,
436    interpreter: Interpreter,
437    /// Parser timeout (stored separately for use before interpreter runs)
438    parser_timeout: std::time::Duration,
439    /// Maximum input script size in bytes
440    max_input_bytes: usize,
441    /// Maximum AST nesting depth for parsing
442    max_ast_depth: usize,
443    /// Maximum parser operations (fuel)
444    max_parser_operations: usize,
445    /// Logging configuration
446    #[cfg(feature = "logging")]
447    log_config: logging::LogConfig,
448}
449
450impl Default for Bash {
451    fn default() -> Self {
452        Self::new()
453    }
454}
455
456impl Bash {
457    /// Create a new Bash instance with default settings.
458    pub fn new() -> Self {
459        let fs: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
460        let interpreter = Interpreter::new(Arc::clone(&fs));
461        let parser_timeout = ExecutionLimits::default().parser_timeout;
462        let max_input_bytes = ExecutionLimits::default().max_input_bytes;
463        let max_ast_depth = ExecutionLimits::default().max_ast_depth;
464        let max_parser_operations = ExecutionLimits::default().max_parser_operations;
465        Self {
466            fs,
467            interpreter,
468            parser_timeout,
469            max_input_bytes,
470            max_ast_depth,
471            max_parser_operations,
472            #[cfg(feature = "logging")]
473            log_config: logging::LogConfig::default(),
474        }
475    }
476
477    /// Create a new BashBuilder for customized configuration.
478    pub fn builder() -> BashBuilder {
479        BashBuilder::default()
480    }
481
482    /// Execute a bash script and return the result.
483    ///
484    /// This method first validates that the script does not exceed the maximum
485    /// input size, then parses the script with a timeout, AST depth limit, and fuel limit,
486    /// then executes the resulting AST.
487    pub async fn exec(&mut self, script: &str) -> Result<ExecResult> {
488        // THREAT[TM-LOG-001]: Sensitive data in logs
489        // Mitigation: Use LogConfig to redact sensitive script content
490        #[cfg(feature = "logging")]
491        {
492            let script_info = logging::format_script_for_log(script, &self.log_config);
493            tracing::info!(target: "bashkit::session", script = %script_info, "Starting script execution");
494        }
495
496        // Check input size before parsing (V1 mitigation)
497        let input_len = script.len();
498        if input_len > self.max_input_bytes {
499            #[cfg(feature = "logging")]
500            tracing::error!(
501                target: "bashkit::session",
502                input_len = input_len,
503                max_bytes = self.max_input_bytes,
504                "Script exceeds maximum input size"
505            );
506            return Err(Error::ResourceLimit(LimitExceeded::InputTooLarge(
507                input_len,
508                self.max_input_bytes,
509            )));
510        }
511
512        let parser_timeout = self.parser_timeout;
513        let max_ast_depth = self.max_ast_depth;
514        let max_parser_operations = self.max_parser_operations;
515        let script_owned = script.to_owned();
516
517        #[cfg(feature = "logging")]
518        tracing::debug!(
519            target: "bashkit::parser",
520            input_len = input_len,
521            max_ast_depth = max_ast_depth,
522            max_operations = max_parser_operations,
523            "Parsing script"
524        );
525
526        // Parse with timeout using spawn_blocking since parsing is sync
527        let parse_result = tokio::time::timeout(parser_timeout, async {
528            tokio::task::spawn_blocking(move || {
529                let parser =
530                    Parser::with_limits(&script_owned, max_ast_depth, max_parser_operations);
531                parser.parse()
532            })
533            .await
534        })
535        .await;
536
537        let ast = match parse_result {
538            Ok(Ok(result)) => {
539                match &result {
540                    Ok(_) => {
541                        #[cfg(feature = "logging")]
542                        tracing::debug!(target: "bashkit::parser", "Parse completed successfully");
543                    }
544                    Err(_e) => {
545                        #[cfg(feature = "logging")]
546                        tracing::warn!(target: "bashkit::parser", error = %_e, "Parse error");
547                    }
548                }
549                result?
550            }
551            Ok(Err(join_error)) => {
552                #[cfg(feature = "logging")]
553                tracing::error!(
554                    target: "bashkit::parser",
555                    error = %join_error,
556                    "Parser task failed"
557                );
558                return Err(Error::Parse(format!("parser task failed: {}", join_error)));
559            }
560            Err(_elapsed) => {
561                #[cfg(feature = "logging")]
562                tracing::error!(
563                    target: "bashkit::parser",
564                    timeout_ms = parser_timeout.as_millis() as u64,
565                    "Parser timeout exceeded"
566                );
567                return Err(Error::ResourceLimit(LimitExceeded::ParserTimeout(
568                    parser_timeout,
569                )));
570            }
571        };
572
573        #[cfg(feature = "logging")]
574        tracing::debug!(target: "bashkit::interpreter", "Starting interpretation");
575
576        let result = self.interpreter.execute(&ast).await;
577
578        #[cfg(feature = "logging")]
579        match &result {
580            Ok(exec_result) => {
581                tracing::info!(
582                    target: "bashkit::session",
583                    exit_code = exec_result.exit_code,
584                    stdout_len = exec_result.stdout.len(),
585                    stderr_len = exec_result.stderr.len(),
586                    "Script execution completed"
587                );
588            }
589            Err(e) => {
590                tracing::error!(
591                    target: "bashkit::session",
592                    error = %e,
593                    "Script execution failed"
594                );
595            }
596        }
597
598        result
599    }
600
601    /// Execute a bash script with streaming output.
602    ///
603    /// Like [`exec`](Self::exec), but calls `output_callback` with incremental
604    /// `(stdout_chunk, stderr_chunk)` pairs as output is produced. Callbacks fire
605    /// after each loop iteration, command list element, and top-level command.
606    ///
607    /// The full result is still returned in [`ExecResult`] for callers that need it.
608    ///
609    /// # Example
610    ///
611    /// ```rust
612    /// use bashkit::Bash;
613    /// use std::sync::{Arc, Mutex};
614    ///
615    /// # #[tokio::main]
616    /// # async fn main() -> bashkit::Result<()> {
617    /// let chunks: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
618    /// let chunks_cb = chunks.clone();
619    /// let mut bash = Bash::new();
620    /// let result = bash.exec_streaming(
621    ///     "for i in 1 2 3; do echo $i; done",
622    ///     Box::new(move |stdout, _stderr| {
623    ///         chunks_cb.lock().unwrap().push(stdout.to_string());
624    ///     }),
625    /// ).await?;
626    /// assert_eq!(result.stdout, "1\n2\n3\n");
627    /// assert_eq!(*chunks.lock().unwrap(), vec!["1\n", "2\n", "3\n"]);
628    /// # Ok(())
629    /// # }
630    /// ```
631    pub async fn exec_streaming(
632        &mut self,
633        script: &str,
634        output_callback: OutputCallback,
635    ) -> Result<ExecResult> {
636        self.interpreter.set_output_callback(output_callback);
637        let result = self.exec(script).await;
638        self.interpreter.clear_output_callback();
639        result
640    }
641
642    /// Get a clone of the underlying filesystem.
643    ///
644    /// Provides direct access to the virtual filesystem for:
645    /// - Pre-populating files before script execution
646    /// - Reading binary file outputs after execution
647    /// - Injecting test data or configuration
648    ///
649    /// # Example
650    /// ```rust,no_run
651    /// use bashkit::Bash;
652    /// use std::path::Path;
653    ///
654    /// #[tokio::main]
655    /// async fn main() -> anyhow::Result<()> {
656    ///     let mut bash = Bash::new();
657    ///     let fs = bash.fs();
658    ///
659    ///     // Pre-populate config file
660    ///     fs.mkdir(Path::new("/config"), false).await?;
661    ///     fs.write_file(Path::new("/config/app.txt"), b"debug=true\n").await?;
662    ///
663    ///     // Bash script can read pre-populated files
664    ///     let result = bash.exec("cat /config/app.txt").await?;
665    ///     assert_eq!(result.stdout, "debug=true\n");
666    ///
667    ///     // Bash creates output, read it directly
668    ///     bash.exec("echo 'done' > /output.txt").await?;
669    ///     let output = fs.read_file(Path::new("/output.txt")).await?;
670    ///     assert_eq!(output, b"done\n");
671    ///     Ok(())
672    /// }
673    /// ```
674    pub fn fs(&self) -> Arc<dyn FileSystem> {
675        Arc::clone(&self.fs)
676    }
677
678    /// Capture the current shell state (variables, env, cwd, options).
679    ///
680    /// Returns a serializable snapshot of the interpreter state. Combine with
681    /// [`InMemoryFs::snapshot()`] for full session persistence.
682    ///
683    /// # Example
684    ///
685    /// ```rust
686    /// use bashkit::Bash;
687    ///
688    /// # #[tokio::main]
689    /// # async fn main() -> bashkit::Result<()> {
690    /// let mut bash = Bash::new();
691    /// bash.exec("x=42").await?;
692    ///
693    /// let state = bash.shell_state();
694    ///
695    /// bash.exec("x=99").await?;
696    /// bash.restore_shell_state(&state);
697    ///
698    /// let result = bash.exec("echo $x").await?;
699    /// assert_eq!(result.stdout, "42\n");
700    /// # Ok(())
701    /// # }
702    /// ```
703    pub fn shell_state(&self) -> ShellState {
704        self.interpreter.shell_state()
705    }
706
707    /// Restore shell state from a previous snapshot.
708    ///
709    /// Restores variables, env, cwd, arrays, aliases, traps, and options.
710    /// Does not restore functions or builtins — those remain as-is.
711    pub fn restore_shell_state(&mut self, state: &ShellState) {
712        self.interpreter.restore_shell_state(state);
713    }
714}
715
716/// Builder for customized Bash configuration.
717///
718/// # Example
719///
720/// ```rust
721/// use bashkit::{Bash, ExecutionLimits};
722///
723/// let bash = Bash::builder()
724///     .env("HOME", "/home/user")
725///     .username("deploy")
726///     .hostname("prod-server")
727///     .limits(ExecutionLimits::new().max_commands(1000))
728///     .build();
729/// ```
730///
731/// ## Custom Builtins
732///
733/// You can register custom builtins to extend bashkit with domain-specific commands:
734///
735/// ```rust
736/// use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, async_trait};
737///
738/// struct MyCommand;
739///
740/// #[async_trait]
741/// impl Builtin for MyCommand {
742///     async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
743///         Ok(ExecResult::ok(format!("Hello from custom command!\n")))
744///     }
745/// }
746///
747/// let bash = Bash::builder()
748///     .builtin("mycommand", Box::new(MyCommand))
749///     .build();
750/// ```
751/// A file to be mounted during builder construction.
752struct MountedFile {
753    path: PathBuf,
754    content: String,
755    mode: u32,
756}
757
758#[derive(Default)]
759pub struct BashBuilder {
760    fs: Option<Arc<dyn FileSystem>>,
761    env: HashMap<String, String>,
762    cwd: Option<PathBuf>,
763    limits: ExecutionLimits,
764    username: Option<String>,
765    hostname: Option<String>,
766    /// Fixed epoch for virtualizing the `date` builtin (TM-INF-018)
767    fixed_epoch: Option<i64>,
768    custom_builtins: HashMap<String, Box<dyn Builtin>>,
769    /// Files to mount in the virtual filesystem
770    mounted_files: Vec<MountedFile>,
771    /// Network allowlist for curl/wget builtins
772    #[cfg(feature = "http_client")]
773    network_allowlist: Option<NetworkAllowlist>,
774    /// Logging configuration
775    #[cfg(feature = "logging")]
776    log_config: Option<logging::LogConfig>,
777    /// Git configuration for git builtins
778    #[cfg(feature = "git")]
779    git_config: Option<GitConfig>,
780}
781
782impl BashBuilder {
783    /// Set a custom filesystem.
784    pub fn fs(mut self, fs: Arc<dyn FileSystem>) -> Self {
785        self.fs = Some(fs);
786        self
787    }
788
789    /// Set an environment variable.
790    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
791        self.env.insert(key.into(), value.into());
792        self
793    }
794
795    /// Set the current working directory.
796    pub fn cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
797        self.cwd = Some(cwd.into());
798        self
799    }
800
801    /// Set execution limits.
802    pub fn limits(mut self, limits: ExecutionLimits) -> Self {
803        self.limits = limits;
804        self
805    }
806
807    /// Set the sandbox username.
808    ///
809    /// This configures `whoami` and `id` builtins to return this username,
810    /// and automatically sets the `USER` environment variable.
811    pub fn username(mut self, username: impl Into<String>) -> Self {
812        self.username = Some(username.into());
813        self
814    }
815
816    /// Set the sandbox hostname.
817    ///
818    /// This configures `hostname` and `uname -n` builtins to return this hostname.
819    pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
820        self.hostname = Some(hostname.into());
821        self
822    }
823
824    /// Set a fixed Unix epoch for the `date` builtin.
825    ///
826    /// THREAT[TM-INF-018]: Prevents `date` from leaking real host time.
827    /// When set, `date` returns this fixed time instead of the real clock.
828    pub fn fixed_epoch(mut self, epoch: i64) -> Self {
829        self.fixed_epoch = Some(epoch);
830        self
831    }
832
833    /// Configure network access for curl/wget builtins.
834    ///
835    /// Network access is disabled by default. Use this method to enable HTTP
836    /// requests from scripts with a URL allowlist for security.
837    ///
838    /// # Security
839    ///
840    /// The allowlist uses a default-deny model:
841    /// - Only URLs matching allowlist patterns can be accessed
842    /// - Pattern matching is literal (no DNS resolution) to prevent DNS rebinding
843    /// - Scheme, host, port, and path prefix are all validated
844    ///
845    /// # Example
846    ///
847    /// ```rust
848    /// use bashkit::{Bash, NetworkAllowlist};
849    ///
850    /// // Allow access to specific APIs only
851    /// let allowlist = NetworkAllowlist::new()
852    ///     .allow("https://api.example.com")
853    ///     .allow("https://cdn.example.com/assets");
854    ///
855    /// let bash = Bash::builder()
856    ///     .network(allowlist)
857    ///     .build();
858    /// ```
859    ///
860    /// # Warning
861    ///
862    /// Using [`NetworkAllowlist::allow_all()`] is dangerous and should only be
863    /// used for testing or when the script is fully trusted.
864    #[cfg(feature = "http_client")]
865    pub fn network(mut self, allowlist: NetworkAllowlist) -> Self {
866        self.network_allowlist = Some(allowlist);
867        self
868    }
869
870    /// Configure logging behavior.
871    ///
872    /// When the `logging` feature is enabled, Bashkit can emit structured logs
873    /// at various levels (error, warn, info, debug, trace) during execution.
874    ///
875    /// # Log Levels
876    ///
877    /// - **ERROR**: Unrecoverable failures, exceptions, security violations
878    /// - **WARN**: Recoverable issues, limit warnings, deprecated usage
879    /// - **INFO**: Session lifecycle (start/end), high-level execution flow
880    /// - **DEBUG**: Command execution, variable expansion, control flow
881    /// - **TRACE**: Internal parser/interpreter state, detailed data flow
882    ///
883    /// # Security (TM-LOG-001)
884    ///
885    /// By default, sensitive data is redacted from logs:
886    /// - Environment variables matching secret patterns (PASSWORD, TOKEN, etc.)
887    /// - URL credentials (user:pass@host)
888    /// - Values that look like API keys or JWTs
889    ///
890    /// # Example
891    ///
892    /// ```rust
893    /// use bashkit::{Bash, LogConfig};
894    ///
895    /// let bash = Bash::builder()
896    ///     .log_config(LogConfig::new()
897    ///         .redact_env("MY_CUSTOM_SECRET"))
898    ///     .build();
899    /// ```
900    ///
901    /// # Warning
902    ///
903    /// Do not use `LogConfig::unsafe_disable_redaction()` or
904    /// `LogConfig::unsafe_log_scripts()` in production, as they may expose
905    /// sensitive data in logs.
906    #[cfg(feature = "logging")]
907    pub fn log_config(mut self, config: logging::LogConfig) -> Self {
908        self.log_config = Some(config);
909        self
910    }
911
912    /// Configure git support for git commands.
913    ///
914    /// Git access is disabled by default. Use this method to enable git
915    /// commands with the specified configuration.
916    ///
917    /// # Security
918    ///
919    /// - All operations are confined to the virtual filesystem
920    /// - Author identity is sandboxed (configurable, never from host)
921    /// - Remote operations (Phase 2) require URL allowlist
922    /// - No access to host git config or credentials
923    ///
924    /// # Example
925    ///
926    /// ```rust
927    /// use bashkit::{Bash, GitConfig};
928    ///
929    /// let bash = Bash::builder()
930    ///     .git(GitConfig::new()
931    ///         .author("CI Bot", "ci@example.com"))
932    ///     .build();
933    /// ```
934    ///
935    /// # Threat Mitigations
936    ///
937    /// - TM-GIT-002: Host identity leak - uses configured author, never host
938    /// - TM-GIT-003: Host config access - no filesystem access outside VFS
939    /// - TM-GIT-005: Repository escape - all paths within VFS
940    #[cfg(feature = "git")]
941    pub fn git(mut self, config: GitConfig) -> Self {
942        self.git_config = Some(config);
943        self
944    }
945
946    /// Enable embedded Python (`python`/`python3` builtins) via Monty interpreter
947    /// with default resource limits.
948    ///
949    /// Monty runs directly in the host process with resource limits enforced
950    /// by Monty's runtime (memory, allocations, time, recursion).
951    ///
952    /// Requires the `python` feature flag. Python `pathlib.Path` operations are
953    /// bridged to the virtual filesystem.
954    ///
955    /// # Example
956    ///
957    /// ```rust,ignore
958    /// let bash = Bash::builder().python().build();
959    /// ```
960    #[cfg(feature = "python")]
961    pub fn python(self) -> Self {
962        self.python_with_limits(builtins::PythonLimits::default())
963    }
964
965    /// Enable embedded Python with custom resource limits.
966    ///
967    /// See [`BashBuilder::python`] for details.
968    ///
969    /// # Example
970    ///
971    /// ```rust,ignore
972    /// use bashkit::PythonLimits;
973    /// use std::time::Duration;
974    ///
975    /// let bash = Bash::builder()
976    ///     .python_with_limits(PythonLimits::default().max_duration(Duration::from_secs(5)))
977    ///     .build();
978    /// ```
979    #[cfg(feature = "python")]
980    pub fn python_with_limits(self, limits: builtins::PythonLimits) -> Self {
981        self.builtin(
982            "python",
983            Box::new(builtins::Python::with_limits(limits.clone())),
984        )
985        .builtin("python3", Box::new(builtins::Python::with_limits(limits)))
986    }
987
988    /// Enable embedded Python with external function handlers.
989    ///
990    /// See [`PythonExternalFnHandler`] for handler details.
991    #[cfg(feature = "python")]
992    pub fn python_with_external_handler(
993        self,
994        limits: builtins::PythonLimits,
995        external_fns: Vec<String>,
996        handler: builtins::PythonExternalFnHandler,
997    ) -> Self {
998        self.builtin(
999            "python",
1000            Box::new(
1001                builtins::Python::with_limits(limits.clone())
1002                    .with_external_handler(external_fns.clone(), handler.clone()),
1003            ),
1004        )
1005        .builtin(
1006            "python3",
1007            Box::new(
1008                builtins::Python::with_limits(limits).with_external_handler(external_fns, handler),
1009            ),
1010        )
1011    }
1012
1013    /// Register a custom builtin command.
1014    ///
1015    /// Custom builtins extend bashkit with domain-specific commands that can be
1016    /// invoked from bash scripts. They have full access to the execution context
1017    /// including arguments, environment, shell variables, and the virtual filesystem.
1018    ///
1019    /// Custom builtins can override default builtins if registered with the same name.
1020    ///
1021    /// # Arguments
1022    ///
1023    /// * `name` - The command name (e.g., "psql", "kubectl")
1024    /// * `builtin` - A boxed implementation of the [`Builtin`] trait
1025    ///
1026    /// # Example
1027    ///
1028    /// ```rust
1029    /// use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, async_trait};
1030    ///
1031    /// struct Greet {
1032    ///     default_name: String,
1033    /// }
1034    ///
1035    /// #[async_trait]
1036    /// impl Builtin for Greet {
1037    ///     async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
1038    ///         let name = ctx.args.first()
1039    ///             .map(|s| s.as_str())
1040    ///             .unwrap_or(&self.default_name);
1041    ///         Ok(ExecResult::ok(format!("Hello, {}!\n", name)))
1042    ///     }
1043    /// }
1044    ///
1045    /// let bash = Bash::builder()
1046    ///     .builtin("greet", Box::new(Greet { default_name: "World".into() }))
1047    ///     .build();
1048    /// ```
1049    pub fn builtin(mut self, name: impl Into<String>, builtin: Box<dyn Builtin>) -> Self {
1050        self.custom_builtins.insert(name.into(), builtin);
1051        self
1052    }
1053
1054    /// Mount a text file in the virtual filesystem.
1055    ///
1056    /// This creates a regular file (mode `0o644`) with the specified content at
1057    /// the given path. Parent directories are created automatically.
1058    ///
1059    /// Mounted files are added via an [`OverlayFs`] layer on top of the base
1060    /// filesystem. This means:
1061    /// - The base filesystem remains unchanged
1062    /// - Mounted files take precedence over base filesystem files
1063    /// - Works with any filesystem implementation
1064    ///
1065    /// # Example
1066    ///
1067    /// ```rust
1068    /// use bashkit::Bash;
1069    ///
1070    /// # #[tokio::main]
1071    /// # async fn main() -> bashkit::Result<()> {
1072    /// let mut bash = Bash::builder()
1073    ///     .mount_text("/config/app.conf", "debug=true\nport=8080\n")
1074    ///     .mount_text("/data/users.json", r#"["alice", "bob"]"#)
1075    ///     .build();
1076    ///
1077    /// let result = bash.exec("cat /config/app.conf").await?;
1078    /// assert_eq!(result.stdout, "debug=true\nport=8080\n");
1079    /// # Ok(())
1080    /// # }
1081    /// ```
1082    pub fn mount_text(mut self, path: impl Into<PathBuf>, content: impl Into<String>) -> Self {
1083        self.mounted_files.push(MountedFile {
1084            path: path.into(),
1085            content: content.into(),
1086            mode: 0o644,
1087        });
1088        self
1089    }
1090
1091    /// Mount a readonly text file in the virtual filesystem.
1092    ///
1093    /// This creates a readonly file (mode `0o444`) with the specified content.
1094    /// Parent directories are created automatically.
1095    ///
1096    /// Readonly files are useful for:
1097    /// - Configuration that shouldn't be modified by scripts
1098    /// - Reference data that should remain immutable
1099    /// - Simulating system files like `/etc/passwd`
1100    ///
1101    /// Mounted files are added via an [`OverlayFs`] layer on top of the base
1102    /// filesystem. This means:
1103    /// - The base filesystem remains unchanged
1104    /// - Mounted files take precedence over base filesystem files
1105    /// - Works with any filesystem implementation
1106    ///
1107    /// # Example
1108    ///
1109    /// ```rust
1110    /// use bashkit::Bash;
1111    ///
1112    /// # #[tokio::main]
1113    /// # async fn main() -> bashkit::Result<()> {
1114    /// let mut bash = Bash::builder()
1115    ///     .mount_readonly_text("/etc/version", "1.2.3")
1116    ///     .mount_readonly_text("/etc/app.conf", "production=true\n")
1117    ///     .build();
1118    ///
1119    /// // Can read the file
1120    /// let result = bash.exec("cat /etc/version").await?;
1121    /// assert_eq!(result.stdout, "1.2.3");
1122    ///
1123    /// // File has readonly permissions
1124    /// let stat = bash.fs().stat(std::path::Path::new("/etc/version")).await?;
1125    /// assert_eq!(stat.mode, 0o444);
1126    /// # Ok(())
1127    /// # }
1128    /// ```
1129    pub fn mount_readonly_text(
1130        mut self,
1131        path: impl Into<PathBuf>,
1132        content: impl Into<String>,
1133    ) -> Self {
1134        self.mounted_files.push(MountedFile {
1135            path: path.into(),
1136            content: content.into(),
1137            mode: 0o444,
1138        });
1139        self
1140    }
1141
1142    /// Build the Bash instance.
1143    ///
1144    /// If mounted files are specified, they are added via an [`OverlayFs`] layer
1145    /// on top of the base filesystem. This means:
1146    /// - The base filesystem remains unchanged
1147    /// - Mounted files take precedence over base filesystem files
1148    /// - Works with any filesystem implementation
1149    ///
1150    /// # Example
1151    ///
1152    /// ```rust
1153    /// use bashkit::{Bash, InMemoryFs};
1154    /// use std::sync::Arc;
1155    ///
1156    /// # #[tokio::main]
1157    /// # async fn main() -> bashkit::Result<()> {
1158    /// // Works with default InMemoryFs
1159    /// let mut bash = Bash::builder()
1160    ///     .mount_text("/config/app.conf", "debug=true\n")
1161    ///     .build();
1162    ///
1163    /// // Also works with custom filesystems
1164    /// let custom_fs = Arc::new(InMemoryFs::new());
1165    /// let mut bash = Bash::builder()
1166    ///     .fs(custom_fs)
1167    ///     .mount_text("/config/app.conf", "debug=true\n")
1168    ///     .mount_readonly_text("/etc/version", "1.0.0")
1169    ///     .build();
1170    ///
1171    /// let result = bash.exec("cat /config/app.conf").await?;
1172    /// assert_eq!(result.stdout, "debug=true\n");
1173    /// # Ok(())
1174    /// # }
1175    /// ```
1176    pub fn build(self) -> Bash {
1177        let base_fs = self.fs.unwrap_or_else(|| Arc::new(InMemoryFs::new()));
1178
1179        // If there are mounted files, wrap in an OverlayFs
1180        let fs: Arc<dyn FileSystem> = if self.mounted_files.is_empty() {
1181            base_fs
1182        } else {
1183            let overlay = OverlayFs::new(base_fs);
1184            // Add mounted files to the overlay layer
1185            for mf in &self.mounted_files {
1186                overlay.upper().add_file(&mf.path, &mf.content, mf.mode);
1187            }
1188            Arc::new(overlay)
1189        };
1190
1191        Self::build_with_fs(
1192            fs,
1193            self.env,
1194            self.username,
1195            self.hostname,
1196            self.fixed_epoch,
1197            self.cwd,
1198            self.limits,
1199            self.custom_builtins,
1200            #[cfg(feature = "http_client")]
1201            self.network_allowlist,
1202            #[cfg(feature = "logging")]
1203            self.log_config,
1204            #[cfg(feature = "git")]
1205            self.git_config,
1206        )
1207    }
1208
1209    /// Internal helper to build Bash with a configured filesystem.
1210    #[allow(clippy::too_many_arguments)]
1211    fn build_with_fs(
1212        fs: Arc<dyn FileSystem>,
1213        env: HashMap<String, String>,
1214        username: Option<String>,
1215        hostname: Option<String>,
1216        fixed_epoch: Option<i64>,
1217        cwd: Option<PathBuf>,
1218        limits: ExecutionLimits,
1219        custom_builtins: HashMap<String, Box<dyn Builtin>>,
1220        #[cfg(feature = "http_client")] network_allowlist: Option<NetworkAllowlist>,
1221        #[cfg(feature = "logging")] log_config: Option<logging::LogConfig>,
1222        #[cfg(feature = "git")] git_config: Option<GitConfig>,
1223    ) -> Bash {
1224        #[cfg(feature = "logging")]
1225        let log_config = log_config.unwrap_or_default();
1226
1227        #[cfg(feature = "logging")]
1228        tracing::debug!(
1229            target: "bashkit::config",
1230            redact_sensitive = log_config.redact_sensitive,
1231            log_scripts = log_config.log_script_content,
1232            "Bash instance configured"
1233        );
1234
1235        let mut interpreter = Interpreter::with_config(
1236            Arc::clone(&fs),
1237            username.clone(),
1238            hostname,
1239            fixed_epoch,
1240            custom_builtins,
1241        );
1242
1243        // Set environment variables (also override shell variable defaults)
1244        for (key, value) in &env {
1245            interpreter.set_env(key, value);
1246            // Shell variables like HOME, USER should also be set as variables
1247            // so they take precedence over the defaults
1248            interpreter.set_var(key, value);
1249        }
1250        drop(env);
1251
1252        // If username is set, automatically set USER env var
1253        if let Some(ref username) = username {
1254            interpreter.set_env("USER", username);
1255            interpreter.set_var("USER", username);
1256        }
1257
1258        if let Some(cwd) = cwd {
1259            interpreter.set_cwd(cwd);
1260        }
1261
1262        // Configure HTTP client for network builtins
1263        #[cfg(feature = "http_client")]
1264        if let Some(allowlist) = network_allowlist {
1265            let client = network::HttpClient::new(allowlist);
1266            interpreter.set_http_client(client);
1267        }
1268
1269        // Configure git client for git builtins
1270        #[cfg(feature = "git")]
1271        if let Some(config) = git_config {
1272            let client = git::GitClient::new(config);
1273            interpreter.set_git_client(client);
1274        }
1275
1276        let parser_timeout = limits.parser_timeout;
1277        let max_input_bytes = limits.max_input_bytes;
1278        let max_ast_depth = limits.max_ast_depth;
1279        let max_parser_operations = limits.max_parser_operations;
1280        interpreter.set_limits(limits);
1281
1282        Bash {
1283            fs,
1284            interpreter,
1285            parser_timeout,
1286            max_input_bytes,
1287            max_ast_depth,
1288            max_parser_operations,
1289            #[cfg(feature = "logging")]
1290            log_config,
1291        }
1292    }
1293}
1294
1295// =============================================================================
1296// Documentation Modules
1297// =============================================================================
1298// These modules embed external markdown guides into rustdoc.
1299// Source files live in crates/bashkit/docs/ - edit there, not here.
1300// See specs/008-documentation.md for the documentation approach.
1301
1302/// Guide for creating custom builtins to extend Bashkit.
1303///
1304/// This guide covers:
1305/// - Implementing the [`Builtin`] trait
1306/// - Accessing execution context ([`BuiltinContext`])
1307/// - Working with arguments, environment, and filesystem
1308/// - Best practices and examples
1309///
1310/// **Related:** [`BashBuilder::builtin`], [`compatibility_scorecard`]
1311#[doc = include_str!("../docs/custom_builtins.md")]
1312pub mod custom_builtins_guide {}
1313
1314/// Bash compatibility scorecard.
1315///
1316/// Tracks feature parity with real bash:
1317/// - Implemented vs missing features
1318/// - Builtins, syntax, expansions
1319/// - POSIX compliance status
1320/// - Resource limits
1321///
1322/// **Related:** [`custom_builtins_guide`], [`threat_model`]
1323#[doc = include_str!("../docs/compatibility.md")]
1324pub mod compatibility_scorecard {}
1325
1326/// Security threat model guide.
1327///
1328/// This guide documents security threats addressed by Bashkit and their mitigations.
1329/// All threats use stable IDs for tracking and code references.
1330///
1331/// **Topics covered:**
1332/// - Denial of Service mitigations (TM-DOS-*)
1333/// - Sandbox escape prevention (TM-ESC-*)
1334/// - Information disclosure protection (TM-INF-*)
1335/// - Network security controls (TM-NET-*)
1336/// - Multi-tenant isolation (TM-ISO-*)
1337///
1338/// **Related:** [`ExecutionLimits`], [`FsLimits`], [`NetworkAllowlist`]
1339#[doc = include_str!("../docs/threat-model.md")]
1340pub mod threat_model {}
1341
1342/// Guide for embedded Python via the Monty interpreter.
1343///
1344/// **Experimental:** The Monty integration is experimental with known security
1345/// issues. See the guide below and [`threat_model`] for details.
1346///
1347/// This guide covers:
1348/// - Enabling Python with [`BashBuilder::python`]
1349/// - VFS bridging (`pathlib.Path` → virtual filesystem)
1350/// - Configuring resource limits with [`PythonLimits`]
1351/// - LLM tool integration via [`BashToolBuilder::python`]
1352/// - Known limitations (no `open()`, no HTTP, no classes)
1353///
1354/// **Related:** [`BashBuilder::python`], [`PythonLimits`], [`threat_model`]
1355#[cfg(feature = "python")]
1356#[doc = include_str!("../docs/python.md")]
1357pub mod python_guide {}
1358
1359/// Logging guide for Bashkit.
1360///
1361/// This guide covers configuring structured logging, log levels, security
1362/// considerations, and integration with tracing subscribers.
1363///
1364/// **Topics covered:**
1365/// - Enabling the `logging` feature
1366/// - Log levels and targets
1367/// - Security: sensitive data redaction (TM-LOG-*)
1368/// - Integration with tracing-subscriber
1369///
1370/// **Related:** [`LogConfig`], [`threat_model`]
1371#[cfg(feature = "logging")]
1372#[doc = include_str!("../docs/logging.md")]
1373pub mod logging_guide {}
1374
1375#[cfg(test)]
1376#[allow(clippy::unwrap_used)]
1377mod tests {
1378    use super::*;
1379    use std::sync::{Arc, Mutex};
1380
1381    #[tokio::test]
1382    async fn test_echo_hello() {
1383        let mut bash = Bash::new();
1384        let result = bash.exec("echo hello").await.unwrap();
1385        assert_eq!(result.stdout, "hello\n");
1386        assert_eq!(result.exit_code, 0);
1387    }
1388
1389    #[tokio::test]
1390    async fn test_echo_multiple_args() {
1391        let mut bash = Bash::new();
1392        let result = bash.exec("echo hello world").await.unwrap();
1393        assert_eq!(result.stdout, "hello world\n");
1394        assert_eq!(result.exit_code, 0);
1395    }
1396
1397    #[tokio::test]
1398    async fn test_variable_expansion() {
1399        let mut bash = Bash::builder().env("HOME", "/home/user").build();
1400        let result = bash.exec("echo $HOME").await.unwrap();
1401        assert_eq!(result.stdout, "/home/user\n");
1402        assert_eq!(result.exit_code, 0);
1403    }
1404
1405    #[tokio::test]
1406    async fn test_variable_brace_expansion() {
1407        let mut bash = Bash::builder().env("USER", "testuser").build();
1408        let result = bash.exec("echo ${USER}").await.unwrap();
1409        assert_eq!(result.stdout, "testuser\n");
1410    }
1411
1412    #[tokio::test]
1413    async fn test_undefined_variable_expands_to_empty() {
1414        let mut bash = Bash::new();
1415        let result = bash.exec("echo $UNDEFINED_VAR").await.unwrap();
1416        assert_eq!(result.stdout, "\n");
1417    }
1418
1419    #[tokio::test]
1420    async fn test_pipeline() {
1421        let mut bash = Bash::new();
1422        let result = bash.exec("echo hello | cat").await.unwrap();
1423        assert_eq!(result.stdout, "hello\n");
1424    }
1425
1426    #[tokio::test]
1427    async fn test_pipeline_three_commands() {
1428        let mut bash = Bash::new();
1429        let result = bash.exec("echo hello | cat | cat").await.unwrap();
1430        assert_eq!(result.stdout, "hello\n");
1431    }
1432
1433    #[tokio::test]
1434    async fn test_redirect_output() {
1435        let mut bash = Bash::new();
1436        let result = bash.exec("echo hello > /tmp/test.txt").await.unwrap();
1437        assert_eq!(result.stdout, "");
1438        assert_eq!(result.exit_code, 0);
1439
1440        // Read the file back
1441        let result = bash.exec("cat /tmp/test.txt").await.unwrap();
1442        assert_eq!(result.stdout, "hello\n");
1443    }
1444
1445    #[tokio::test]
1446    async fn test_redirect_append() {
1447        let mut bash = Bash::new();
1448        bash.exec("echo hello > /tmp/append.txt").await.unwrap();
1449        bash.exec("echo world >> /tmp/append.txt").await.unwrap();
1450
1451        let result = bash.exec("cat /tmp/append.txt").await.unwrap();
1452        assert_eq!(result.stdout, "hello\nworld\n");
1453    }
1454
1455    #[tokio::test]
1456    async fn test_command_list_and() {
1457        let mut bash = Bash::new();
1458        let result = bash.exec("true && echo success").await.unwrap();
1459        assert_eq!(result.stdout, "success\n");
1460    }
1461
1462    #[tokio::test]
1463    async fn test_command_list_and_short_circuit() {
1464        let mut bash = Bash::new();
1465        let result = bash.exec("false && echo should_not_print").await.unwrap();
1466        assert_eq!(result.stdout, "");
1467        assert_eq!(result.exit_code, 1);
1468    }
1469
1470    #[tokio::test]
1471    async fn test_command_list_or() {
1472        let mut bash = Bash::new();
1473        let result = bash.exec("false || echo fallback").await.unwrap();
1474        assert_eq!(result.stdout, "fallback\n");
1475    }
1476
1477    #[tokio::test]
1478    async fn test_command_list_or_short_circuit() {
1479        let mut bash = Bash::new();
1480        let result = bash.exec("true || echo should_not_print").await.unwrap();
1481        assert_eq!(result.stdout, "");
1482        assert_eq!(result.exit_code, 0);
1483    }
1484
1485    /// Phase 1 target test: `echo $HOME | cat > /tmp/out && cat /tmp/out`
1486    #[tokio::test]
1487    async fn test_phase1_target() {
1488        let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
1489
1490        let result = bash
1491            .exec("echo $HOME | cat > /tmp/out && cat /tmp/out")
1492            .await
1493            .unwrap();
1494
1495        assert_eq!(result.stdout, "/home/testuser\n");
1496        assert_eq!(result.exit_code, 0);
1497    }
1498
1499    #[tokio::test]
1500    async fn test_redirect_input() {
1501        let mut bash = Bash::new();
1502        // Create a file first
1503        bash.exec("echo hello > /tmp/input.txt").await.unwrap();
1504
1505        // Read it using input redirection
1506        let result = bash.exec("cat < /tmp/input.txt").await.unwrap();
1507        assert_eq!(result.stdout, "hello\n");
1508    }
1509
1510    #[tokio::test]
1511    async fn test_here_string() {
1512        let mut bash = Bash::new();
1513        let result = bash.exec("cat <<< hello").await.unwrap();
1514        assert_eq!(result.stdout, "hello\n");
1515    }
1516
1517    #[tokio::test]
1518    async fn test_if_true() {
1519        let mut bash = Bash::new();
1520        let result = bash.exec("if true; then echo yes; fi").await.unwrap();
1521        assert_eq!(result.stdout, "yes\n");
1522    }
1523
1524    #[tokio::test]
1525    async fn test_if_false() {
1526        let mut bash = Bash::new();
1527        let result = bash.exec("if false; then echo yes; fi").await.unwrap();
1528        assert_eq!(result.stdout, "");
1529    }
1530
1531    #[tokio::test]
1532    async fn test_if_else() {
1533        let mut bash = Bash::new();
1534        let result = bash
1535            .exec("if false; then echo yes; else echo no; fi")
1536            .await
1537            .unwrap();
1538        assert_eq!(result.stdout, "no\n");
1539    }
1540
1541    #[tokio::test]
1542    async fn test_if_elif() {
1543        let mut bash = Bash::new();
1544        let result = bash
1545            .exec("if false; then echo one; elif true; then echo two; else echo three; fi")
1546            .await
1547            .unwrap();
1548        assert_eq!(result.stdout, "two\n");
1549    }
1550
1551    #[tokio::test]
1552    async fn test_for_loop() {
1553        let mut bash = Bash::new();
1554        let result = bash.exec("for i in a b c; do echo $i; done").await.unwrap();
1555        assert_eq!(result.stdout, "a\nb\nc\n");
1556    }
1557
1558    #[tokio::test]
1559    async fn test_for_loop_positional_params() {
1560        let mut bash = Bash::new();
1561        // for x; do ... done iterates over positional parameters inside a function
1562        let result = bash
1563            .exec("f() { for x; do echo $x; done; }; f one two three")
1564            .await
1565            .unwrap();
1566        assert_eq!(result.stdout, "one\ntwo\nthree\n");
1567    }
1568
1569    #[tokio::test]
1570    async fn test_while_loop() {
1571        let mut bash = Bash::new();
1572        // While with false condition - executes 0 times
1573        let result = bash.exec("while false; do echo loop; done").await.unwrap();
1574        assert_eq!(result.stdout, "");
1575    }
1576
1577    #[tokio::test]
1578    async fn test_subshell() {
1579        let mut bash = Bash::new();
1580        let result = bash.exec("(echo hello)").await.unwrap();
1581        assert_eq!(result.stdout, "hello\n");
1582    }
1583
1584    #[tokio::test]
1585    async fn test_brace_group() {
1586        let mut bash = Bash::new();
1587        let result = bash.exec("{ echo hello; }").await.unwrap();
1588        assert_eq!(result.stdout, "hello\n");
1589    }
1590
1591    #[tokio::test]
1592    async fn test_function_keyword() {
1593        let mut bash = Bash::new();
1594        let result = bash
1595            .exec("function greet { echo hello; }; greet")
1596            .await
1597            .unwrap();
1598        assert_eq!(result.stdout, "hello\n");
1599    }
1600
1601    #[tokio::test]
1602    async fn test_function_posix() {
1603        let mut bash = Bash::new();
1604        let result = bash.exec("greet() { echo hello; }; greet").await.unwrap();
1605        assert_eq!(result.stdout, "hello\n");
1606    }
1607
1608    #[tokio::test]
1609    async fn test_function_args() {
1610        let mut bash = Bash::new();
1611        let result = bash
1612            .exec("greet() { echo $1 $2; }; greet world foo")
1613            .await
1614            .unwrap();
1615        assert_eq!(result.stdout, "world foo\n");
1616    }
1617
1618    #[tokio::test]
1619    async fn test_function_arg_count() {
1620        let mut bash = Bash::new();
1621        let result = bash
1622            .exec("count() { echo $#; }; count a b c")
1623            .await
1624            .unwrap();
1625        assert_eq!(result.stdout, "3\n");
1626    }
1627
1628    #[tokio::test]
1629    async fn test_case_literal() {
1630        let mut bash = Bash::new();
1631        let result = bash
1632            .exec("case foo in foo) echo matched ;; esac")
1633            .await
1634            .unwrap();
1635        assert_eq!(result.stdout, "matched\n");
1636    }
1637
1638    #[tokio::test]
1639    async fn test_case_wildcard() {
1640        let mut bash = Bash::new();
1641        let result = bash
1642            .exec("case bar in *) echo default ;; esac")
1643            .await
1644            .unwrap();
1645        assert_eq!(result.stdout, "default\n");
1646    }
1647
1648    #[tokio::test]
1649    async fn test_case_no_match() {
1650        let mut bash = Bash::new();
1651        let result = bash.exec("case foo in bar) echo no ;; esac").await.unwrap();
1652        assert_eq!(result.stdout, "");
1653    }
1654
1655    #[tokio::test]
1656    async fn test_case_multiple_patterns() {
1657        let mut bash = Bash::new();
1658        let result = bash
1659            .exec("case foo in bar|foo|baz) echo matched ;; esac")
1660            .await
1661            .unwrap();
1662        assert_eq!(result.stdout, "matched\n");
1663    }
1664
1665    #[tokio::test]
1666    async fn test_case_bracket_expr() {
1667        let mut bash = Bash::new();
1668        // Test [abc] bracket expression
1669        let result = bash
1670            .exec("case b in [abc]) echo matched ;; esac")
1671            .await
1672            .unwrap();
1673        assert_eq!(result.stdout, "matched\n");
1674    }
1675
1676    #[tokio::test]
1677    async fn test_case_bracket_range() {
1678        let mut bash = Bash::new();
1679        // Test [a-z] range expression
1680        let result = bash
1681            .exec("case m in [a-z]) echo letter ;; esac")
1682            .await
1683            .unwrap();
1684        assert_eq!(result.stdout, "letter\n");
1685    }
1686
1687    #[tokio::test]
1688    async fn test_case_bracket_negation() {
1689        let mut bash = Bash::new();
1690        // Test [!abc] negation
1691        let result = bash
1692            .exec("case x in [!abc]) echo not_abc ;; esac")
1693            .await
1694            .unwrap();
1695        assert_eq!(result.stdout, "not_abc\n");
1696    }
1697
1698    #[tokio::test]
1699    async fn test_break_as_command() {
1700        let mut bash = Bash::new();
1701        // Just run break alone - should not error
1702        let result = bash.exec("break").await.unwrap();
1703        // break outside of loop returns success with no output
1704        assert_eq!(result.exit_code, 0);
1705    }
1706
1707    #[tokio::test]
1708    async fn test_for_one_item() {
1709        let mut bash = Bash::new();
1710        // Simple for loop with one item
1711        let result = bash.exec("for i in a; do echo $i; done").await.unwrap();
1712        assert_eq!(result.stdout, "a\n");
1713    }
1714
1715    #[tokio::test]
1716    async fn test_for_with_break() {
1717        let mut bash = Bash::new();
1718        // For loop with break
1719        let result = bash.exec("for i in a; do break; done").await.unwrap();
1720        assert_eq!(result.stdout, "");
1721        assert_eq!(result.exit_code, 0);
1722    }
1723
1724    #[tokio::test]
1725    async fn test_for_echo_break() {
1726        let mut bash = Bash::new();
1727        // For loop with echo then break - tests the semicolon command list in body
1728        let result = bash
1729            .exec("for i in a b c; do echo $i; break; done")
1730            .await
1731            .unwrap();
1732        assert_eq!(result.stdout, "a\n");
1733    }
1734
1735    #[tokio::test]
1736    async fn test_test_string_empty() {
1737        let mut bash = Bash::new();
1738        let result = bash.exec("test -z '' && echo yes").await.unwrap();
1739        assert_eq!(result.stdout, "yes\n");
1740    }
1741
1742    #[tokio::test]
1743    async fn test_test_string_not_empty() {
1744        let mut bash = Bash::new();
1745        let result = bash.exec("test -n 'hello' && echo yes").await.unwrap();
1746        assert_eq!(result.stdout, "yes\n");
1747    }
1748
1749    #[tokio::test]
1750    async fn test_test_string_equal() {
1751        let mut bash = Bash::new();
1752        let result = bash.exec("test foo = foo && echo yes").await.unwrap();
1753        assert_eq!(result.stdout, "yes\n");
1754    }
1755
1756    #[tokio::test]
1757    async fn test_test_string_not_equal() {
1758        let mut bash = Bash::new();
1759        let result = bash.exec("test foo != bar && echo yes").await.unwrap();
1760        assert_eq!(result.stdout, "yes\n");
1761    }
1762
1763    #[tokio::test]
1764    async fn test_test_numeric_equal() {
1765        let mut bash = Bash::new();
1766        let result = bash.exec("test 5 -eq 5 && echo yes").await.unwrap();
1767        assert_eq!(result.stdout, "yes\n");
1768    }
1769
1770    #[tokio::test]
1771    async fn test_test_numeric_less_than() {
1772        let mut bash = Bash::new();
1773        let result = bash.exec("test 3 -lt 5 && echo yes").await.unwrap();
1774        assert_eq!(result.stdout, "yes\n");
1775    }
1776
1777    #[tokio::test]
1778    async fn test_bracket_form() {
1779        let mut bash = Bash::new();
1780        let result = bash.exec("[ foo = foo ] && echo yes").await.unwrap();
1781        assert_eq!(result.stdout, "yes\n");
1782    }
1783
1784    #[tokio::test]
1785    async fn test_if_with_test() {
1786        let mut bash = Bash::new();
1787        let result = bash
1788            .exec("if [ 5 -gt 3 ]; then echo bigger; fi")
1789            .await
1790            .unwrap();
1791        assert_eq!(result.stdout, "bigger\n");
1792    }
1793
1794    #[tokio::test]
1795    async fn test_variable_assignment() {
1796        let mut bash = Bash::new();
1797        let result = bash.exec("FOO=bar; echo $FOO").await.unwrap();
1798        assert_eq!(result.stdout, "bar\n");
1799    }
1800
1801    #[tokio::test]
1802    async fn test_variable_assignment_inline() {
1803        let mut bash = Bash::new();
1804        // Assignment before command
1805        let result = bash.exec("MSG=hello; echo $MSG world").await.unwrap();
1806        assert_eq!(result.stdout, "hello world\n");
1807    }
1808
1809    #[tokio::test]
1810    async fn test_variable_assignment_only() {
1811        let mut bash = Bash::new();
1812        // Assignment without command should succeed silently
1813        let result = bash.exec("FOO=bar").await.unwrap();
1814        assert_eq!(result.stdout, "");
1815        assert_eq!(result.exit_code, 0);
1816
1817        // Verify the variable was set
1818        let result = bash.exec("echo $FOO").await.unwrap();
1819        assert_eq!(result.stdout, "bar\n");
1820    }
1821
1822    #[tokio::test]
1823    async fn test_multiple_assignments() {
1824        let mut bash = Bash::new();
1825        let result = bash.exec("A=1; B=2; C=3; echo $A $B $C").await.unwrap();
1826        assert_eq!(result.stdout, "1 2 3\n");
1827    }
1828
1829    #[tokio::test]
1830    async fn test_prefix_assignment_visible_in_env() {
1831        let mut bash = Bash::new();
1832        // VAR=value command should make VAR visible in the command's environment
1833        let result = bash.exec("MYVAR=hello printenv MYVAR").await.unwrap();
1834        assert_eq!(result.stdout, "hello\n");
1835    }
1836
1837    #[tokio::test]
1838    async fn test_prefix_assignment_temporary() {
1839        let mut bash = Bash::new();
1840        // Prefix assignment should NOT persist after the command
1841        bash.exec("MYVAR=hello printenv MYVAR").await.unwrap();
1842        let result = bash.exec("echo ${MYVAR:-unset}").await.unwrap();
1843        assert_eq!(result.stdout, "unset\n");
1844    }
1845
1846    #[tokio::test]
1847    async fn test_prefix_assignment_does_not_clobber_existing_env() {
1848        let mut bash = Bash::new();
1849        // Set up existing env var
1850        let result = bash
1851            .exec("EXISTING=original; export EXISTING; EXISTING=temp printenv EXISTING")
1852            .await
1853            .unwrap();
1854        assert_eq!(result.stdout, "temp\n");
1855    }
1856
1857    #[tokio::test]
1858    async fn test_prefix_assignment_multiple_vars() {
1859        let mut bash = Bash::new();
1860        // Multiple prefix assignments on same command
1861        let result = bash.exec("A=one B=two printenv A").await.unwrap();
1862        assert_eq!(result.stdout, "one\n");
1863        assert_eq!(result.exit_code, 0);
1864    }
1865
1866    #[tokio::test]
1867    async fn test_prefix_assignment_empty_value() {
1868        let mut bash = Bash::new();
1869        // Empty value is still set in environment
1870        let result = bash.exec("MYVAR= printenv MYVAR").await.unwrap();
1871        assert_eq!(result.stdout, "\n");
1872        assert_eq!(result.exit_code, 0);
1873    }
1874
1875    #[tokio::test]
1876    async fn test_prefix_assignment_not_found_without_prefix() {
1877        let mut bash = Bash::new();
1878        // printenv for a var that was never set should fail
1879        let result = bash.exec("printenv NONEXISTENT").await.unwrap();
1880        assert_eq!(result.stdout, "");
1881        assert_eq!(result.exit_code, 1);
1882    }
1883
1884    #[tokio::test]
1885    async fn test_prefix_assignment_does_not_persist_in_variables() {
1886        let mut bash = Bash::new();
1887        // After prefix assignment with command, var should not be in shell scope
1888        bash.exec("TMPVAR=gone echo ok").await.unwrap();
1889        let result = bash.exec("echo \"${TMPVAR:-unset}\"").await.unwrap();
1890        assert_eq!(result.stdout, "unset\n");
1891    }
1892
1893    #[tokio::test]
1894    async fn test_assignment_only_persists() {
1895        let mut bash = Bash::new();
1896        // Assignment without a command should persist (not a prefix assignment)
1897        bash.exec("PERSIST=yes").await.unwrap();
1898        let result = bash.exec("echo $PERSIST").await.unwrap();
1899        assert_eq!(result.stdout, "yes\n");
1900    }
1901
1902    #[tokio::test]
1903    async fn test_printf_string() {
1904        let mut bash = Bash::new();
1905        let result = bash.exec("printf '%s' hello").await.unwrap();
1906        assert_eq!(result.stdout, "hello");
1907    }
1908
1909    #[tokio::test]
1910    async fn test_printf_newline() {
1911        let mut bash = Bash::new();
1912        let result = bash.exec("printf 'hello\\n'").await.unwrap();
1913        assert_eq!(result.stdout, "hello\n");
1914    }
1915
1916    #[tokio::test]
1917    async fn test_printf_multiple_args() {
1918        let mut bash = Bash::new();
1919        let result = bash.exec("printf '%s %s\\n' hello world").await.unwrap();
1920        assert_eq!(result.stdout, "hello world\n");
1921    }
1922
1923    #[tokio::test]
1924    async fn test_printf_integer() {
1925        let mut bash = Bash::new();
1926        let result = bash.exec("printf '%d' 42").await.unwrap();
1927        assert_eq!(result.stdout, "42");
1928    }
1929
1930    #[tokio::test]
1931    async fn test_export() {
1932        let mut bash = Bash::new();
1933        let result = bash.exec("export FOO=bar; echo $FOO").await.unwrap();
1934        assert_eq!(result.stdout, "bar\n");
1935    }
1936
1937    #[tokio::test]
1938    async fn test_read_basic() {
1939        let mut bash = Bash::new();
1940        let result = bash.exec("echo hello | read VAR; echo $VAR").await.unwrap();
1941        assert_eq!(result.stdout, "hello\n");
1942    }
1943
1944    #[tokio::test]
1945    async fn test_read_multiple_vars() {
1946        let mut bash = Bash::new();
1947        let result = bash
1948            .exec("echo 'a b c' | read X Y Z; echo $X $Y $Z")
1949            .await
1950            .unwrap();
1951        assert_eq!(result.stdout, "a b c\n");
1952    }
1953
1954    #[tokio::test]
1955    async fn test_glob_star() {
1956        let mut bash = Bash::new();
1957        // Create some files
1958        bash.exec("echo a > /tmp/file1.txt").await.unwrap();
1959        bash.exec("echo b > /tmp/file2.txt").await.unwrap();
1960        bash.exec("echo c > /tmp/other.log").await.unwrap();
1961
1962        // Glob for *.txt files
1963        let result = bash.exec("echo /tmp/*.txt").await.unwrap();
1964        assert_eq!(result.stdout, "/tmp/file1.txt /tmp/file2.txt\n");
1965    }
1966
1967    #[tokio::test]
1968    async fn test_glob_question_mark() {
1969        let mut bash = Bash::new();
1970        // Create some files
1971        bash.exec("echo a > /tmp/a1.txt").await.unwrap();
1972        bash.exec("echo b > /tmp/a2.txt").await.unwrap();
1973        bash.exec("echo c > /tmp/a10.txt").await.unwrap();
1974
1975        // Glob for a?.txt (single character)
1976        let result = bash.exec("echo /tmp/a?.txt").await.unwrap();
1977        assert_eq!(result.stdout, "/tmp/a1.txt /tmp/a2.txt\n");
1978    }
1979
1980    #[tokio::test]
1981    async fn test_glob_no_match() {
1982        let mut bash = Bash::new();
1983        // Glob that doesn't match anything should return the pattern
1984        let result = bash.exec("echo /nonexistent/*.xyz").await.unwrap();
1985        assert_eq!(result.stdout, "/nonexistent/*.xyz\n");
1986    }
1987
1988    #[tokio::test]
1989    async fn test_command_substitution() {
1990        let mut bash = Bash::new();
1991        let result = bash.exec("echo $(echo hello)").await.unwrap();
1992        assert_eq!(result.stdout, "hello\n");
1993    }
1994
1995    #[tokio::test]
1996    async fn test_command_substitution_in_string() {
1997        let mut bash = Bash::new();
1998        let result = bash.exec("echo \"result: $(echo 42)\"").await.unwrap();
1999        assert_eq!(result.stdout, "result: 42\n");
2000    }
2001
2002    #[tokio::test]
2003    async fn test_command_substitution_pipeline() {
2004        let mut bash = Bash::new();
2005        let result = bash.exec("echo $(echo hello | cat)").await.unwrap();
2006        assert_eq!(result.stdout, "hello\n");
2007    }
2008
2009    #[tokio::test]
2010    async fn test_command_substitution_variable() {
2011        let mut bash = Bash::new();
2012        let result = bash.exec("VAR=$(echo test); echo $VAR").await.unwrap();
2013        assert_eq!(result.stdout, "test\n");
2014    }
2015
2016    #[tokio::test]
2017    async fn test_arithmetic_simple() {
2018        let mut bash = Bash::new();
2019        let result = bash.exec("echo $((1 + 2))").await.unwrap();
2020        assert_eq!(result.stdout, "3\n");
2021    }
2022
2023    #[tokio::test]
2024    async fn test_arithmetic_multiply() {
2025        let mut bash = Bash::new();
2026        let result = bash.exec("echo $((3 * 4))").await.unwrap();
2027        assert_eq!(result.stdout, "12\n");
2028    }
2029
2030    #[tokio::test]
2031    async fn test_arithmetic_with_variable() {
2032        let mut bash = Bash::new();
2033        let result = bash.exec("X=5; echo $((X + 3))").await.unwrap();
2034        assert_eq!(result.stdout, "8\n");
2035    }
2036
2037    #[tokio::test]
2038    async fn test_arithmetic_complex() {
2039        let mut bash = Bash::new();
2040        let result = bash.exec("echo $((2 + 3 * 4))").await.unwrap();
2041        assert_eq!(result.stdout, "14\n");
2042    }
2043
2044    #[tokio::test]
2045    async fn test_heredoc_simple() {
2046        let mut bash = Bash::new();
2047        let result = bash.exec("cat <<EOF\nhello\nworld\nEOF").await.unwrap();
2048        assert_eq!(result.stdout, "hello\nworld\n");
2049    }
2050
2051    #[tokio::test]
2052    async fn test_heredoc_single_line() {
2053        let mut bash = Bash::new();
2054        let result = bash.exec("cat <<END\ntest\nEND").await.unwrap();
2055        assert_eq!(result.stdout, "test\n");
2056    }
2057
2058    #[tokio::test]
2059    async fn test_unset() {
2060        let mut bash = Bash::new();
2061        let result = bash
2062            .exec("FOO=bar; unset FOO; echo \"x${FOO}y\"")
2063            .await
2064            .unwrap();
2065        assert_eq!(result.stdout, "xy\n");
2066    }
2067
2068    #[tokio::test]
2069    async fn test_local_basic() {
2070        let mut bash = Bash::new();
2071        // Test that local command runs without error
2072        let result = bash.exec("local X=test; echo $X").await.unwrap();
2073        assert_eq!(result.stdout, "test\n");
2074    }
2075
2076    #[tokio::test]
2077    async fn test_set_option() {
2078        let mut bash = Bash::new();
2079        let result = bash.exec("set -e; echo ok").await.unwrap();
2080        assert_eq!(result.stdout, "ok\n");
2081    }
2082
2083    #[tokio::test]
2084    async fn test_param_default() {
2085        let mut bash = Bash::new();
2086        // ${var:-default} when unset
2087        let result = bash.exec("echo ${UNSET:-default}").await.unwrap();
2088        assert_eq!(result.stdout, "default\n");
2089
2090        // ${var:-default} when set
2091        let result = bash.exec("X=value; echo ${X:-default}").await.unwrap();
2092        assert_eq!(result.stdout, "value\n");
2093    }
2094
2095    #[tokio::test]
2096    async fn test_param_assign_default() {
2097        let mut bash = Bash::new();
2098        // ${var:=default} assigns when unset
2099        let result = bash.exec("echo ${NEW:=assigned}; echo $NEW").await.unwrap();
2100        assert_eq!(result.stdout, "assigned\nassigned\n");
2101    }
2102
2103    #[tokio::test]
2104    async fn test_param_length() {
2105        let mut bash = Bash::new();
2106        let result = bash.exec("X=hello; echo ${#X}").await.unwrap();
2107        assert_eq!(result.stdout, "5\n");
2108    }
2109
2110    #[tokio::test]
2111    async fn test_param_remove_prefix() {
2112        let mut bash = Bash::new();
2113        // ${var#pattern} - remove shortest prefix
2114        let result = bash.exec("X=hello.world.txt; echo ${X#*.}").await.unwrap();
2115        assert_eq!(result.stdout, "world.txt\n");
2116    }
2117
2118    #[tokio::test]
2119    async fn test_param_remove_suffix() {
2120        let mut bash = Bash::new();
2121        // ${var%pattern} - remove shortest suffix
2122        let result = bash.exec("X=file.tar.gz; echo ${X%.*}").await.unwrap();
2123        assert_eq!(result.stdout, "file.tar\n");
2124    }
2125
2126    #[tokio::test]
2127    async fn test_array_basic() {
2128        let mut bash = Bash::new();
2129        // Basic array declaration and access
2130        let result = bash.exec("arr=(a b c); echo ${arr[1]}").await.unwrap();
2131        assert_eq!(result.stdout, "b\n");
2132    }
2133
2134    #[tokio::test]
2135    async fn test_array_all_elements() {
2136        let mut bash = Bash::new();
2137        // ${arr[@]} - all elements
2138        let result = bash
2139            .exec("arr=(one two three); echo ${arr[@]}")
2140            .await
2141            .unwrap();
2142        assert_eq!(result.stdout, "one two three\n");
2143    }
2144
2145    #[tokio::test]
2146    async fn test_array_length() {
2147        let mut bash = Bash::new();
2148        // ${#arr[@]} - number of elements
2149        let result = bash.exec("arr=(a b c d e); echo ${#arr[@]}").await.unwrap();
2150        assert_eq!(result.stdout, "5\n");
2151    }
2152
2153    #[tokio::test]
2154    async fn test_array_indexed_assignment() {
2155        let mut bash = Bash::new();
2156        // arr[n]=value assignment
2157        let result = bash
2158            .exec("arr[0]=first; arr[1]=second; echo ${arr[0]} ${arr[1]}")
2159            .await
2160            .unwrap();
2161        assert_eq!(result.stdout, "first second\n");
2162    }
2163
2164    // Resource limit tests
2165
2166    #[tokio::test]
2167    async fn test_command_limit() {
2168        let limits = ExecutionLimits::new().max_commands(5);
2169        let mut bash = Bash::builder().limits(limits).build();
2170
2171        // Run 6 commands - should fail on the 6th
2172        let result = bash.exec("true; true; true; true; true; true").await;
2173        assert!(result.is_err());
2174        let err = result.unwrap_err();
2175        assert!(
2176            err.to_string().contains("maximum command count exceeded"),
2177            "Expected command limit error, got: {}",
2178            err
2179        );
2180    }
2181
2182    #[tokio::test]
2183    async fn test_command_limit_not_exceeded() {
2184        let limits = ExecutionLimits::new().max_commands(10);
2185        let mut bash = Bash::builder().limits(limits).build();
2186
2187        // Run 5 commands - should succeed
2188        let result = bash.exec("true; true; true; true; true").await.unwrap();
2189        assert_eq!(result.exit_code, 0);
2190    }
2191
2192    #[tokio::test]
2193    async fn test_loop_iteration_limit() {
2194        let limits = ExecutionLimits::new().max_loop_iterations(5);
2195        let mut bash = Bash::builder().limits(limits).build();
2196
2197        // Loop that tries to run 10 times
2198        let result = bash
2199            .exec("for i in 1 2 3 4 5 6 7 8 9 10; do echo $i; done")
2200            .await;
2201        assert!(result.is_err());
2202        let err = result.unwrap_err();
2203        assert!(
2204            err.to_string().contains("maximum loop iterations exceeded"),
2205            "Expected loop limit error, got: {}",
2206            err
2207        );
2208    }
2209
2210    #[tokio::test]
2211    async fn test_loop_iteration_limit_not_exceeded() {
2212        let limits = ExecutionLimits::new().max_loop_iterations(10);
2213        let mut bash = Bash::builder().limits(limits).build();
2214
2215        // Loop that runs 5 times - should succeed
2216        let result = bash
2217            .exec("for i in 1 2 3 4 5; do echo $i; done")
2218            .await
2219            .unwrap();
2220        assert_eq!(result.stdout, "1\n2\n3\n4\n5\n");
2221    }
2222
2223    #[tokio::test]
2224    async fn test_function_depth_limit() {
2225        let limits = ExecutionLimits::new().max_function_depth(3);
2226        let mut bash = Bash::builder().limits(limits).build();
2227
2228        // Recursive function that would go 5 deep
2229        let result = bash
2230            .exec("f() { echo $1; if [ $1 -lt 5 ]; then f $(($1 + 1)); fi; }; f 1")
2231            .await;
2232        assert!(result.is_err());
2233        let err = result.unwrap_err();
2234        assert!(
2235            err.to_string().contains("maximum function depth exceeded"),
2236            "Expected function depth error, got: {}",
2237            err
2238        );
2239    }
2240
2241    #[tokio::test]
2242    async fn test_function_depth_limit_not_exceeded() {
2243        let limits = ExecutionLimits::new().max_function_depth(10);
2244        let mut bash = Bash::builder().limits(limits).build();
2245
2246        // Simple function call - should succeed
2247        let result = bash.exec("f() { echo hello; }; f").await.unwrap();
2248        assert_eq!(result.stdout, "hello\n");
2249    }
2250
2251    #[tokio::test]
2252    async fn test_while_loop_limit() {
2253        let limits = ExecutionLimits::new().max_loop_iterations(3);
2254        let mut bash = Bash::builder().limits(limits).build();
2255
2256        // While loop with counter
2257        let result = bash
2258            .exec("i=0; while [ $i -lt 10 ]; do echo $i; i=$((i + 1)); done")
2259            .await;
2260        assert!(result.is_err());
2261        let err = result.unwrap_err();
2262        assert!(
2263            err.to_string().contains("maximum loop iterations exceeded"),
2264            "Expected loop limit error, got: {}",
2265            err
2266        );
2267    }
2268
2269    #[tokio::test]
2270    async fn test_default_limits_allow_normal_scripts() {
2271        // Default limits should allow typical scripts to run
2272        let mut bash = Bash::new();
2273        // Avoid using "done" as a word after a for loop - it causes parsing ambiguity
2274        let result = bash
2275            .exec("for i in 1 2 3 4 5; do echo $i; done && echo finished")
2276            .await
2277            .unwrap();
2278        assert_eq!(result.stdout, "1\n2\n3\n4\n5\nfinished\n");
2279    }
2280
2281    #[tokio::test]
2282    async fn test_for_followed_by_echo_done() {
2283        let mut bash = Bash::new();
2284        let result = bash
2285            .exec("for i in 1; do echo $i; done; echo ok")
2286            .await
2287            .unwrap();
2288        assert_eq!(result.stdout, "1\nok\n");
2289    }
2290
2291    // Filesystem access tests
2292
2293    #[tokio::test]
2294    async fn test_fs_read_write_binary() {
2295        let bash = Bash::new();
2296        let fs = bash.fs();
2297        let path = std::path::Path::new("/tmp/binary.bin");
2298
2299        // Write binary data with null bytes and high bytes
2300        let binary_data: Vec<u8> = vec![0x00, 0x01, 0xFF, 0xFE, 0x42, 0x00, 0x7F];
2301        fs.write_file(path, &binary_data).await.unwrap();
2302
2303        // Read it back
2304        let content = fs.read_file(path).await.unwrap();
2305        assert_eq!(content, binary_data);
2306    }
2307
2308    #[tokio::test]
2309    async fn test_fs_write_then_exec_cat() {
2310        let mut bash = Bash::new();
2311        let path = std::path::Path::new("/tmp/prepopulated.txt");
2312
2313        // Pre-populate a file before running bash
2314        bash.fs()
2315            .write_file(path, b"Hello from Rust!\n")
2316            .await
2317            .unwrap();
2318
2319        // Access it from bash
2320        let result = bash.exec("cat /tmp/prepopulated.txt").await.unwrap();
2321        assert_eq!(result.stdout, "Hello from Rust!\n");
2322    }
2323
2324    #[tokio::test]
2325    async fn test_fs_exec_then_read() {
2326        let mut bash = Bash::new();
2327        let path = std::path::Path::new("/tmp/from_bash.txt");
2328
2329        // Create file via bash
2330        bash.exec("echo 'Created by bash' > /tmp/from_bash.txt")
2331            .await
2332            .unwrap();
2333
2334        // Read it directly
2335        let content = bash.fs().read_file(path).await.unwrap();
2336        assert_eq!(content, b"Created by bash\n");
2337    }
2338
2339    #[tokio::test]
2340    async fn test_fs_exists_and_stat() {
2341        let bash = Bash::new();
2342        let fs = bash.fs();
2343        let path = std::path::Path::new("/tmp/testfile.txt");
2344
2345        // File doesn't exist yet
2346        assert!(!fs.exists(path).await.unwrap());
2347
2348        // Create it
2349        fs.write_file(path, b"content").await.unwrap();
2350
2351        // Now exists
2352        assert!(fs.exists(path).await.unwrap());
2353
2354        // Check metadata
2355        let stat = fs.stat(path).await.unwrap();
2356        assert!(stat.file_type.is_file());
2357        assert_eq!(stat.size, 7); // "content" = 7 bytes
2358    }
2359
2360    #[tokio::test]
2361    async fn test_fs_mkdir_and_read_dir() {
2362        let bash = Bash::new();
2363        let fs = bash.fs();
2364
2365        // Create nested directories
2366        fs.mkdir(std::path::Path::new("/data/nested/dir"), true)
2367            .await
2368            .unwrap();
2369
2370        // Create some files
2371        fs.write_file(std::path::Path::new("/data/file1.txt"), b"1")
2372            .await
2373            .unwrap();
2374        fs.write_file(std::path::Path::new("/data/file2.txt"), b"2")
2375            .await
2376            .unwrap();
2377
2378        // Read directory
2379        let entries = fs.read_dir(std::path::Path::new("/data")).await.unwrap();
2380        let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
2381        assert!(names.contains(&"nested"));
2382        assert!(names.contains(&"file1.txt"));
2383        assert!(names.contains(&"file2.txt"));
2384    }
2385
2386    #[tokio::test]
2387    async fn test_fs_append() {
2388        let bash = Bash::new();
2389        let fs = bash.fs();
2390        let path = std::path::Path::new("/tmp/append.txt");
2391
2392        fs.write_file(path, b"line1\n").await.unwrap();
2393        fs.append_file(path, b"line2\n").await.unwrap();
2394        fs.append_file(path, b"line3\n").await.unwrap();
2395
2396        let content = fs.read_file(path).await.unwrap();
2397        assert_eq!(content, b"line1\nline2\nline3\n");
2398    }
2399
2400    #[tokio::test]
2401    async fn test_fs_copy_and_rename() {
2402        let bash = Bash::new();
2403        let fs = bash.fs();
2404
2405        fs.write_file(std::path::Path::new("/tmp/original.txt"), b"data")
2406            .await
2407            .unwrap();
2408
2409        // Copy
2410        fs.copy(
2411            std::path::Path::new("/tmp/original.txt"),
2412            std::path::Path::new("/tmp/copied.txt"),
2413        )
2414        .await
2415        .unwrap();
2416
2417        // Rename
2418        fs.rename(
2419            std::path::Path::new("/tmp/copied.txt"),
2420            std::path::Path::new("/tmp/renamed.txt"),
2421        )
2422        .await
2423        .unwrap();
2424
2425        // Verify
2426        let content = fs
2427            .read_file(std::path::Path::new("/tmp/renamed.txt"))
2428            .await
2429            .unwrap();
2430        assert_eq!(content, b"data");
2431        assert!(!fs
2432            .exists(std::path::Path::new("/tmp/copied.txt"))
2433            .await
2434            .unwrap());
2435    }
2436
2437    // Bug fix tests
2438
2439    #[tokio::test]
2440    async fn test_echo_done_as_argument() {
2441        // BUG: "done" should be parsed as a regular argument when not in loop context
2442        let mut bash = Bash::new();
2443        let result = bash
2444            .exec("for i in 1; do echo $i; done; echo done")
2445            .await
2446            .unwrap();
2447        assert_eq!(result.stdout, "1\ndone\n");
2448    }
2449
2450    #[tokio::test]
2451    async fn test_simple_echo_done() {
2452        // Simple echo done without any loop
2453        let mut bash = Bash::new();
2454        let result = bash.exec("echo done").await.unwrap();
2455        assert_eq!(result.stdout, "done\n");
2456    }
2457
2458    #[tokio::test]
2459    async fn test_dev_null_redirect() {
2460        // BUG: Redirecting to /dev/null should discard output silently
2461        let mut bash = Bash::new();
2462        let result = bash.exec("echo hello > /dev/null; echo ok").await.unwrap();
2463        assert_eq!(result.stdout, "ok\n");
2464    }
2465
2466    #[tokio::test]
2467    async fn test_string_concatenation_in_loop() {
2468        // Test string concatenation in a loop
2469        let mut bash = Bash::new();
2470        // First test: basic for loop still works
2471        let result = bash.exec("for i in a b c; do echo $i; done").await.unwrap();
2472        assert_eq!(result.stdout, "a\nb\nc\n");
2473
2474        // Test variable assignment followed by for loop
2475        let mut bash = Bash::new();
2476        let result = bash
2477            .exec("result=x; for i in a b c; do echo $i; done; echo $result")
2478            .await
2479            .unwrap();
2480        assert_eq!(result.stdout, "a\nb\nc\nx\n");
2481
2482        // Test string concatenation in a loop
2483        let mut bash = Bash::new();
2484        let result = bash
2485            .exec("result=start; for i in a b c; do result=${result}$i; done; echo $result")
2486            .await
2487            .unwrap();
2488        assert_eq!(result.stdout, "startabc\n");
2489    }
2490
2491    // Negative/edge case tests for reserved word handling
2492
2493    #[tokio::test]
2494    async fn test_done_still_terminates_loop() {
2495        // Ensure "done" still works as a loop terminator
2496        let mut bash = Bash::new();
2497        let result = bash.exec("for i in 1 2; do echo $i; done").await.unwrap();
2498        assert_eq!(result.stdout, "1\n2\n");
2499    }
2500
2501    #[tokio::test]
2502    async fn test_fi_still_terminates_if() {
2503        // Ensure "fi" still works as an if terminator
2504        let mut bash = Bash::new();
2505        let result = bash.exec("if true; then echo yes; fi").await.unwrap();
2506        assert_eq!(result.stdout, "yes\n");
2507    }
2508
2509    #[tokio::test]
2510    async fn test_echo_fi_as_argument() {
2511        // "fi" should be a valid argument outside of if context
2512        let mut bash = Bash::new();
2513        let result = bash.exec("echo fi").await.unwrap();
2514        assert_eq!(result.stdout, "fi\n");
2515    }
2516
2517    #[tokio::test]
2518    async fn test_echo_then_as_argument() {
2519        // "then" should be a valid argument outside of if context
2520        let mut bash = Bash::new();
2521        let result = bash.exec("echo then").await.unwrap();
2522        assert_eq!(result.stdout, "then\n");
2523    }
2524
2525    #[tokio::test]
2526    async fn test_reserved_words_in_quotes_are_arguments() {
2527        // Reserved words in quotes should always be arguments
2528        let mut bash = Bash::new();
2529        let result = bash.exec("echo 'done' 'fi' 'then'").await.unwrap();
2530        assert_eq!(result.stdout, "done fi then\n");
2531    }
2532
2533    #[tokio::test]
2534    async fn test_nested_loops_done_keyword() {
2535        // Nested loops should properly match done keywords
2536        let mut bash = Bash::new();
2537        let result = bash
2538            .exec("for i in 1; do for j in a; do echo $i$j; done; done")
2539            .await
2540            .unwrap();
2541        assert_eq!(result.stdout, "1a\n");
2542    }
2543
2544    // Negative/edge case tests for /dev/null
2545
2546    #[tokio::test]
2547    async fn test_dev_null_read_returns_empty() {
2548        // Reading from /dev/null should return empty
2549        let mut bash = Bash::new();
2550        let result = bash.exec("cat /dev/null").await.unwrap();
2551        assert_eq!(result.stdout, "");
2552    }
2553
2554    #[tokio::test]
2555    async fn test_dev_null_append() {
2556        // Appending to /dev/null should work silently
2557        let mut bash = Bash::new();
2558        let result = bash.exec("echo hello >> /dev/null; echo ok").await.unwrap();
2559        assert_eq!(result.stdout, "ok\n");
2560    }
2561
2562    #[tokio::test]
2563    async fn test_dev_null_in_pipeline() {
2564        // /dev/null in a pipeline should work
2565        let mut bash = Bash::new();
2566        let result = bash
2567            .exec("echo hello | cat > /dev/null; echo ok")
2568            .await
2569            .unwrap();
2570        assert_eq!(result.stdout, "ok\n");
2571    }
2572
2573    #[tokio::test]
2574    async fn test_dev_null_exists() {
2575        // /dev/null should exist and be readable
2576        let mut bash = Bash::new();
2577        let result = bash.exec("cat /dev/null; echo exit_$?").await.unwrap();
2578        assert_eq!(result.stdout, "exit_0\n");
2579    }
2580
2581    // Custom username/hostname tests
2582
2583    #[tokio::test]
2584    async fn test_custom_username_whoami() {
2585        let mut bash = Bash::builder().username("alice").build();
2586        let result = bash.exec("whoami").await.unwrap();
2587        assert_eq!(result.stdout, "alice\n");
2588    }
2589
2590    #[tokio::test]
2591    async fn test_custom_username_id() {
2592        let mut bash = Bash::builder().username("bob").build();
2593        let result = bash.exec("id").await.unwrap();
2594        assert!(result.stdout.contains("uid=1000(bob)"));
2595        assert!(result.stdout.contains("gid=1000(bob)"));
2596    }
2597
2598    #[tokio::test]
2599    async fn test_custom_username_sets_user_env() {
2600        let mut bash = Bash::builder().username("charlie").build();
2601        let result = bash.exec("echo $USER").await.unwrap();
2602        assert_eq!(result.stdout, "charlie\n");
2603    }
2604
2605    #[tokio::test]
2606    async fn test_custom_hostname() {
2607        let mut bash = Bash::builder().hostname("my-server").build();
2608        let result = bash.exec("hostname").await.unwrap();
2609        assert_eq!(result.stdout, "my-server\n");
2610    }
2611
2612    #[tokio::test]
2613    async fn test_custom_hostname_uname() {
2614        let mut bash = Bash::builder().hostname("custom-host").build();
2615        let result = bash.exec("uname -n").await.unwrap();
2616        assert_eq!(result.stdout, "custom-host\n");
2617    }
2618
2619    #[tokio::test]
2620    async fn test_default_username_and_hostname() {
2621        // Default values should still work
2622        let mut bash = Bash::new();
2623        let result = bash.exec("whoami").await.unwrap();
2624        assert_eq!(result.stdout, "sandbox\n");
2625
2626        let result = bash.exec("hostname").await.unwrap();
2627        assert_eq!(result.stdout, "bashkit-sandbox\n");
2628    }
2629
2630    #[tokio::test]
2631    async fn test_custom_username_and_hostname_combined() {
2632        let mut bash = Bash::builder()
2633            .username("deploy")
2634            .hostname("prod-server-01")
2635            .build();
2636
2637        let result = bash.exec("whoami && hostname").await.unwrap();
2638        assert_eq!(result.stdout, "deploy\nprod-server-01\n");
2639
2640        let result = bash.exec("echo $USER").await.unwrap();
2641        assert_eq!(result.stdout, "deploy\n");
2642    }
2643
2644    // Custom builtins tests
2645
2646    mod custom_builtins {
2647        use super::*;
2648        use crate::builtins::{Builtin, Context};
2649        use crate::ExecResult;
2650        use async_trait::async_trait;
2651
2652        /// A simple custom builtin that outputs a static string
2653        struct Hello;
2654
2655        #[async_trait]
2656        impl Builtin for Hello {
2657            async fn execute(&self, _ctx: Context<'_>) -> crate::Result<ExecResult> {
2658                Ok(ExecResult::ok("Hello from custom builtin!\n".to_string()))
2659            }
2660        }
2661
2662        #[tokio::test]
2663        async fn test_custom_builtin_basic() {
2664            let mut bash = Bash::builder().builtin("hello", Box::new(Hello)).build();
2665
2666            let result = bash.exec("hello").await.unwrap();
2667            assert_eq!(result.stdout, "Hello from custom builtin!\n");
2668            assert_eq!(result.exit_code, 0);
2669        }
2670
2671        /// A custom builtin that uses arguments
2672        struct Greet;
2673
2674        #[async_trait]
2675        impl Builtin for Greet {
2676            async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2677                let name = ctx.args.first().map(|s| s.as_str()).unwrap_or("World");
2678                Ok(ExecResult::ok(format!("Hello, {}!\n", name)))
2679            }
2680        }
2681
2682        #[tokio::test]
2683        async fn test_custom_builtin_with_args() {
2684            let mut bash = Bash::builder().builtin("greet", Box::new(Greet)).build();
2685
2686            let result = bash.exec("greet").await.unwrap();
2687            assert_eq!(result.stdout, "Hello, World!\n");
2688
2689            let result = bash.exec("greet Alice").await.unwrap();
2690            assert_eq!(result.stdout, "Hello, Alice!\n");
2691
2692            let result = bash.exec("greet Bob Charlie").await.unwrap();
2693            assert_eq!(result.stdout, "Hello, Bob!\n");
2694        }
2695
2696        /// A custom builtin that reads from stdin
2697        struct Upper;
2698
2699        #[async_trait]
2700        impl Builtin for Upper {
2701            async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2702                let input = ctx.stdin.unwrap_or("");
2703                Ok(ExecResult::ok(input.to_uppercase()))
2704            }
2705        }
2706
2707        #[tokio::test]
2708        async fn test_custom_builtin_with_stdin() {
2709            let mut bash = Bash::builder().builtin("upper", Box::new(Upper)).build();
2710
2711            let result = bash.exec("echo hello | upper").await.unwrap();
2712            assert_eq!(result.stdout, "HELLO\n");
2713        }
2714
2715        /// A custom builtin that interacts with the filesystem
2716        struct WriteFile;
2717
2718        #[async_trait]
2719        impl Builtin for WriteFile {
2720            async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2721                if ctx.args.len() < 2 {
2722                    return Ok(ExecResult::err(
2723                        "Usage: writefile <path> <content>\n".to_string(),
2724                        1,
2725                    ));
2726                }
2727                let path = std::path::Path::new(&ctx.args[0]);
2728                let content = ctx.args[1..].join(" ");
2729                ctx.fs.write_file(path, content.as_bytes()).await?;
2730                Ok(ExecResult::ok(String::new()))
2731            }
2732        }
2733
2734        #[tokio::test]
2735        async fn test_custom_builtin_with_filesystem() {
2736            let mut bash = Bash::builder()
2737                .builtin("writefile", Box::new(WriteFile))
2738                .build();
2739
2740            bash.exec("writefile /tmp/test.txt custom content here")
2741                .await
2742                .unwrap();
2743
2744            let result = bash.exec("cat /tmp/test.txt").await.unwrap();
2745            assert_eq!(result.stdout, "custom content here");
2746        }
2747
2748        /// A custom builtin that overrides a default builtin
2749        struct CustomEcho;
2750
2751        #[async_trait]
2752        impl Builtin for CustomEcho {
2753            async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2754                let msg = ctx.args.join(" ");
2755                Ok(ExecResult::ok(format!("[CUSTOM] {}\n", msg)))
2756            }
2757        }
2758
2759        #[tokio::test]
2760        async fn test_custom_builtin_override_default() {
2761            let mut bash = Bash::builder()
2762                .builtin("echo", Box::new(CustomEcho))
2763                .build();
2764
2765            let result = bash.exec("echo hello world").await.unwrap();
2766            assert_eq!(result.stdout, "[CUSTOM] hello world\n");
2767        }
2768
2769        /// Test multiple custom builtins
2770        #[tokio::test]
2771        async fn test_multiple_custom_builtins() {
2772            let mut bash = Bash::builder()
2773                .builtin("hello", Box::new(Hello))
2774                .builtin("greet", Box::new(Greet))
2775                .builtin("upper", Box::new(Upper))
2776                .build();
2777
2778            let result = bash.exec("hello").await.unwrap();
2779            assert_eq!(result.stdout, "Hello from custom builtin!\n");
2780
2781            let result = bash.exec("greet Test").await.unwrap();
2782            assert_eq!(result.stdout, "Hello, Test!\n");
2783
2784            let result = bash.exec("echo foo | upper").await.unwrap();
2785            assert_eq!(result.stdout, "FOO\n");
2786        }
2787
2788        /// A custom builtin with internal state
2789        struct Counter {
2790            prefix: String,
2791        }
2792
2793        #[async_trait]
2794        impl Builtin for Counter {
2795            async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2796                let count = ctx
2797                    .args
2798                    .first()
2799                    .and_then(|s| s.parse::<i32>().ok())
2800                    .unwrap_or(1);
2801                let mut output = String::new();
2802                for i in 1..=count {
2803                    output.push_str(&format!("{}{}\n", self.prefix, i));
2804                }
2805                Ok(ExecResult::ok(output))
2806            }
2807        }
2808
2809        #[tokio::test]
2810        async fn test_custom_builtin_with_state() {
2811            let mut bash = Bash::builder()
2812                .builtin(
2813                    "count",
2814                    Box::new(Counter {
2815                        prefix: "Item ".to_string(),
2816                    }),
2817                )
2818                .build();
2819
2820            let result = bash.exec("count 3").await.unwrap();
2821            assert_eq!(result.stdout, "Item 1\nItem 2\nItem 3\n");
2822        }
2823
2824        /// A custom builtin that returns an error
2825        struct Fail;
2826
2827        #[async_trait]
2828        impl Builtin for Fail {
2829            async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2830                let code = ctx
2831                    .args
2832                    .first()
2833                    .and_then(|s| s.parse::<i32>().ok())
2834                    .unwrap_or(1);
2835                Ok(ExecResult::err(
2836                    format!("Failed with code {}\n", code),
2837                    code,
2838                ))
2839            }
2840        }
2841
2842        #[tokio::test]
2843        async fn test_custom_builtin_error() {
2844            let mut bash = Bash::builder().builtin("fail", Box::new(Fail)).build();
2845
2846            let result = bash.exec("fail 42").await.unwrap();
2847            assert_eq!(result.exit_code, 42);
2848            assert_eq!(result.stderr, "Failed with code 42\n");
2849        }
2850
2851        #[tokio::test]
2852        async fn test_custom_builtin_in_script() {
2853            let mut bash = Bash::builder().builtin("greet", Box::new(Greet)).build();
2854
2855            let script = r#"
2856                for name in Alice Bob Charlie; do
2857                    greet $name
2858                done
2859            "#;
2860
2861            let result = bash.exec(script).await.unwrap();
2862            assert_eq!(
2863                result.stdout,
2864                "Hello, Alice!\nHello, Bob!\nHello, Charlie!\n"
2865            );
2866        }
2867
2868        #[tokio::test]
2869        async fn test_custom_builtin_with_conditionals() {
2870            let mut bash = Bash::builder()
2871                .builtin("fail", Box::new(Fail))
2872                .builtin("hello", Box::new(Hello))
2873                .build();
2874
2875            let result = bash.exec("fail 1 || hello").await.unwrap();
2876            assert_eq!(result.stdout, "Hello from custom builtin!\n");
2877            assert_eq!(result.exit_code, 0);
2878
2879            let result = bash.exec("hello && fail 5").await.unwrap();
2880            assert_eq!(result.exit_code, 5);
2881        }
2882
2883        /// A custom builtin that reads environment variables
2884        struct EnvReader;
2885
2886        #[async_trait]
2887        impl Builtin for EnvReader {
2888            async fn execute(&self, ctx: Context<'_>) -> crate::Result<ExecResult> {
2889                let var_name = ctx.args.first().map(|s| s.as_str()).unwrap_or("HOME");
2890                let value = ctx
2891                    .env
2892                    .get(var_name)
2893                    .map(|s| s.as_str())
2894                    .unwrap_or("(not set)");
2895                Ok(ExecResult::ok(format!("{}={}\n", var_name, value)))
2896            }
2897        }
2898
2899        #[tokio::test]
2900        async fn test_custom_builtin_reads_env() {
2901            let mut bash = Bash::builder()
2902                .env("MY_VAR", "my_value")
2903                .builtin("readenv", Box::new(EnvReader))
2904                .build();
2905
2906            let result = bash.exec("readenv MY_VAR").await.unwrap();
2907            assert_eq!(result.stdout, "MY_VAR=my_value\n");
2908
2909            let result = bash.exec("readenv UNKNOWN").await.unwrap();
2910            assert_eq!(result.stdout, "UNKNOWN=(not set)\n");
2911        }
2912    }
2913
2914    // Parser timeout tests
2915
2916    #[tokio::test]
2917    async fn test_parser_timeout_default() {
2918        // Default parser timeout should be 5 seconds
2919        let limits = ExecutionLimits::default();
2920        assert_eq!(limits.parser_timeout, std::time::Duration::from_secs(5));
2921    }
2922
2923    #[tokio::test]
2924    async fn test_parser_timeout_custom() {
2925        // Parser timeout can be customized
2926        let limits = ExecutionLimits::new().parser_timeout(std::time::Duration::from_millis(100));
2927        assert_eq!(limits.parser_timeout, std::time::Duration::from_millis(100));
2928    }
2929
2930    #[tokio::test]
2931    async fn test_parser_timeout_normal_script() {
2932        // Normal scripts should complete well within timeout
2933        let limits = ExecutionLimits::new().parser_timeout(std::time::Duration::from_secs(1));
2934        let mut bash = Bash::builder().limits(limits).build();
2935        let result = bash.exec("echo hello").await.unwrap();
2936        assert_eq!(result.stdout, "hello\n");
2937    }
2938
2939    // Parser fuel tests
2940
2941    #[tokio::test]
2942    async fn test_parser_fuel_default() {
2943        // Default parser fuel should be 100,000
2944        let limits = ExecutionLimits::default();
2945        assert_eq!(limits.max_parser_operations, 100_000);
2946    }
2947
2948    #[tokio::test]
2949    async fn test_parser_fuel_custom() {
2950        // Parser fuel can be customized
2951        let limits = ExecutionLimits::new().max_parser_operations(1000);
2952        assert_eq!(limits.max_parser_operations, 1000);
2953    }
2954
2955    #[tokio::test]
2956    async fn test_parser_fuel_normal_script() {
2957        // Normal scripts should parse within fuel limit
2958        let limits = ExecutionLimits::new().max_parser_operations(1000);
2959        let mut bash = Bash::builder().limits(limits).build();
2960        let result = bash.exec("echo hello").await.unwrap();
2961        assert_eq!(result.stdout, "hello\n");
2962    }
2963
2964    // Input size limit tests
2965
2966    #[tokio::test]
2967    async fn test_input_size_limit_default() {
2968        // Default input size limit should be 10MB
2969        let limits = ExecutionLimits::default();
2970        assert_eq!(limits.max_input_bytes, 10_000_000);
2971    }
2972
2973    #[tokio::test]
2974    async fn test_input_size_limit_custom() {
2975        // Input size limit can be customized
2976        let limits = ExecutionLimits::new().max_input_bytes(1000);
2977        assert_eq!(limits.max_input_bytes, 1000);
2978    }
2979
2980    #[tokio::test]
2981    async fn test_input_size_limit_enforced() {
2982        // Scripts exceeding the limit should be rejected
2983        let limits = ExecutionLimits::new().max_input_bytes(10);
2984        let mut bash = Bash::builder().limits(limits).build();
2985
2986        // This script is longer than 10 bytes
2987        let result = bash.exec("echo hello world").await;
2988        assert!(result.is_err());
2989        let err = result.unwrap_err();
2990        assert!(
2991            err.to_string().contains("input too large"),
2992            "Expected input size error, got: {}",
2993            err
2994        );
2995    }
2996
2997    #[tokio::test]
2998    async fn test_input_size_limit_normal_script() {
2999        // Normal scripts should complete within limit
3000        let limits = ExecutionLimits::new().max_input_bytes(1000);
3001        let mut bash = Bash::builder().limits(limits).build();
3002        let result = bash.exec("echo hello").await.unwrap();
3003        assert_eq!(result.stdout, "hello\n");
3004    }
3005
3006    // AST depth limit tests
3007
3008    #[tokio::test]
3009    async fn test_ast_depth_limit_default() {
3010        // Default AST depth limit should be 100
3011        let limits = ExecutionLimits::default();
3012        assert_eq!(limits.max_ast_depth, 100);
3013    }
3014
3015    #[tokio::test]
3016    async fn test_ast_depth_limit_custom() {
3017        // AST depth limit can be customized
3018        let limits = ExecutionLimits::new().max_ast_depth(10);
3019        assert_eq!(limits.max_ast_depth, 10);
3020    }
3021
3022    #[tokio::test]
3023    async fn test_ast_depth_limit_normal_script() {
3024        // Normal scripts should parse within limit
3025        let limits = ExecutionLimits::new().max_ast_depth(10);
3026        let mut bash = Bash::builder().limits(limits).build();
3027        let result = bash.exec("if true; then echo ok; fi").await.unwrap();
3028        assert_eq!(result.stdout, "ok\n");
3029    }
3030
3031    #[tokio::test]
3032    async fn test_ast_depth_limit_enforced() {
3033        // Deeply nested scripts should be rejected
3034        let limits = ExecutionLimits::new().max_ast_depth(2);
3035        let mut bash = Bash::builder().limits(limits).build();
3036
3037        // This script has 3 levels of nesting (exceeds limit of 2)
3038        let result = bash
3039            .exec("if true; then if true; then if true; then echo nested; fi; fi; fi")
3040            .await;
3041        assert!(result.is_err());
3042        let err = result.unwrap_err();
3043        assert!(
3044            err.to_string().contains("AST nesting too deep"),
3045            "Expected AST depth error, got: {}",
3046            err
3047        );
3048    }
3049
3050    #[tokio::test]
3051    async fn test_parser_fuel_enforced() {
3052        // Scripts exceeding fuel limit should be rejected
3053        // With fuel of 3, parsing "echo a" should fail (needs multiple operations)
3054        let limits = ExecutionLimits::new().max_parser_operations(3);
3055        let mut bash = Bash::builder().limits(limits).build();
3056
3057        // Even a simple script needs more than 3 parsing operations
3058        let result = bash.exec("echo a; echo b; echo c").await;
3059        assert!(result.is_err());
3060        let err = result.unwrap_err();
3061        assert!(
3062            err.to_string().contains("parser fuel exhausted"),
3063            "Expected parser fuel error, got: {}",
3064            err
3065        );
3066    }
3067
3068    // set -e (errexit) tests
3069
3070    #[tokio::test]
3071    async fn test_set_e_basic() {
3072        // set -e should exit on non-zero return
3073        let mut bash = Bash::new();
3074        let result = bash
3075            .exec("set -e; true; false; echo should_not_reach")
3076            .await
3077            .unwrap();
3078        assert_eq!(result.stdout, "");
3079        assert_eq!(result.exit_code, 1);
3080    }
3081
3082    #[tokio::test]
3083    async fn test_set_e_after_failing_cmd() {
3084        // set -e exits immediately on failed command
3085        let mut bash = Bash::new();
3086        let result = bash
3087            .exec("set -e; echo before; false; echo after")
3088            .await
3089            .unwrap();
3090        assert_eq!(result.stdout, "before\n");
3091        assert_eq!(result.exit_code, 1);
3092    }
3093
3094    #[tokio::test]
3095    async fn test_set_e_disabled() {
3096        // set +e disables errexit
3097        let mut bash = Bash::new();
3098        let result = bash
3099            .exec("set -e; set +e; false; echo still_running")
3100            .await
3101            .unwrap();
3102        assert_eq!(result.stdout, "still_running\n");
3103    }
3104
3105    #[tokio::test]
3106    async fn test_set_e_in_pipeline_last() {
3107        // set -e only checks last command in pipeline
3108        let mut bash = Bash::new();
3109        let result = bash
3110            .exec("set -e; false | true; echo reached")
3111            .await
3112            .unwrap();
3113        assert_eq!(result.stdout, "reached\n");
3114    }
3115
3116    #[tokio::test]
3117    async fn test_set_e_in_if_condition() {
3118        // set -e should not trigger on if condition failure
3119        let mut bash = Bash::new();
3120        let result = bash
3121            .exec("set -e; if false; then echo yes; else echo no; fi; echo done")
3122            .await
3123            .unwrap();
3124        assert_eq!(result.stdout, "no\ndone\n");
3125    }
3126
3127    #[tokio::test]
3128    async fn test_set_e_in_while_condition() {
3129        // set -e should not trigger on while condition failure
3130        let mut bash = Bash::new();
3131        let result = bash
3132            .exec("set -e; x=0; while [ \"$x\" -lt 2 ]; do echo \"x=$x\"; x=$((x + 1)); done; echo done")
3133            .await
3134            .unwrap();
3135        assert_eq!(result.stdout, "x=0\nx=1\ndone\n");
3136    }
3137
3138    #[tokio::test]
3139    async fn test_set_e_in_brace_group() {
3140        // set -e should work inside brace groups
3141        let mut bash = Bash::new();
3142        let result = bash
3143            .exec("set -e; { echo start; false; echo unreached; }; echo after")
3144            .await
3145            .unwrap();
3146        assert_eq!(result.stdout, "start\n");
3147        assert_eq!(result.exit_code, 1);
3148    }
3149
3150    #[tokio::test]
3151    async fn test_set_e_and_chain() {
3152        // set -e should not trigger on && chain (false && ... is expected to not run second)
3153        let mut bash = Bash::new();
3154        let result = bash
3155            .exec("set -e; false && echo one; echo reached")
3156            .await
3157            .unwrap();
3158        assert_eq!(result.stdout, "reached\n");
3159    }
3160
3161    #[tokio::test]
3162    async fn test_set_e_or_chain() {
3163        // set -e should not trigger on || chain (true || false is expected to short circuit)
3164        let mut bash = Bash::new();
3165        let result = bash
3166            .exec("set -e; true || false; echo reached")
3167            .await
3168            .unwrap();
3169        assert_eq!(result.stdout, "reached\n");
3170    }
3171
3172    // Tilde expansion tests
3173
3174    #[tokio::test]
3175    async fn test_tilde_expansion_basic() {
3176        // ~ should expand to $HOME
3177        let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3178        let result = bash.exec("echo ~").await.unwrap();
3179        assert_eq!(result.stdout, "/home/testuser\n");
3180    }
3181
3182    #[tokio::test]
3183    async fn test_tilde_expansion_with_path() {
3184        // ~/path should expand to $HOME/path
3185        let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3186        let result = bash.exec("echo ~/documents/file.txt").await.unwrap();
3187        assert_eq!(result.stdout, "/home/testuser/documents/file.txt\n");
3188    }
3189
3190    #[tokio::test]
3191    async fn test_tilde_expansion_in_assignment() {
3192        // Tilde expansion should work in variable assignments
3193        let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3194        let result = bash.exec("DIR=~/data; echo $DIR").await.unwrap();
3195        assert_eq!(result.stdout, "/home/testuser/data\n");
3196    }
3197
3198    #[tokio::test]
3199    async fn test_tilde_expansion_default_home() {
3200        // ~ should default to /home/sandbox (DEFAULT_USERNAME is "sandbox")
3201        let mut bash = Bash::new();
3202        let result = bash.exec("echo ~").await.unwrap();
3203        assert_eq!(result.stdout, "/home/sandbox\n");
3204    }
3205
3206    #[tokio::test]
3207    async fn test_tilde_not_at_start() {
3208        // ~ not at start of word should not expand
3209        let mut bash = Bash::builder().env("HOME", "/home/testuser").build();
3210        let result = bash.exec("echo foo~bar").await.unwrap();
3211        assert_eq!(result.stdout, "foo~bar\n");
3212    }
3213
3214    // Special variables tests
3215
3216    #[tokio::test]
3217    async fn test_special_var_dollar_dollar() {
3218        // $$ - current process ID
3219        let mut bash = Bash::new();
3220        let result = bash.exec("echo $$").await.unwrap();
3221        // Should be a numeric value
3222        let pid: u32 = result.stdout.trim().parse().expect("$$ should be a number");
3223        assert!(pid > 0, "$$ should be a positive number");
3224    }
3225
3226    #[tokio::test]
3227    async fn test_special_var_random() {
3228        // $RANDOM - random number between 0 and 32767
3229        let mut bash = Bash::new();
3230        let result = bash.exec("echo $RANDOM").await.unwrap();
3231        let random: u32 = result
3232            .stdout
3233            .trim()
3234            .parse()
3235            .expect("$RANDOM should be a number");
3236        assert!(random < 32768, "$RANDOM should be < 32768");
3237    }
3238
3239    #[tokio::test]
3240    async fn test_special_var_random_varies() {
3241        // $RANDOM should return different values on different calls
3242        let mut bash = Bash::new();
3243        let result1 = bash.exec("echo $RANDOM").await.unwrap();
3244        let result2 = bash.exec("echo $RANDOM").await.unwrap();
3245        // With high probability, they should be different
3246        // (small chance they're the same, so this test may rarely fail)
3247        // We'll just check they're both valid numbers
3248        let _: u32 = result1
3249            .stdout
3250            .trim()
3251            .parse()
3252            .expect("$RANDOM should be a number");
3253        let _: u32 = result2
3254            .stdout
3255            .trim()
3256            .parse()
3257            .expect("$RANDOM should be a number");
3258    }
3259
3260    #[tokio::test]
3261    async fn test_special_var_lineno() {
3262        // $LINENO - current line number
3263        let mut bash = Bash::new();
3264        let result = bash.exec("echo $LINENO").await.unwrap();
3265        assert_eq!(result.stdout, "1\n");
3266    }
3267
3268    #[tokio::test]
3269    async fn test_lineno_multiline() {
3270        // $LINENO tracks line numbers across multiple lines
3271        let mut bash = Bash::new();
3272        let result = bash
3273            .exec(
3274                r#"echo "line $LINENO"
3275echo "line $LINENO"
3276echo "line $LINENO""#,
3277            )
3278            .await
3279            .unwrap();
3280        assert_eq!(result.stdout, "line 1\nline 2\nline 3\n");
3281    }
3282
3283    #[tokio::test]
3284    async fn test_lineno_in_loop() {
3285        // $LINENO inside a for loop
3286        let mut bash = Bash::new();
3287        let result = bash
3288            .exec(
3289                r#"for i in 1 2; do
3290  echo "loop $LINENO"
3291done"#,
3292            )
3293            .await
3294            .unwrap();
3295        // Loop body is on line 2
3296        assert_eq!(result.stdout, "loop 2\nloop 2\n");
3297    }
3298
3299    // File test operator tests
3300
3301    #[tokio::test]
3302    async fn test_file_test_r_readable() {
3303        // -r file: true if file exists (readable in virtual fs)
3304        let mut bash = Bash::new();
3305        bash.exec("echo hello > /tmp/readable.txt").await.unwrap();
3306        let result = bash
3307            .exec("test -r /tmp/readable.txt && echo yes")
3308            .await
3309            .unwrap();
3310        assert_eq!(result.stdout, "yes\n");
3311    }
3312
3313    #[tokio::test]
3314    async fn test_file_test_r_not_exists() {
3315        // -r file: false if file doesn't exist
3316        let mut bash = Bash::new();
3317        let result = bash
3318            .exec("test -r /tmp/nonexistent.txt && echo yes || echo no")
3319            .await
3320            .unwrap();
3321        assert_eq!(result.stdout, "no\n");
3322    }
3323
3324    #[tokio::test]
3325    async fn test_file_test_w_writable() {
3326        // -w file: true if file exists (writable in virtual fs)
3327        let mut bash = Bash::new();
3328        bash.exec("echo hello > /tmp/writable.txt").await.unwrap();
3329        let result = bash
3330            .exec("test -w /tmp/writable.txt && echo yes")
3331            .await
3332            .unwrap();
3333        assert_eq!(result.stdout, "yes\n");
3334    }
3335
3336    #[tokio::test]
3337    async fn test_file_test_x_executable() {
3338        // -x file: true if file exists and has execute permission
3339        let mut bash = Bash::new();
3340        bash.exec("echo '#!/bin/bash' > /tmp/script.sh")
3341            .await
3342            .unwrap();
3343        bash.exec("chmod 755 /tmp/script.sh").await.unwrap();
3344        let result = bash
3345            .exec("test -x /tmp/script.sh && echo yes")
3346            .await
3347            .unwrap();
3348        assert_eq!(result.stdout, "yes\n");
3349    }
3350
3351    #[tokio::test]
3352    async fn test_file_test_x_not_executable() {
3353        // -x file: false if file has no execute permission
3354        let mut bash = Bash::new();
3355        bash.exec("echo 'data' > /tmp/noexec.txt").await.unwrap();
3356        bash.exec("chmod 644 /tmp/noexec.txt").await.unwrap();
3357        let result = bash
3358            .exec("test -x /tmp/noexec.txt && echo yes || echo no")
3359            .await
3360            .unwrap();
3361        assert_eq!(result.stdout, "no\n");
3362    }
3363
3364    #[tokio::test]
3365    async fn test_file_test_e_exists() {
3366        // -e file: true if file exists
3367        let mut bash = Bash::new();
3368        bash.exec("echo hello > /tmp/exists.txt").await.unwrap();
3369        let result = bash
3370            .exec("test -e /tmp/exists.txt && echo yes")
3371            .await
3372            .unwrap();
3373        assert_eq!(result.stdout, "yes\n");
3374    }
3375
3376    #[tokio::test]
3377    async fn test_file_test_f_regular() {
3378        // -f file: true if regular file
3379        let mut bash = Bash::new();
3380        bash.exec("echo hello > /tmp/regular.txt").await.unwrap();
3381        let result = bash
3382            .exec("test -f /tmp/regular.txt && echo yes")
3383            .await
3384            .unwrap();
3385        assert_eq!(result.stdout, "yes\n");
3386    }
3387
3388    #[tokio::test]
3389    async fn test_file_test_d_directory() {
3390        // -d file: true if directory
3391        let mut bash = Bash::new();
3392        bash.exec("mkdir -p /tmp/mydir").await.unwrap();
3393        let result = bash.exec("test -d /tmp/mydir && echo yes").await.unwrap();
3394        assert_eq!(result.stdout, "yes\n");
3395    }
3396
3397    #[tokio::test]
3398    async fn test_file_test_s_size() {
3399        // -s file: true if file has size > 0
3400        let mut bash = Bash::new();
3401        bash.exec("echo hello > /tmp/nonempty.txt").await.unwrap();
3402        let result = bash
3403            .exec("test -s /tmp/nonempty.txt && echo yes")
3404            .await
3405            .unwrap();
3406        assert_eq!(result.stdout, "yes\n");
3407    }
3408
3409    // ============================================================
3410    // Stderr Redirection Tests
3411    // ============================================================
3412
3413    #[tokio::test]
3414    async fn test_redirect_both_stdout_stderr() {
3415        // &> redirects both stdout and stderr to file
3416        let mut bash = Bash::new();
3417        // echo outputs to stdout, we use &> to redirect both to file
3418        let result = bash.exec("echo hello &> /tmp/out.txt").await.unwrap();
3419        // stdout should be empty (redirected to file)
3420        assert_eq!(result.stdout, "");
3421        // Verify file contents
3422        let check = bash.exec("cat /tmp/out.txt").await.unwrap();
3423        assert_eq!(check.stdout, "hello\n");
3424    }
3425
3426    #[tokio::test]
3427    async fn test_stderr_redirect_to_file() {
3428        // 2> redirects stderr to file
3429        // We need a command that outputs to stderr - let's use a command that fails
3430        // Or use a subshell with explicit stderr output
3431        let mut bash = Bash::new();
3432        // Create a test script that outputs to both stdout and stderr
3433        bash.exec("echo stdout; echo stderr 2> /tmp/err.txt")
3434            .await
3435            .unwrap();
3436        // Note: echo stderr doesn't actually output to stderr, it outputs to stdout
3437        // We need to test with actual stderr output
3438    }
3439
3440    #[tokio::test]
3441    async fn test_fd_redirect_parsing() {
3442        // Test that 2> is parsed correctly
3443        let mut bash = Bash::new();
3444        // Just test the parsing doesn't error
3445        let result = bash.exec("true 2> /tmp/err.txt").await.unwrap();
3446        assert_eq!(result.exit_code, 0);
3447    }
3448
3449    #[tokio::test]
3450    async fn test_fd_redirect_append_parsing() {
3451        // Test that 2>> is parsed correctly
3452        let mut bash = Bash::new();
3453        let result = bash.exec("true 2>> /tmp/err.txt").await.unwrap();
3454        assert_eq!(result.exit_code, 0);
3455    }
3456
3457    #[tokio::test]
3458    async fn test_fd_dup_parsing() {
3459        // Test that 2>&1 is parsed correctly
3460        let mut bash = Bash::new();
3461        let result = bash.exec("echo hello 2>&1").await.unwrap();
3462        assert_eq!(result.stdout, "hello\n");
3463        assert_eq!(result.exit_code, 0);
3464    }
3465
3466    #[tokio::test]
3467    async fn test_dup_output_redirect_stdout_to_stderr() {
3468        // >&2 redirects stdout to stderr
3469        let mut bash = Bash::new();
3470        let result = bash.exec("echo hello >&2").await.unwrap();
3471        // stdout should be moved to stderr
3472        assert_eq!(result.stdout, "");
3473        assert_eq!(result.stderr, "hello\n");
3474    }
3475
3476    #[tokio::test]
3477    async fn test_lexer_redirect_both() {
3478        // Test that &> is lexed as a single token, not & followed by >
3479        let mut bash = Bash::new();
3480        // Without proper lexing, this would be parsed as background + redirect
3481        let result = bash.exec("echo test &> /tmp/both.txt").await.unwrap();
3482        assert_eq!(result.stdout, "");
3483        let check = bash.exec("cat /tmp/both.txt").await.unwrap();
3484        assert_eq!(check.stdout, "test\n");
3485    }
3486
3487    #[tokio::test]
3488    async fn test_lexer_dup_output() {
3489        // Test that >& is lexed correctly
3490        let mut bash = Bash::new();
3491        let result = bash.exec("echo test >&2").await.unwrap();
3492        assert_eq!(result.stdout, "");
3493        assert_eq!(result.stderr, "test\n");
3494    }
3495
3496    #[tokio::test]
3497    async fn test_digit_before_redirect() {
3498        // Test that 2> works with digits
3499        let mut bash = Bash::new();
3500        // 2> should be recognized as stderr redirect
3501        let result = bash.exec("echo hello 2> /tmp/err.txt").await.unwrap();
3502        assert_eq!(result.exit_code, 0);
3503        // stdout should still have the output since echo doesn't write to stderr
3504        assert_eq!(result.stdout, "hello\n");
3505    }
3506
3507    // ============================================================
3508    // Arithmetic Logical Operator Tests
3509    // ============================================================
3510
3511    #[tokio::test]
3512    async fn test_arithmetic_logical_and_true() {
3513        // Both sides true
3514        let mut bash = Bash::new();
3515        let result = bash.exec("echo $((1 && 1))").await.unwrap();
3516        assert_eq!(result.stdout, "1\n");
3517    }
3518
3519    #[tokio::test]
3520    async fn test_arithmetic_logical_and_false_left() {
3521        // Left side false - short circuits
3522        let mut bash = Bash::new();
3523        let result = bash.exec("echo $((0 && 1))").await.unwrap();
3524        assert_eq!(result.stdout, "0\n");
3525    }
3526
3527    #[tokio::test]
3528    async fn test_arithmetic_logical_and_false_right() {
3529        // Right side false
3530        let mut bash = Bash::new();
3531        let result = bash.exec("echo $((1 && 0))").await.unwrap();
3532        assert_eq!(result.stdout, "0\n");
3533    }
3534
3535    #[tokio::test]
3536    async fn test_arithmetic_logical_or_false() {
3537        // Both sides false
3538        let mut bash = Bash::new();
3539        let result = bash.exec("echo $((0 || 0))").await.unwrap();
3540        assert_eq!(result.stdout, "0\n");
3541    }
3542
3543    #[tokio::test]
3544    async fn test_arithmetic_logical_or_true_left() {
3545        // Left side true - short circuits
3546        let mut bash = Bash::new();
3547        let result = bash.exec("echo $((1 || 0))").await.unwrap();
3548        assert_eq!(result.stdout, "1\n");
3549    }
3550
3551    #[tokio::test]
3552    async fn test_arithmetic_logical_or_true_right() {
3553        // Right side true
3554        let mut bash = Bash::new();
3555        let result = bash.exec("echo $((0 || 1))").await.unwrap();
3556        assert_eq!(result.stdout, "1\n");
3557    }
3558
3559    #[tokio::test]
3560    async fn test_arithmetic_logical_combined() {
3561        // Combined && and || with expressions
3562        let mut bash = Bash::new();
3563        // (5 > 3) && (2 < 4) => 1 && 1 => 1
3564        let result = bash.exec("echo $((5 > 3 && 2 < 4))").await.unwrap();
3565        assert_eq!(result.stdout, "1\n");
3566    }
3567
3568    #[tokio::test]
3569    async fn test_arithmetic_logical_with_comparison() {
3570        // || with comparison
3571        let mut bash = Bash::new();
3572        // (5 < 3) || (2 < 4) => 0 || 1 => 1
3573        let result = bash.exec("echo $((5 < 3 || 2 < 4))").await.unwrap();
3574        assert_eq!(result.stdout, "1\n");
3575    }
3576
3577    // ============================================================
3578    // Brace Expansion Tests
3579    // ============================================================
3580
3581    #[tokio::test]
3582    async fn test_brace_expansion_list() {
3583        // {a,b,c} expands to a b c
3584        let mut bash = Bash::new();
3585        let result = bash.exec("echo {a,b,c}").await.unwrap();
3586        assert_eq!(result.stdout, "a b c\n");
3587    }
3588
3589    #[tokio::test]
3590    async fn test_brace_expansion_with_prefix() {
3591        // file{1,2,3}.txt expands to file1.txt file2.txt file3.txt
3592        let mut bash = Bash::new();
3593        let result = bash.exec("echo file{1,2,3}.txt").await.unwrap();
3594        assert_eq!(result.stdout, "file1.txt file2.txt file3.txt\n");
3595    }
3596
3597    #[tokio::test]
3598    async fn test_brace_expansion_numeric_range() {
3599        // {1..5} expands to 1 2 3 4 5
3600        let mut bash = Bash::new();
3601        let result = bash.exec("echo {1..5}").await.unwrap();
3602        assert_eq!(result.stdout, "1 2 3 4 5\n");
3603    }
3604
3605    #[tokio::test]
3606    async fn test_brace_expansion_char_range() {
3607        // {a..e} expands to a b c d e
3608        let mut bash = Bash::new();
3609        let result = bash.exec("echo {a..e}").await.unwrap();
3610        assert_eq!(result.stdout, "a b c d e\n");
3611    }
3612
3613    #[tokio::test]
3614    async fn test_brace_expansion_reverse_range() {
3615        // {5..1} expands to 5 4 3 2 1
3616        let mut bash = Bash::new();
3617        let result = bash.exec("echo {5..1}").await.unwrap();
3618        assert_eq!(result.stdout, "5 4 3 2 1\n");
3619    }
3620
3621    #[tokio::test]
3622    async fn test_brace_expansion_nested() {
3623        // Nested brace expansion: {a,b}{1,2}
3624        let mut bash = Bash::new();
3625        let result = bash.exec("echo {a,b}{1,2}").await.unwrap();
3626        assert_eq!(result.stdout, "a1 a2 b1 b2\n");
3627    }
3628
3629    #[tokio::test]
3630    async fn test_brace_expansion_with_suffix() {
3631        // Prefix and suffix: pre{x,y}suf
3632        let mut bash = Bash::new();
3633        let result = bash.exec("echo pre{x,y}suf").await.unwrap();
3634        assert_eq!(result.stdout, "prexsuf preysuf\n");
3635    }
3636
3637    #[tokio::test]
3638    async fn test_brace_expansion_empty_item() {
3639        // {,foo} expands to (empty) foo
3640        let mut bash = Bash::new();
3641        let result = bash.exec("echo x{,y}z").await.unwrap();
3642        assert_eq!(result.stdout, "xz xyz\n");
3643    }
3644
3645    // ============================================================
3646    // String Comparison Tests
3647    // ============================================================
3648
3649    #[tokio::test]
3650    async fn test_string_less_than() {
3651        let mut bash = Bash::new();
3652        let result = bash
3653            .exec("test apple '<' banana && echo yes")
3654            .await
3655            .unwrap();
3656        assert_eq!(result.stdout, "yes\n");
3657    }
3658
3659    #[tokio::test]
3660    async fn test_string_greater_than() {
3661        let mut bash = Bash::new();
3662        let result = bash
3663            .exec("test banana '>' apple && echo yes")
3664            .await
3665            .unwrap();
3666        assert_eq!(result.stdout, "yes\n");
3667    }
3668
3669    #[tokio::test]
3670    async fn test_string_less_than_false() {
3671        let mut bash = Bash::new();
3672        let result = bash
3673            .exec("test banana '<' apple && echo yes || echo no")
3674            .await
3675            .unwrap();
3676        assert_eq!(result.stdout, "no\n");
3677    }
3678
3679    // ============================================================
3680    // Array Indices Tests
3681    // ============================================================
3682
3683    #[tokio::test]
3684    async fn test_array_indices_basic() {
3685        // ${!arr[@]} returns the indices of the array
3686        let mut bash = Bash::new();
3687        let result = bash.exec("arr=(a b c); echo ${!arr[@]}").await.unwrap();
3688        assert_eq!(result.stdout, "0 1 2\n");
3689    }
3690
3691    #[tokio::test]
3692    async fn test_array_indices_sparse() {
3693        // ${!arr[@]} should show indices even for sparse arrays
3694        let mut bash = Bash::new();
3695        let result = bash
3696            .exec("arr[0]=a; arr[5]=b; arr[10]=c; echo ${!arr[@]}")
3697            .await
3698            .unwrap();
3699        assert_eq!(result.stdout, "0 5 10\n");
3700    }
3701
3702    #[tokio::test]
3703    async fn test_array_indices_star() {
3704        // ${!arr[*]} should also work
3705        let mut bash = Bash::new();
3706        let result = bash.exec("arr=(x y z); echo ${!arr[*]}").await.unwrap();
3707        assert_eq!(result.stdout, "0 1 2\n");
3708    }
3709
3710    #[tokio::test]
3711    async fn test_array_indices_empty() {
3712        // Empty array should return empty string
3713        let mut bash = Bash::new();
3714        let result = bash.exec("arr=(); echo \"${!arr[@]}\"").await.unwrap();
3715        assert_eq!(result.stdout, "\n");
3716    }
3717
3718    // ============================================================
3719    // Text file builder methods
3720    // ============================================================
3721
3722    #[tokio::test]
3723    async fn test_text_file_basic() {
3724        let mut bash = Bash::builder()
3725            .mount_text("/config/app.conf", "debug=true\nport=8080\n")
3726            .build();
3727
3728        let result = bash.exec("cat /config/app.conf").await.unwrap();
3729        assert_eq!(result.stdout, "debug=true\nport=8080\n");
3730    }
3731
3732    #[tokio::test]
3733    async fn test_text_file_multiple() {
3734        let mut bash = Bash::builder()
3735            .mount_text("/data/file1.txt", "content one")
3736            .mount_text("/data/file2.txt", "content two")
3737            .mount_text("/other/file3.txt", "content three")
3738            .build();
3739
3740        let result = bash.exec("cat /data/file1.txt").await.unwrap();
3741        assert_eq!(result.stdout, "content one");
3742
3743        let result = bash.exec("cat /data/file2.txt").await.unwrap();
3744        assert_eq!(result.stdout, "content two");
3745
3746        let result = bash.exec("cat /other/file3.txt").await.unwrap();
3747        assert_eq!(result.stdout, "content three");
3748    }
3749
3750    #[tokio::test]
3751    async fn test_text_file_nested_directory() {
3752        // Parent directories should be created automatically
3753        let mut bash = Bash::builder()
3754            .mount_text("/a/b/c/d/file.txt", "nested content")
3755            .build();
3756
3757        let result = bash.exec("cat /a/b/c/d/file.txt").await.unwrap();
3758        assert_eq!(result.stdout, "nested content");
3759    }
3760
3761    #[tokio::test]
3762    async fn test_text_file_mode() {
3763        let bash = Bash::builder()
3764            .mount_text("/tmp/writable.txt", "content")
3765            .build();
3766
3767        let stat = bash
3768            .fs()
3769            .stat(std::path::Path::new("/tmp/writable.txt"))
3770            .await
3771            .unwrap();
3772        assert_eq!(stat.mode, 0o644);
3773    }
3774
3775    #[tokio::test]
3776    async fn test_readonly_text_basic() {
3777        let mut bash = Bash::builder()
3778            .mount_readonly_text("/etc/version", "1.2.3")
3779            .build();
3780
3781        let result = bash.exec("cat /etc/version").await.unwrap();
3782        assert_eq!(result.stdout, "1.2.3");
3783    }
3784
3785    #[tokio::test]
3786    async fn test_readonly_text_mode() {
3787        let bash = Bash::builder()
3788            .mount_readonly_text("/etc/readonly.conf", "immutable")
3789            .build();
3790
3791        let stat = bash
3792            .fs()
3793            .stat(std::path::Path::new("/etc/readonly.conf"))
3794            .await
3795            .unwrap();
3796        assert_eq!(stat.mode, 0o444);
3797    }
3798
3799    #[tokio::test]
3800    async fn test_text_file_mixed_readonly_writable() {
3801        let bash = Bash::builder()
3802            .mount_text("/data/writable.txt", "can edit")
3803            .mount_readonly_text("/data/readonly.txt", "cannot edit")
3804            .build();
3805
3806        let writable_stat = bash
3807            .fs()
3808            .stat(std::path::Path::new("/data/writable.txt"))
3809            .await
3810            .unwrap();
3811        let readonly_stat = bash
3812            .fs()
3813            .stat(std::path::Path::new("/data/readonly.txt"))
3814            .await
3815            .unwrap();
3816
3817        assert_eq!(writable_stat.mode, 0o644);
3818        assert_eq!(readonly_stat.mode, 0o444);
3819    }
3820
3821    #[tokio::test]
3822    async fn test_text_file_with_env() {
3823        // text_file should work alongside other builder methods
3824        let mut bash = Bash::builder()
3825            .env("APP_NAME", "testapp")
3826            .mount_text("/config/app.conf", "name=${APP_NAME}")
3827            .build();
3828
3829        let result = bash.exec("echo $APP_NAME").await.unwrap();
3830        assert_eq!(result.stdout, "testapp\n");
3831
3832        let result = bash.exec("cat /config/app.conf").await.unwrap();
3833        assert_eq!(result.stdout, "name=${APP_NAME}");
3834    }
3835
3836    #[tokio::test]
3837    async fn test_text_file_json() {
3838        let mut bash = Bash::builder()
3839            .mount_text("/data/users.json", r#"["alice", "bob", "charlie"]"#)
3840            .build();
3841
3842        let result = bash.exec("cat /data/users.json | jq '.[0]'").await.unwrap();
3843        assert_eq!(result.stdout, "\"alice\"\n");
3844    }
3845
3846    #[tokio::test]
3847    async fn test_mount_with_custom_filesystem() {
3848        // Mount files work with custom filesystems via OverlayFs
3849        let custom_fs = std::sync::Arc::new(InMemoryFs::new());
3850
3851        // Pre-populate the base filesystem
3852        custom_fs
3853            .write_file(std::path::Path::new("/base.txt"), b"from base")
3854            .await
3855            .unwrap();
3856
3857        let mut bash = Bash::builder()
3858            .fs(custom_fs)
3859            .mount_text("/mounted.txt", "from mount")
3860            .mount_readonly_text("/readonly.txt", "immutable")
3861            .build();
3862
3863        // Can read base file
3864        let result = bash.exec("cat /base.txt").await.unwrap();
3865        assert_eq!(result.stdout, "from base");
3866
3867        // Can read mounted files
3868        let result = bash.exec("cat /mounted.txt").await.unwrap();
3869        assert_eq!(result.stdout, "from mount");
3870
3871        let result = bash.exec("cat /readonly.txt").await.unwrap();
3872        assert_eq!(result.stdout, "immutable");
3873
3874        // Mounted readonly file has correct permissions
3875        let stat = bash
3876            .fs()
3877            .stat(std::path::Path::new("/readonly.txt"))
3878            .await
3879            .unwrap();
3880        assert_eq!(stat.mode, 0o444);
3881    }
3882
3883    #[tokio::test]
3884    async fn test_mount_overwrites_base_file() {
3885        // Mounted files take precedence over base filesystem
3886        let custom_fs = std::sync::Arc::new(InMemoryFs::new());
3887        custom_fs
3888            .write_file(std::path::Path::new("/config.txt"), b"original")
3889            .await
3890            .unwrap();
3891
3892        let mut bash = Bash::builder()
3893            .fs(custom_fs)
3894            .mount_text("/config.txt", "overwritten")
3895            .build();
3896
3897        let result = bash.exec("cat /config.txt").await.unwrap();
3898        assert_eq!(result.stdout, "overwritten");
3899    }
3900
3901    // ============================================================
3902    // Parser Error Location Tests
3903    // ============================================================
3904
3905    #[tokio::test]
3906    async fn test_parse_error_includes_line_number() {
3907        // Parse errors should include line/column info
3908        let mut bash = Bash::new();
3909        let result = bash
3910            .exec(
3911                r#"echo ok
3912if true; then
3913echo missing fi"#,
3914            )
3915            .await;
3916        // Should fail to parse due to missing 'fi'
3917        assert!(result.is_err());
3918        let err = result.unwrap_err();
3919        let err_msg = format!("{}", err);
3920        // Error should mention line number
3921        assert!(
3922            err_msg.contains("line") || err_msg.contains("parse"),
3923            "Error should be a parse error: {}",
3924            err_msg
3925        );
3926    }
3927
3928    #[tokio::test]
3929    async fn test_parse_error_on_specific_line() {
3930        // Syntax error on line 3 should report line 3
3931        use crate::parser::Parser;
3932        let script = "echo line1\necho line2\nif true; then\n";
3933        let result = Parser::new(script).parse();
3934        assert!(result.is_err());
3935        let err = result.unwrap_err();
3936        let err_msg = format!("{}", err);
3937        // Error should mention the problem (either "expected" or "syntax error")
3938        assert!(
3939            err_msg.contains("expected") || err_msg.contains("syntax error"),
3940            "Error should be a parse error: {}",
3941            err_msg
3942        );
3943    }
3944
3945    // ==================== Root directory access tests ====================
3946
3947    #[tokio::test]
3948    async fn test_cd_to_root_and_ls() {
3949        // Test: cd / && ls should work
3950        let mut bash = Bash::new();
3951        let result = bash.exec("cd / && ls").await.unwrap();
3952        assert_eq!(
3953            result.exit_code, 0,
3954            "cd / && ls should succeed: {}",
3955            result.stderr
3956        );
3957        assert!(result.stdout.contains("tmp"), "Root should contain tmp");
3958        assert!(result.stdout.contains("home"), "Root should contain home");
3959    }
3960
3961    #[tokio::test]
3962    async fn test_cd_to_root_and_pwd() {
3963        // Test: cd / && pwd should show /
3964        let mut bash = Bash::new();
3965        let result = bash.exec("cd / && pwd").await.unwrap();
3966        assert_eq!(result.exit_code, 0, "cd / && pwd should succeed");
3967        assert_eq!(result.stdout.trim(), "/");
3968    }
3969
3970    #[tokio::test]
3971    async fn test_cd_to_root_and_ls_dot() {
3972        // Test: cd / && ls . should list root contents
3973        let mut bash = Bash::new();
3974        let result = bash.exec("cd / && ls .").await.unwrap();
3975        assert_eq!(
3976            result.exit_code, 0,
3977            "cd / && ls . should succeed: {}",
3978            result.stderr
3979        );
3980        assert!(result.stdout.contains("tmp"), "Root should contain tmp");
3981        assert!(result.stdout.contains("home"), "Root should contain home");
3982    }
3983
3984    #[tokio::test]
3985    async fn test_ls_root_directly() {
3986        // Test: ls / should work
3987        let mut bash = Bash::new();
3988        let result = bash.exec("ls /").await.unwrap();
3989        assert_eq!(
3990            result.exit_code, 0,
3991            "ls / should succeed: {}",
3992            result.stderr
3993        );
3994        assert!(result.stdout.contains("tmp"), "Root should contain tmp");
3995        assert!(result.stdout.contains("home"), "Root should contain home");
3996        assert!(result.stdout.contains("dev"), "Root should contain dev");
3997    }
3998
3999    #[tokio::test]
4000    async fn test_ls_root_long_format() {
4001        // Test: ls -la / should work
4002        let mut bash = Bash::new();
4003        let result = bash.exec("ls -la /").await.unwrap();
4004        assert_eq!(
4005            result.exit_code, 0,
4006            "ls -la / should succeed: {}",
4007            result.stderr
4008        );
4009        assert!(result.stdout.contains("tmp"), "Root should contain tmp");
4010        assert!(
4011            result.stdout.contains("drw"),
4012            "Should show directory permissions"
4013        );
4014    }
4015
4016    // === Issue 1: Heredoc file writes ===
4017
4018    #[tokio::test]
4019    async fn test_heredoc_redirect_to_file() {
4020        // cat > file <<'EOF' is the #1 way LLMs create multi-line files
4021        let mut bash = Bash::new();
4022        let result = bash
4023            .exec("cat > /tmp/out.txt <<'EOF'\nhello\nworld\nEOF\ncat /tmp/out.txt")
4024            .await
4025            .unwrap();
4026        assert_eq!(result.stdout, "hello\nworld\n");
4027        assert_eq!(result.exit_code, 0);
4028    }
4029
4030    #[tokio::test]
4031    async fn test_heredoc_redirect_to_file_unquoted() {
4032        let mut bash = Bash::new();
4033        let result = bash
4034            .exec("cat > /tmp/out.txt <<EOF\nhello\nworld\nEOF\ncat /tmp/out.txt")
4035            .await
4036            .unwrap();
4037        assert_eq!(result.stdout, "hello\nworld\n");
4038        assert_eq!(result.exit_code, 0);
4039    }
4040
4041    // === Issue 2: Compound pipelines ===
4042
4043    #[tokio::test]
4044    async fn test_pipe_to_while_read() {
4045        // cmd | while read ...; do ... done is extremely common
4046        let mut bash = Bash::new();
4047        let result = bash
4048            .exec("echo -e 'a\\nb\\nc' | while read line; do echo \"got: $line\"; done")
4049            .await
4050            .unwrap();
4051        assert!(
4052            result.stdout.contains("got: a"),
4053            "stdout: {}",
4054            result.stdout
4055        );
4056        assert!(
4057            result.stdout.contains("got: b"),
4058            "stdout: {}",
4059            result.stdout
4060        );
4061        assert!(
4062            result.stdout.contains("got: c"),
4063            "stdout: {}",
4064            result.stdout
4065        );
4066    }
4067
4068    #[tokio::test]
4069    async fn test_pipe_to_while_read_count() {
4070        let mut bash = Bash::new();
4071        let result = bash
4072            .exec("printf 'x\\ny\\nz\\n' | while read line; do echo $line; done")
4073            .await
4074            .unwrap();
4075        assert_eq!(result.stdout, "x\ny\nz\n");
4076    }
4077
4078    // === Issue 3: Source loading functions ===
4079
4080    #[tokio::test]
4081    async fn test_source_loads_functions() {
4082        let mut bash = Bash::new();
4083        // Write a function library, then source it and call the function
4084        bash.exec("cat > /tmp/lib.sh <<'EOF'\ngreet() { echo \"hello $1\"; }\nEOF")
4085            .await
4086            .unwrap();
4087        let result = bash.exec("source /tmp/lib.sh; greet world").await.unwrap();
4088        assert_eq!(result.stdout, "hello world\n");
4089        assert_eq!(result.exit_code, 0);
4090    }
4091
4092    #[tokio::test]
4093    async fn test_source_loads_variables() {
4094        let mut bash = Bash::new();
4095        bash.exec("echo 'MY_VAR=loaded' > /tmp/vars.sh")
4096            .await
4097            .unwrap();
4098        let result = bash
4099            .exec("source /tmp/vars.sh; echo $MY_VAR")
4100            .await
4101            .unwrap();
4102        assert_eq!(result.stdout, "loaded\n");
4103    }
4104
4105    // === Issue 4: chmod +x symbolic mode ===
4106
4107    #[tokio::test]
4108    async fn test_chmod_symbolic_plus_x() {
4109        let mut bash = Bash::new();
4110        bash.exec("echo '#!/bin/bash' > /tmp/script.sh")
4111            .await
4112            .unwrap();
4113        let result = bash.exec("chmod +x /tmp/script.sh").await.unwrap();
4114        assert_eq!(
4115            result.exit_code, 0,
4116            "chmod +x should succeed: {}",
4117            result.stderr
4118        );
4119    }
4120
4121    #[tokio::test]
4122    async fn test_chmod_symbolic_u_plus_x() {
4123        let mut bash = Bash::new();
4124        bash.exec("echo 'test' > /tmp/file.txt").await.unwrap();
4125        let result = bash.exec("chmod u+x /tmp/file.txt").await.unwrap();
4126        assert_eq!(
4127            result.exit_code, 0,
4128            "chmod u+x should succeed: {}",
4129            result.stderr
4130        );
4131    }
4132
4133    #[tokio::test]
4134    async fn test_chmod_symbolic_a_plus_r() {
4135        let mut bash = Bash::new();
4136        bash.exec("echo 'test' > /tmp/file.txt").await.unwrap();
4137        let result = bash.exec("chmod a+r /tmp/file.txt").await.unwrap();
4138        assert_eq!(
4139            result.exit_code, 0,
4140            "chmod a+r should succeed: {}",
4141            result.stderr
4142        );
4143    }
4144
4145    // === Issue 5: Awk arrays ===
4146
4147    #[tokio::test]
4148    async fn test_awk_array_length() {
4149        // length(arr) should return element count
4150        let mut bash = Bash::new();
4151        let result = bash
4152            .exec(r#"echo "" | awk 'BEGIN{a[1]="x"; a[2]="y"; a[3]="z"} END{print length(a)}'"#)
4153            .await
4154            .unwrap();
4155        assert_eq!(result.stdout, "3\n");
4156    }
4157
4158    #[tokio::test]
4159    async fn test_awk_array_read_after_split() {
4160        // split() + reading elements back
4161        let mut bash = Bash::new();
4162        let result = bash
4163            .exec(r#"echo "a:b:c" | awk '{n=split($0,arr,":"); for(i=1;i<=n;i++) print arr[i]}'"#)
4164            .await
4165            .unwrap();
4166        assert_eq!(result.stdout, "a\nb\nc\n");
4167    }
4168
4169    #[tokio::test]
4170    async fn test_awk_array_word_count_pattern() {
4171        // Classic word frequency count - the most common awk array pattern
4172        let mut bash = Bash::new();
4173        let result = bash
4174            .exec(
4175                r#"printf "apple\nbanana\napple\ncherry\nbanana\napple" | awk '{count[$1]++} END{for(w in count) print w, count[w]}'"#,
4176            )
4177            .await
4178            .unwrap();
4179        assert!(
4180            result.stdout.contains("apple 3"),
4181            "stdout: {}",
4182            result.stdout
4183        );
4184        assert!(
4185            result.stdout.contains("banana 2"),
4186            "stdout: {}",
4187            result.stdout
4188        );
4189        assert!(
4190            result.stdout.contains("cherry 1"),
4191            "stdout: {}",
4192            result.stdout
4193        );
4194    }
4195
4196    // ---- Streaming output tests ----
4197
4198    #[tokio::test]
4199    async fn test_exec_streaming_for_loop() {
4200        let chunks = Arc::new(Mutex::new(Vec::new()));
4201        let chunks_cb = chunks.clone();
4202        let mut bash = Bash::new();
4203
4204        let result = bash
4205            .exec_streaming(
4206                "for i in 1 2 3; do echo $i; done",
4207                Box::new(move |stdout, _stderr| {
4208                    chunks_cb.lock().unwrap().push(stdout.to_string());
4209                }),
4210            )
4211            .await
4212            .unwrap();
4213
4214        assert_eq!(result.stdout, "1\n2\n3\n");
4215        assert_eq!(
4216            *chunks.lock().unwrap(),
4217            vec!["1\n", "2\n", "3\n"],
4218            "each loop iteration should stream separately"
4219        );
4220    }
4221
4222    #[tokio::test]
4223    async fn test_exec_streaming_while_loop() {
4224        let chunks = Arc::new(Mutex::new(Vec::new()));
4225        let chunks_cb = chunks.clone();
4226        let mut bash = Bash::new();
4227
4228        let result = bash
4229            .exec_streaming(
4230                "i=0; while [ $i -lt 3 ]; do i=$((i+1)); echo $i; done",
4231                Box::new(move |stdout, _stderr| {
4232                    chunks_cb.lock().unwrap().push(stdout.to_string());
4233                }),
4234            )
4235            .await
4236            .unwrap();
4237
4238        assert_eq!(result.stdout, "1\n2\n3\n");
4239        let chunks = chunks.lock().unwrap();
4240        // The while loop emits each iteration; surrounding list may add events too
4241        assert!(
4242            chunks.contains(&"1\n".to_string()),
4243            "should contain first iteration output"
4244        );
4245        assert!(
4246            chunks.contains(&"2\n".to_string()),
4247            "should contain second iteration output"
4248        );
4249        assert!(
4250            chunks.contains(&"3\n".to_string()),
4251            "should contain third iteration output"
4252        );
4253    }
4254
4255    #[tokio::test]
4256    async fn test_exec_streaming_no_callback_still_works() {
4257        // exec (non-streaming) should still work fine
4258        let mut bash = Bash::new();
4259        let result = bash.exec("for i in a b c; do echo $i; done").await.unwrap();
4260        assert_eq!(result.stdout, "a\nb\nc\n");
4261    }
4262
4263    #[tokio::test]
4264    async fn test_exec_streaming_nested_loops_no_duplicates() {
4265        let chunks = Arc::new(Mutex::new(Vec::new()));
4266        let chunks_cb = chunks.clone();
4267        let mut bash = Bash::new();
4268
4269        let result = bash
4270            .exec_streaming(
4271                "for i in 1 2; do for j in a b; do echo \"$i$j\"; done; done",
4272                Box::new(move |stdout, _stderr| {
4273                    chunks_cb.lock().unwrap().push(stdout.to_string());
4274                }),
4275            )
4276            .await
4277            .unwrap();
4278
4279        assert_eq!(result.stdout, "1a\n1b\n2a\n2b\n");
4280        let chunks = chunks.lock().unwrap();
4281        // Inner loop should emit each iteration; outer should not duplicate
4282        let total_chars: usize = chunks.iter().map(|c| c.len()).sum();
4283        assert_eq!(
4284            total_chars,
4285            result.stdout.len(),
4286            "total streamed bytes should match final output: chunks={:?}",
4287            *chunks
4288        );
4289    }
4290
4291    #[tokio::test]
4292    async fn test_exec_streaming_mixed_list_and_loop() {
4293        let chunks = Arc::new(Mutex::new(Vec::new()));
4294        let chunks_cb = chunks.clone();
4295        let mut bash = Bash::new();
4296
4297        let result = bash
4298            .exec_streaming(
4299                "echo start; for i in 1 2; do echo $i; done; echo end",
4300                Box::new(move |stdout, _stderr| {
4301                    chunks_cb.lock().unwrap().push(stdout.to_string());
4302                }),
4303            )
4304            .await
4305            .unwrap();
4306
4307        assert_eq!(result.stdout, "start\n1\n2\nend\n");
4308        let chunks = chunks.lock().unwrap();
4309        assert_eq!(
4310            *chunks,
4311            vec!["start\n", "1\n", "2\n", "end\n"],
4312            "mixed list+loop should produce exactly 4 events"
4313        );
4314    }
4315
4316    #[tokio::test]
4317    async fn test_exec_streaming_stderr() {
4318        let stderr_chunks = Arc::new(Mutex::new(Vec::new()));
4319        let stderr_cb = stderr_chunks.clone();
4320        let mut bash = Bash::new();
4321
4322        let result = bash
4323            .exec_streaming(
4324                "echo ok; echo err >&2; echo ok2",
4325                Box::new(move |_stdout, stderr| {
4326                    if !stderr.is_empty() {
4327                        stderr_cb.lock().unwrap().push(stderr.to_string());
4328                    }
4329                }),
4330            )
4331            .await
4332            .unwrap();
4333
4334        assert_eq!(result.stdout, "ok\nok2\n");
4335        assert_eq!(result.stderr, "err\n");
4336        let stderr_chunks = stderr_chunks.lock().unwrap();
4337        assert!(
4338            stderr_chunks.contains(&"err\n".to_string()),
4339            "stderr should be streamed: {:?}",
4340            *stderr_chunks
4341        );
4342    }
4343
4344    // ---- Streamed vs non-streamed equivalence tests ----
4345    //
4346    // These run the same script through exec() and exec_streaming() and assert
4347    // that the final ExecResult is identical, plus concatenated chunks == stdout.
4348
4349    /// Helper: run script both ways, assert equivalence.
4350    async fn assert_streaming_equivalence(script: &str) {
4351        // Non-streaming
4352        let mut bash_plain = Bash::new();
4353        let plain = bash_plain.exec(script).await.unwrap();
4354
4355        // Streaming
4356        let stdout_chunks: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
4357        let stderr_chunks: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
4358        let so = stdout_chunks.clone();
4359        let se = stderr_chunks.clone();
4360        let mut bash_stream = Bash::new();
4361        let streamed = bash_stream
4362            .exec_streaming(
4363                script,
4364                Box::new(move |stdout, stderr| {
4365                    if !stdout.is_empty() {
4366                        so.lock().unwrap().push(stdout.to_string());
4367                    }
4368                    if !stderr.is_empty() {
4369                        se.lock().unwrap().push(stderr.to_string());
4370                    }
4371                }),
4372            )
4373            .await
4374            .unwrap();
4375
4376        // Final results must match
4377        assert_eq!(
4378            plain.stdout, streamed.stdout,
4379            "stdout mismatch for: {script}"
4380        );
4381        assert_eq!(
4382            plain.stderr, streamed.stderr,
4383            "stderr mismatch for: {script}"
4384        );
4385        assert_eq!(
4386            plain.exit_code, streamed.exit_code,
4387            "exit_code mismatch for: {script}"
4388        );
4389
4390        // Concatenated chunks must equal full stdout/stderr
4391        let reassembled_stdout: String = stdout_chunks.lock().unwrap().iter().cloned().collect();
4392        assert_eq!(
4393            reassembled_stdout, streamed.stdout,
4394            "reassembled stdout chunks != final stdout for: {script}"
4395        );
4396        let reassembled_stderr: String = stderr_chunks.lock().unwrap().iter().cloned().collect();
4397        assert_eq!(
4398            reassembled_stderr, streamed.stderr,
4399            "reassembled stderr chunks != final stderr for: {script}"
4400        );
4401    }
4402
4403    #[tokio::test]
4404    async fn test_streaming_equivalence_for_loop() {
4405        assert_streaming_equivalence("for i in 1 2 3; do echo $i; done").await;
4406    }
4407
4408    #[tokio::test]
4409    async fn test_streaming_equivalence_while_loop() {
4410        assert_streaming_equivalence("i=0; while [ $i -lt 4 ]; do i=$((i+1)); echo $i; done").await;
4411    }
4412
4413    #[tokio::test]
4414    async fn test_streaming_equivalence_nested_loops() {
4415        assert_streaming_equivalence("for i in a b; do for j in 1 2; do echo \"$i$j\"; done; done")
4416            .await;
4417    }
4418
4419    #[tokio::test]
4420    async fn test_streaming_equivalence_mixed_list() {
4421        assert_streaming_equivalence("echo start; for i in x y; do echo $i; done; echo end").await;
4422    }
4423
4424    #[tokio::test]
4425    async fn test_streaming_equivalence_stderr() {
4426        assert_streaming_equivalence("echo out; echo err >&2; echo out2").await;
4427    }
4428
4429    #[tokio::test]
4430    async fn test_streaming_equivalence_pipeline() {
4431        assert_streaming_equivalence("echo -e 'a\\nb\\nc' | grep b").await;
4432    }
4433
4434    #[tokio::test]
4435    async fn test_streaming_equivalence_conditionals() {
4436        assert_streaming_equivalence("if true; then echo yes; else echo no; fi; echo done").await;
4437    }
4438
4439    #[tokio::test]
4440    async fn test_streaming_equivalence_subshell() {
4441        assert_streaming_equivalence("x=$(echo hello); echo $x").await;
4442    }
4443}