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