Skip to main content

kaish_kernel/
kernel.rs

1//! The Kernel (核) — the heart of kaish.
2//!
3//! The Kernel owns and coordinates all core components:
4//! - Interpreter state (scope, $?)
5//! - Tool registry (builtins, user tools, MCP)
6//! - VFS router (mount points)
7//! - Job manager (background jobs)
8//!
9//! # Architecture
10//!
11//! ```text
12//! ┌────────────────────────────────────────────────────────────┐
13//! │                         Kernel (核)                         │
14//! │  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
15//! │  │   Scope      │  │ ToolRegistry │  │  VfsRouter       │  │
16//! │  │  (variables) │  │   (builtins, │  │  (mount points)  │  │
17//! │  │              │  │    MCP, user)│  │                  │  │
18//! │  └──────────────┘  └──────────────┘  └──────────────────┘  │
19//! │  ┌──────────────────────────────┐  ┌──────────────────┐    │
20//! │  │  JobManager (background)     │  │  ExecResult ($?) │    │
21//! │  └──────────────────────────────┘  └──────────────────┘    │
22//! └────────────────────────────────────────────────────────────┘
23//! ```
24
25use std::collections::HashMap;
26use std::path::PathBuf;
27use std::sync::Arc;
28
29use anyhow::{Context, Result};
30use tokio::sync::RwLock;
31
32use crate::ast::{Arg, Expr, Stmt, StringPart, ToolDef, Value, BinaryOp};
33use crate::backend::{BackendError, KernelBackend};
34use kaish_glob::glob_match;
35use crate::interpreter::{apply_output_format, eval_expr, expand_tilde, json_to_value, value_to_string, ControlFlow, ExecResult, Scope};
36use crate::parser::parse;
37use crate::scheduler::{drain_to_stream, BoundedStream, JobManager, PipelineRunner, DEFAULT_STREAM_MAX_SIZE};
38use crate::tools::{extract_output_format, register_builtins, resolve_in_path, ExecContext, ToolArgs, ToolRegistry};
39use crate::validator::{Severity, Validator};
40use crate::vfs::{JobFs, LocalFs, MemoryFs, VfsRouter};
41
42/// VFS mount mode determines how the local filesystem is exposed.
43///
44/// Different modes trade off convenience vs. security:
45/// - `Passthrough` gives native path access (best for human REPL use)
46/// - `Sandboxed` restricts access to a subtree (safer for agents)
47/// - `NoLocal` provides complete isolation (tests, pure memory mode)
48#[derive(Debug, Clone)]
49pub enum VfsMountMode {
50    /// LocalFs at "/" — native paths work directly.
51    ///
52    /// Full filesystem access. Use for human-operated REPL sessions where
53    /// native paths like `/home/user/project` should just work.
54    ///
55    /// Mounts:
56    /// - `/` → LocalFs("/")
57    /// - `/v` → MemoryFs (blob storage)
58    /// - `/scratch` → MemoryFs (ephemeral)
59    Passthrough,
60
61    /// Transparent sandbox — paths look native but access is restricted.
62    ///
63    /// The local filesystem is mounted at its real path (e.g., `/home/user`),
64    /// so `/home/user/src/project` just works. But paths outside the sandbox
65    /// root are not accessible.
66    ///
67    /// Mounts:
68    /// - `/` → MemoryFs (catches paths outside sandbox)
69    /// - `{root}` → LocalFs(root)  (e.g., `/home/user` → LocalFs)
70    /// - `/tmp` → MemoryFs
71    /// - `/v` → MemoryFs (blob storage)
72    /// - `/scratch` → MemoryFs
73    Sandboxed {
74        /// Root path for local filesystem. Defaults to `$HOME`.
75        /// Can be restricted further, e.g., `~/src`.
76        root: Option<PathBuf>,
77    },
78
79    /// No local filesystem. Memory only.
80    ///
81    /// Complete isolation — no access to the host filesystem.
82    /// Useful for tests or pure sandboxed execution.
83    ///
84    /// Mounts:
85    /// - `/` → MemoryFs
86    /// - `/tmp` → MemoryFs
87    /// - `/v` → MemoryFs
88    /// - `/scratch` → MemoryFs
89    NoLocal,
90}
91
92impl Default for VfsMountMode {
93    fn default() -> Self {
94        VfsMountMode::Sandboxed { root: None }
95    }
96}
97
98/// Configuration for kernel initialization.
99#[derive(Debug, Clone)]
100pub struct KernelConfig {
101    /// Name of this kernel (for identification).
102    pub name: String,
103
104    /// VFS mount mode — controls how local filesystem is exposed.
105    pub vfs_mode: VfsMountMode,
106
107    /// Initial working directory (VFS path).
108    pub cwd: PathBuf,
109
110    /// Whether to skip pre-execution validation.
111    ///
112    /// When false (default), scripts are validated before execution to catch
113    /// errors early. Set to true to skip validation for performance or to
114    /// allow dynamic/external commands.
115    pub skip_validation: bool,
116}
117
118/// Get the default sandbox root ($HOME).
119fn default_sandbox_root() -> PathBuf {
120    std::env::var("HOME")
121        .map(PathBuf::from)
122        .unwrap_or_else(|_| PathBuf::from("/"))
123}
124
125impl Default for KernelConfig {
126    fn default() -> Self {
127        let home = default_sandbox_root();
128        Self {
129            name: "default".to_string(),
130            vfs_mode: VfsMountMode::Sandboxed { root: None },
131            cwd: home,
132            skip_validation: false,
133        }
134    }
135}
136
137impl KernelConfig {
138    /// Create a transient kernel config (sandboxed, for temporary use).
139    pub fn transient() -> Self {
140        let home = default_sandbox_root();
141        Self {
142            name: "transient".to_string(),
143            vfs_mode: VfsMountMode::Sandboxed { root: None },
144            cwd: home,
145            skip_validation: false,
146        }
147    }
148
149    /// Create a kernel config with the given name (sandboxed by default).
150    pub fn named(name: &str) -> Self {
151        let home = default_sandbox_root();
152        Self {
153            name: name.to_string(),
154            vfs_mode: VfsMountMode::Sandboxed { root: None },
155            cwd: home,
156            skip_validation: false,
157        }
158    }
159
160    /// Create a REPL config with passthrough filesystem access.
161    ///
162    /// Native paths like `/home/user/project` work directly.
163    /// The cwd is set to the actual current working directory.
164    pub fn repl() -> Self {
165        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
166        Self {
167            name: "repl".to_string(),
168            vfs_mode: VfsMountMode::Passthrough,
169            cwd,
170            skip_validation: false,
171        }
172    }
173
174    /// Create an MCP server config with sandboxed filesystem access.
175    ///
176    /// Local filesystem is accessible at its real path (e.g., `/home/user`),
177    /// but sandboxed to `$HOME`. Paths outside the sandbox are not accessible.
178    pub fn mcp() -> Self {
179        let home = default_sandbox_root();
180        Self {
181            name: "mcp".to_string(),
182            vfs_mode: VfsMountMode::Sandboxed { root: None },
183            cwd: home,
184            skip_validation: false,
185        }
186    }
187
188    /// Create an MCP server config with a custom sandbox root.
189    ///
190    /// Use this to restrict access to a subdirectory like `~/src`.
191    pub fn mcp_with_root(root: PathBuf) -> Self {
192        Self {
193            name: "mcp".to_string(),
194            vfs_mode: VfsMountMode::Sandboxed { root: Some(root.clone()) },
195            cwd: root,
196            skip_validation: false,
197        }
198    }
199
200    /// Create a config with no local filesystem (memory only).
201    ///
202    /// Useful for tests or pure sandboxed execution.
203    pub fn isolated() -> Self {
204        Self {
205            name: "isolated".to_string(),
206            vfs_mode: VfsMountMode::NoLocal,
207            cwd: PathBuf::from("/"),
208            skip_validation: false,
209        }
210    }
211
212    /// Set the VFS mount mode.
213    pub fn with_vfs_mode(mut self, mode: VfsMountMode) -> Self {
214        self.vfs_mode = mode;
215        self
216    }
217
218    /// Set the initial working directory.
219    pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
220        self.cwd = cwd;
221        self
222    }
223
224    /// Skip pre-execution validation.
225    pub fn with_skip_validation(mut self, skip: bool) -> Self {
226        self.skip_validation = skip;
227        self
228    }
229}
230
231/// The Kernel (核) — executes kaish code.
232///
233/// This is the primary interface for running kaish commands. It owns all
234/// the runtime state: variables, tools, VFS, jobs, and persistence.
235pub struct Kernel {
236    /// Kernel name.
237    name: String,
238    /// Variable scope.
239    scope: RwLock<Scope>,
240    /// Tool registry.
241    tools: Arc<ToolRegistry>,
242    /// User-defined tools (from `tool name { body }` statements).
243    user_tools: RwLock<HashMap<String, ToolDef>>,
244    /// Virtual filesystem router.
245    vfs: Arc<VfsRouter>,
246    /// Background job manager.
247    jobs: Arc<JobManager>,
248    /// Pipeline runner.
249    runner: PipelineRunner,
250    /// Execution context (cwd, stdin, etc.).
251    exec_ctx: RwLock<ExecContext>,
252    /// Whether to skip pre-execution validation.
253    skip_validation: bool,
254}
255
256impl Kernel {
257    /// Create a new kernel with the given configuration.
258    pub fn new(config: KernelConfig) -> Result<Self> {
259        let mut vfs = Self::setup_vfs(&config);
260        let jobs = Arc::new(JobManager::new());
261
262        // Mount JobFs for job observability at /v/jobs
263        vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
264
265        let vfs = Arc::new(vfs);
266
267        // Set up tools
268        let mut tools = ToolRegistry::new();
269        register_builtins(&mut tools);
270        let tools = Arc::new(tools);
271
272        // Pipeline runner
273        let runner = PipelineRunner::new(tools.clone());
274
275        let scope = Scope::new();
276        let cwd = config.cwd;
277
278        // Create execution context with VFS and tools for backend dispatch
279        let mut exec_ctx = ExecContext::with_vfs_and_tools(vfs.clone(), tools.clone());
280        exec_ctx.set_cwd(cwd);
281        exec_ctx.set_job_manager(jobs.clone());
282        exec_ctx.set_tool_schemas(tools.schemas());
283
284        Ok(Self {
285            name: config.name,
286            scope: RwLock::new(scope),
287            tools,
288            user_tools: RwLock::new(HashMap::new()),
289            vfs,
290            jobs,
291            runner,
292            exec_ctx: RwLock::new(exec_ctx),
293            skip_validation: config.skip_validation,
294        })
295    }
296
297    /// Set up VFS based on mount mode.
298    fn setup_vfs(config: &KernelConfig) -> VfsRouter {
299        let mut vfs = VfsRouter::new();
300
301        match &config.vfs_mode {
302            VfsMountMode::Passthrough => {
303                // LocalFs at "/" — native paths work directly
304                vfs.mount("/", LocalFs::new(PathBuf::from("/")));
305                // Memory for blobs and scratch
306                vfs.mount("/v", MemoryFs::new());
307                vfs.mount("/scratch", MemoryFs::new());
308            }
309            VfsMountMode::Sandboxed { root } => {
310                // Memory at root for safety (catches paths outside sandbox)
311                vfs.mount("/", MemoryFs::new());
312                vfs.mount("/v", MemoryFs::new());
313                vfs.mount("/scratch", MemoryFs::new());
314
315                // Real /tmp for interop with other processes
316                vfs.mount("/tmp", LocalFs::new(PathBuf::from("/tmp")));
317
318                // Resolve the sandbox root (defaults to $HOME)
319                let local_root = root.clone().unwrap_or_else(|| {
320                    std::env::var("HOME")
321                        .map(PathBuf::from)
322                        .unwrap_or_else(|_| PathBuf::from("/"))
323                });
324
325                // Mount at the real path for transparent access
326                // e.g., /home/atobey → LocalFs("/home/atobey")
327                // so /home/atobey/src/kaish just works
328                let mount_point = local_root.to_string_lossy().to_string();
329                vfs.mount(&mount_point, LocalFs::new(local_root));
330            }
331            VfsMountMode::NoLocal => {
332                // Pure memory mode — no local filesystem
333                vfs.mount("/", MemoryFs::new());
334                vfs.mount("/tmp", MemoryFs::new());
335                vfs.mount("/v", MemoryFs::new());
336                vfs.mount("/scratch", MemoryFs::new());
337            }
338        }
339
340        vfs
341    }
342
343    /// Create a transient kernel (no persistence).
344    pub fn transient() -> Result<Self> {
345        Self::new(KernelConfig::transient())
346    }
347
348    /// Create a kernel with a custom backend.
349    ///
350    /// This constructor allows embedding kaish in other systems that provide
351    /// their own storage backend (e.g., CRDT-backed storage in kaijutsu).
352    /// The provided backend will be used for all file operations in builtins.
353    ///
354    /// Note: A VfsRouter is still created internally for compatibility with
355    /// the `vfs()` method, but it won't be used for execution context operations.
356    pub fn with_backend(backend: Arc<dyn KernelBackend>, config: KernelConfig) -> Result<Self> {
357        // Create VFS for compatibility (but exec_ctx will use the provided backend)
358        let mut vfs = Self::setup_vfs(&config);
359        let jobs = Arc::new(JobManager::new());
360
361        // Mount JobFs for job observability at /v/jobs
362        vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
363
364        let vfs = Arc::new(vfs);
365
366        // Set up tools
367        let mut tools = ToolRegistry::new();
368        register_builtins(&mut tools);
369        let tools = Arc::new(tools);
370
371        // Pipeline runner
372        let runner = PipelineRunner::new(tools.clone());
373
374        let scope = Scope::new();
375        let cwd = config.cwd;
376
377        // Create execution context with custom backend
378        let mut exec_ctx = ExecContext::with_backend(backend);
379        exec_ctx.set_cwd(cwd);
380        exec_ctx.set_job_manager(jobs.clone());
381        exec_ctx.set_tool_schemas(tools.schemas());
382        exec_ctx.set_tools(tools.clone());
383
384        Ok(Self {
385            name: config.name,
386            scope: RwLock::new(scope),
387            tools,
388            user_tools: RwLock::new(HashMap::new()),
389            vfs,
390            jobs,
391            runner,
392            exec_ctx: RwLock::new(exec_ctx),
393            skip_validation: config.skip_validation,
394        })
395    }
396
397    /// Create a kernel with a custom backend and automatic `/v/*` path support.
398    ///
399    /// This is the recommended constructor for embedders who want their custom backend
400    /// to also support kaish's virtual filesystems (like `/v/jobs` for job observability).
401    ///
402    /// Paths are routed as follows:
403    /// - `/v/*` → Internal VFS (JobFs at `/v/jobs`, MemoryFs at `/v/blobs`, etc.)
404    /// - Everything else → Your custom backend
405    ///
406    /// # Example
407    ///
408    /// ```ignore
409    /// let my_backend: Arc<dyn KernelBackend> = Arc::new(MyBackend::new());
410    /// let kernel = Kernel::with_backend_and_virtual_paths(my_backend, config)?;
411    ///
412    /// // Now agents can use /v/jobs to monitor background commands
413    /// kernel.execute("cargo build &").await?;
414    /// kernel.execute("cat /v/jobs/1/stdout").await?;
415    /// ```
416    pub fn with_backend_and_virtual_paths(
417        backend: Arc<dyn KernelBackend>,
418        config: KernelConfig,
419    ) -> Result<Self> {
420        use crate::backend::VirtualOverlayBackend;
421
422        // Create VFS with virtual filesystems
423        let mut vfs = VfsRouter::new();
424        let jobs = Arc::new(JobManager::new());
425
426        // Mount JobFs for job observability
427        vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
428        // Mount MemoryFs for blob storage
429        vfs.mount("/v/blobs", MemoryFs::new());
430        // Mount MemoryFs for scratch space
431        vfs.mount("/v/scratch", MemoryFs::new());
432
433        let vfs = Arc::new(vfs);
434
435        // Wrap the backend with virtual overlay
436        let overlay: Arc<dyn KernelBackend> = Arc::new(VirtualOverlayBackend::new(backend, vfs.clone()));
437
438        // Set up tools
439        let mut tools = ToolRegistry::new();
440        register_builtins(&mut tools);
441        let tools = Arc::new(tools);
442
443        // Pipeline runner
444        let runner = PipelineRunner::new(tools.clone());
445
446        let scope = Scope::new();
447        let cwd = config.cwd;
448
449        // Create execution context with the overlay backend
450        let mut exec_ctx = ExecContext::with_backend(overlay);
451        exec_ctx.set_cwd(cwd);
452        exec_ctx.set_job_manager(jobs.clone());
453        exec_ctx.set_tool_schemas(tools.schemas());
454        exec_ctx.set_tools(tools.clone());
455
456        Ok(Self {
457            name: config.name,
458            scope: RwLock::new(scope),
459            tools,
460            user_tools: RwLock::new(HashMap::new()),
461            vfs,
462            jobs,
463            runner,
464            exec_ctx: RwLock::new(exec_ctx),
465            skip_validation: config.skip_validation,
466        })
467    }
468
469    /// Get the kernel name.
470    pub fn name(&self) -> &str {
471        &self.name
472    }
473
474    /// Execute kaish source code.
475    ///
476    /// Returns the result of the last statement executed.
477    pub async fn execute(&self, input: &str) -> Result<ExecResult> {
478
479        let program = parse(input).map_err(|errors| {
480            let msg = errors
481                .iter()
482                .map(|e| e.to_string())
483                .collect::<Vec<_>>()
484                .join("; ");
485            anyhow::anyhow!("parse error: {}", msg)
486        })?;
487
488        // Pre-execution validation
489        if !self.skip_validation {
490            let user_tools = self.user_tools.read().await;
491            let validator = Validator::new(&self.tools, &user_tools);
492            let issues = validator.validate(&program);
493
494            // Collect errors (warnings are logged but don't prevent execution)
495            let errors: Vec<_> = issues
496                .iter()
497                .filter(|i| i.severity == Severity::Error)
498                .collect();
499
500            if !errors.is_empty() {
501                let error_msg = errors
502                    .iter()
503                    .map(|e| e.format(input))
504                    .collect::<Vec<_>>()
505                    .join("\n");
506                return Err(anyhow::anyhow!("validation failed:\n{}", error_msg));
507            }
508
509            // Log warnings via tracing (trace level to avoid noise)
510            for warning in issues.iter().filter(|i| i.severity == Severity::Warning) {
511                tracing::trace!("validation: {}", warning.format(input));
512            }
513        }
514
515        let mut result = ExecResult::success("");
516
517        for stmt in program.statements {
518            if matches!(stmt, Stmt::Empty) {
519                continue;
520            }
521            let flow = self.execute_stmt_flow(&stmt).await?;
522            match flow {
523                ControlFlow::Normal(r) => accumulate_result(&mut result, &r),
524                ControlFlow::Exit { code } => {
525                    // Exit terminates execution immediately
526                    let exit_result = ExecResult::success(code.to_string());
527                    return Ok(exit_result);
528                }
529                ControlFlow::Return { value } => {
530                    // Return at top level just returns the value
531                    result = value;
532                }
533                ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
534                    // Break/continue at top level just returns the result
535                    result = r;
536                }
537            }
538        }
539
540        Ok(result)
541    }
542
543    /// Execute a single statement, returning control flow information.
544    fn execute_stmt_flow<'a>(
545        &'a self,
546        stmt: &'a Stmt,
547    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ControlFlow>> + 'a>> {
548        Box::pin(async move {
549        match stmt {
550            Stmt::Assignment(assign) => {
551                // Use async evaluator to support command substitution
552                let value = self.eval_expr_async(&assign.value).await
553                    .context("failed to evaluate assignment")?;
554                let mut scope = self.scope.write().await;
555                if assign.local {
556                    // local: set in innermost (current function) frame
557                    scope.set(&assign.name, value.clone());
558                } else {
559                    // non-local: update existing or create in root frame
560                    scope.set_global(&assign.name, value.clone());
561                }
562                drop(scope);
563
564                // Assignments don't produce output (like sh)
565                Ok(ControlFlow::ok(ExecResult::success("")))
566            }
567            Stmt::Command(cmd) => {
568                let result = self.execute_command(&cmd.name, &cmd.args).await?;
569                self.update_last_result(&result).await;
570
571                // Check for error exit mode (set -e)
572                if !result.ok() {
573                    let scope = self.scope.read().await;
574                    if scope.error_exit_enabled() {
575                        return Ok(ControlFlow::exit_code(result.code));
576                    }
577                }
578
579                Ok(ControlFlow::ok(result))
580            }
581            Stmt::Pipeline(pipeline) => {
582                let result = self.execute_pipeline(pipeline).await?;
583                self.update_last_result(&result).await;
584
585                // Check for error exit mode (set -e)
586                if !result.ok() {
587                    let scope = self.scope.read().await;
588                    if scope.error_exit_enabled() {
589                        return Ok(ControlFlow::exit_code(result.code));
590                    }
591                }
592
593                Ok(ControlFlow::ok(result))
594            }
595            Stmt::If(if_stmt) => {
596                // Use async evaluator to support command substitution in conditions
597                let cond_value = self.eval_expr_async(&if_stmt.condition).await?;
598
599                let branch = if is_truthy(&cond_value) {
600                    &if_stmt.then_branch
601                } else {
602                    if_stmt.else_branch.as_deref().unwrap_or(&[])
603                };
604
605                let mut flow = ControlFlow::ok(ExecResult::success(""));
606                for stmt in branch {
607                    flow = self.execute_stmt_flow(stmt).await?;
608                    if !flow.is_normal() {
609                        return Ok(flow);
610                    }
611                }
612                Ok(flow)
613            }
614            Stmt::For(for_loop) => {
615                // Evaluate all items and collect values for iteration
616                // Use async evaluator to support command substitution like $(seq 1 5)
617                let mut items: Vec<Value> = Vec::new();
618                for item_expr in &for_loop.items {
619                    let item = self.eval_expr_async(item_expr).await?;
620                    // NO implicit word splitting - arrays iterate, strings stay whole
621                    match &item {
622                        // JSON arrays iterate over elements
623                        Value::Json(serde_json::Value::Array(arr)) => {
624                            for elem in arr {
625                                items.push(json_to_value(elem.clone()));
626                            }
627                        }
628                        // Strings are ONE value - no splitting!
629                        // Use $(split "$VAR") for explicit splitting
630                        Value::String(_) => {
631                            items.push(item);
632                        }
633                        // Other values as-is
634                        _ => items.push(item),
635                    }
636                }
637
638                let mut result = ExecResult::success("");
639                {
640                    let mut scope = self.scope.write().await;
641                    scope.push_frame();
642                }
643
644                'outer: for item in items {
645                    {
646                        let mut scope = self.scope.write().await;
647                        scope.set(&for_loop.variable, item);
648                    }
649                    for stmt in &for_loop.body {
650                        let mut flow = self.execute_stmt_flow(stmt).await?;
651                        match &mut flow {
652                            ControlFlow::Normal(r) => accumulate_result(&mut result, r),
653                            ControlFlow::Break { .. } => {
654                                if flow.decrement_level() {
655                                    // Break handled at this level
656                                    break 'outer;
657                                }
658                                // Propagate to outer loop
659                                let mut scope = self.scope.write().await;
660                                scope.pop_frame();
661                                return Ok(flow);
662                            }
663                            ControlFlow::Continue { .. } => {
664                                if flow.decrement_level() {
665                                    // Continue handled at this level
666                                    continue 'outer;
667                                }
668                                // Propagate to outer loop
669                                let mut scope = self.scope.write().await;
670                                scope.pop_frame();
671                                return Ok(flow);
672                            }
673                            ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
674                                let mut scope = self.scope.write().await;
675                                scope.pop_frame();
676                                return Ok(flow);
677                            }
678                        }
679                    }
680                }
681
682                {
683                    let mut scope = self.scope.write().await;
684                    scope.pop_frame();
685                }
686                Ok(ControlFlow::ok(result))
687            }
688            Stmt::While(while_loop) => {
689                let mut result = ExecResult::success("");
690
691                'outer: loop {
692                    // Evaluate condition - use async to support command substitution
693                    let cond_value = self.eval_expr_async(&while_loop.condition).await?;
694
695                    if !is_truthy(&cond_value) {
696                        break;
697                    }
698
699                    // Execute body
700                    for stmt in &while_loop.body {
701                        let mut flow = self.execute_stmt_flow(stmt).await?;
702                        match &mut flow {
703                            ControlFlow::Normal(r) => accumulate_result(&mut result, r),
704                            ControlFlow::Break { .. } => {
705                                if flow.decrement_level() {
706                                    // Break handled at this level
707                                    break 'outer;
708                                }
709                                // Propagate to outer loop
710                                return Ok(flow);
711                            }
712                            ControlFlow::Continue { .. } => {
713                                if flow.decrement_level() {
714                                    // Continue handled at this level
715                                    continue 'outer;
716                                }
717                                // Propagate to outer loop
718                                return Ok(flow);
719                            }
720                            ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
721                                return Ok(flow);
722                            }
723                        }
724                    }
725                }
726
727                Ok(ControlFlow::ok(result))
728            }
729            Stmt::Case(case_stmt) => {
730                // Evaluate the expression to match against
731                let match_value = {
732                    let mut scope = self.scope.write().await;
733                    let value = eval_expr(&case_stmt.expr, &mut scope)?;
734                    value_to_string(&value)
735                };
736
737                // Try each branch until we find a match
738                for branch in &case_stmt.branches {
739                    let matched = branch.patterns.iter().any(|pattern| {
740                        glob_match(pattern, &match_value)
741                    });
742
743                    if matched {
744                        // Execute the branch body
745                        let mut result = ControlFlow::ok(ExecResult::success(""));
746                        for stmt in &branch.body {
747                            result = self.execute_stmt_flow(stmt).await?;
748                            if !result.is_normal() {
749                                return Ok(result);
750                            }
751                        }
752                        return Ok(result);
753                    }
754                }
755
756                // No match - return success with empty output (like sh)
757                Ok(ControlFlow::ok(ExecResult::success("")))
758            }
759            Stmt::Break(levels) => {
760                Ok(ControlFlow::break_n(levels.unwrap_or(1)))
761            }
762            Stmt::Continue(levels) => {
763                Ok(ControlFlow::continue_n(levels.unwrap_or(1)))
764            }
765            Stmt::Return(expr) => {
766                // return [N] - N becomes the exit code, NOT stdout
767                // Shell semantics: return sets exit code, doesn't produce output
768                let result = if let Some(e) = expr {
769                    let mut scope = self.scope.write().await;
770                    let val = eval_expr(e, &mut scope)?;
771                    // Convert value to exit code
772                    let code = match val {
773                        Value::Int(n) => n,
774                        Value::Bool(b) => if b { 0 } else { 1 },
775                        _ => 0,
776                    };
777                    ExecResult {
778                        code,
779                        out: String::new(),
780                        err: String::new(),
781                        data: None,
782                        output: None,
783                    }
784                } else {
785                    ExecResult::success("")
786                };
787                Ok(ControlFlow::return_value(result))
788            }
789            Stmt::Exit(expr) => {
790                let code = if let Some(e) = expr {
791                    let mut scope = self.scope.write().await;
792                    let val = eval_expr(e, &mut scope)?;
793                    match val {
794                        Value::Int(n) => n,
795                        _ => 0,
796                    }
797                } else {
798                    0
799                };
800                Ok(ControlFlow::exit_code(code))
801            }
802            Stmt::ToolDef(tool_def) => {
803                let mut user_tools = self.user_tools.write().await;
804                user_tools.insert(tool_def.name.clone(), tool_def.clone());
805                Ok(ControlFlow::ok(ExecResult::success("")))
806            }
807            Stmt::AndChain { left, right } => {
808                // cmd1 && cmd2 - run cmd2 only if cmd1 succeeds (exit code 0)
809                let left_flow = self.execute_stmt_flow(left).await?;
810                match left_flow {
811                    ControlFlow::Normal(left_result) => {
812                        self.update_last_result(&left_result).await;
813                        if left_result.ok() {
814                            let right_flow = self.execute_stmt_flow(right).await?;
815                            match right_flow {
816                                ControlFlow::Normal(right_result) => {
817                                    self.update_last_result(&right_result).await;
818                                    // Combine left and right output
819                                    let mut combined = left_result;
820                                    accumulate_result(&mut combined, &right_result);
821                                    Ok(ControlFlow::ok(combined))
822                                }
823                                other => Ok(other), // Propagate non-normal flow
824                            }
825                        } else {
826                            Ok(ControlFlow::ok(left_result))
827                        }
828                    }
829                    _ => Ok(left_flow), // Propagate non-normal flow
830                }
831            }
832            Stmt::OrChain { left, right } => {
833                // cmd1 || cmd2 - run cmd2 only if cmd1 fails (non-zero exit code)
834                let left_flow = self.execute_stmt_flow(left).await?;
835                match left_flow {
836                    ControlFlow::Normal(left_result) => {
837                        self.update_last_result(&left_result).await;
838                        if !left_result.ok() {
839                            let right_flow = self.execute_stmt_flow(right).await?;
840                            match right_flow {
841                                ControlFlow::Normal(right_result) => {
842                                    self.update_last_result(&right_result).await;
843                                    // Combine left and right output
844                                    let mut combined = left_result;
845                                    accumulate_result(&mut combined, &right_result);
846                                    Ok(ControlFlow::ok(combined))
847                                }
848                                other => Ok(other), // Propagate non-normal flow
849                            }
850                        } else {
851                            Ok(ControlFlow::ok(left_result))
852                        }
853                    }
854                    _ => Ok(left_flow), // Propagate non-normal flow
855                }
856            }
857            Stmt::Test(test_expr) => {
858                // Evaluate the test expression by wrapping in Expr::Test
859                let expr = crate::ast::Expr::Test(Box::new(test_expr.clone()));
860                let mut scope = self.scope.write().await;
861                let value = eval_expr(&expr, &mut scope)?;
862                drop(scope);
863                let is_true = match value {
864                    crate::ast::Value::Bool(b) => b,
865                    _ => false,
866                };
867                if is_true {
868                    Ok(ControlFlow::ok(ExecResult::success("")))
869                } else {
870                    Ok(ControlFlow::ok(ExecResult::failure(1, "")))
871                }
872            }
873            Stmt::Empty => Ok(ControlFlow::ok(ExecResult::success(""))),
874        }
875        })
876    }
877
878    /// Execute a pipeline.
879    async fn execute_pipeline(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
880        if pipeline.commands.is_empty() {
881            return Ok(ExecResult::success(""));
882        }
883
884        // Handle background execution (`&` operator)
885        if pipeline.background {
886            return self.execute_background(pipeline).await;
887        }
888
889        // For single command without redirects, execute directly for efficiency
890        if pipeline.commands.len() == 1 && pipeline.commands[0].redirects.is_empty() {
891            let cmd = &pipeline.commands[0];
892            return self.execute_command(&cmd.name, &cmd.args).await;
893        }
894
895        // Pipeline with redirects or multiple commands uses the runner
896        let mut ctx = self.exec_ctx.write().await;
897        {
898            let scope = self.scope.read().await;
899            ctx.scope = scope.clone();
900        }
901
902        let result = self.runner.run(&pipeline.commands, &mut ctx).await;
903
904        // Sync changes back from context
905        {
906            let mut scope = self.scope.write().await;
907            *scope = ctx.scope.clone();
908        }
909
910        Ok(result)
911    }
912
913    /// Execute a pipeline in the background.
914    ///
915    /// The command is spawned as a tokio task, registered with the JobManager,
916    /// and its output is captured via BoundedStreams. The job is observable via
917    /// `/v/jobs/{id}/stdout`, `/v/jobs/{id}/stderr`, and `/v/jobs/{id}/status`.
918    ///
919    /// Returns immediately with a job ID like "[1]".
920    async fn execute_background(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
921        use tokio::sync::oneshot;
922
923        // Format the command for display in /v/jobs/{id}/command
924        let command_str = self.format_pipeline(pipeline);
925
926        // Create bounded streams for output capture
927        let stdout = Arc::new(BoundedStream::default_size());
928        let stderr = Arc::new(BoundedStream::default_size());
929
930        // Create channel for result notification
931        let (tx, rx) = oneshot::channel();
932
933        // Register with JobManager to get job ID and create VFS entries
934        let job_id = self.jobs.register_with_streams(
935            command_str.clone(),
936            rx,
937            stdout.clone(),
938            stderr.clone(),
939        ).await;
940
941        // Clone state needed for the spawned task
942        let runner = self.runner.clone();
943        let commands = pipeline.commands.clone();
944        let backend = {
945            let ctx = self.exec_ctx.read().await;
946            ctx.backend.clone()
947        };
948        let scope = {
949            let scope = self.scope.read().await;
950            scope.clone()
951        };
952        let cwd = {
953            let ctx = self.exec_ctx.read().await;
954            ctx.cwd.clone()
955        };
956        let tools = self.tools.clone();
957        let tool_schemas = self.tools.schemas();
958
959        // Spawn the background task
960        tokio::spawn(async move {
961            // Create execution context for the background job
962            // It inherits env vars and cwd from the parent context
963            let mut bg_ctx = ExecContext::with_backend(backend);
964            bg_ctx.scope = scope;
965            bg_ctx.cwd = cwd;
966            bg_ctx.set_tools(tools);
967            bg_ctx.set_tool_schemas(tool_schemas);
968
969            // Execute the pipeline
970            let result = runner.run(&commands, &mut bg_ctx).await;
971
972            // Write output to streams
973            if !result.out.is_empty() {
974                stdout.write(result.out.as_bytes()).await;
975            }
976            if !result.err.is_empty() {
977                stderr.write(result.err.as_bytes()).await;
978            }
979
980            // Close streams
981            stdout.close().await;
982            stderr.close().await;
983
984            // Send result to JobManager (ignore error if receiver dropped)
985            let _ = tx.send(result);
986        });
987
988        Ok(ExecResult::success(format!("[{}]", job_id)))
989    }
990
991    /// Format a pipeline as a command string for display.
992    fn format_pipeline(&self, pipeline: &crate::ast::Pipeline) -> String {
993        pipeline.commands
994            .iter()
995            .map(|cmd| {
996                let mut parts = vec![cmd.name.clone()];
997                for arg in &cmd.args {
998                    match arg {
999                        Arg::Positional(expr) => {
1000                            parts.push(self.format_expr(expr));
1001                        }
1002                        Arg::Named { key, value } => {
1003                            parts.push(format!("{}={}", key, self.format_expr(value)));
1004                        }
1005                        Arg::ShortFlag(name) => {
1006                            parts.push(format!("-{}", name));
1007                        }
1008                        Arg::LongFlag(name) => {
1009                            parts.push(format!("--{}", name));
1010                        }
1011                        Arg::DoubleDash => {
1012                            parts.push("--".to_string());
1013                        }
1014                    }
1015                }
1016                parts.join(" ")
1017            })
1018            .collect::<Vec<_>>()
1019            .join(" | ")
1020    }
1021
1022    /// Format an expression as a string for display.
1023    fn format_expr(&self, expr: &Expr) -> String {
1024        match expr {
1025            Expr::Literal(Value::String(s)) => {
1026                if s.contains(' ') || s.contains('"') {
1027                    format!("'{}'", s.replace('\'', "\\'"))
1028                } else {
1029                    s.clone()
1030                }
1031            }
1032            Expr::Literal(Value::Int(i)) => i.to_string(),
1033            Expr::Literal(Value::Float(f)) => f.to_string(),
1034            Expr::Literal(Value::Bool(b)) => b.to_string(),
1035            Expr::Literal(Value::Null) => "null".to_string(),
1036            Expr::VarRef(path) => {
1037                let name = path.segments.iter()
1038                    .map(|seg| match seg {
1039                        crate::ast::VarSegment::Field(f) => f.clone(),
1040                    })
1041                    .collect::<Vec<_>>()
1042                    .join(".");
1043                format!("${{{}}}", name)
1044            }
1045            Expr::Interpolated(_) => "\"...\"".to_string(),
1046            _ => "...".to_string(),
1047        }
1048    }
1049
1050    /// Execute a single command.
1051    async fn execute_command(&self, name: &str, args: &[Arg]) -> Result<ExecResult> {
1052        // Special built-ins
1053        match name {
1054            "true" => return Ok(ExecResult::success("")),
1055            "false" => return Ok(ExecResult::failure(1, "")),
1056            "source" | "." => return self.execute_source(args).await,
1057            _ => {}
1058        }
1059
1060        // Check user-defined tools first
1061        {
1062            let user_tools = self.user_tools.read().await;
1063            if let Some(tool_def) = user_tools.get(name) {
1064                let tool_def = tool_def.clone();
1065                drop(user_tools);
1066                return self.execute_user_tool(tool_def, args).await;
1067            }
1068        }
1069
1070        // Look up builtin tool
1071        let tool = match self.tools.get(name) {
1072            Some(t) => t,
1073            None => {
1074                // Try executing as .kai script from PATH
1075                if let Some(result) = self.try_execute_script(name, args).await? {
1076                    return Ok(result);
1077                }
1078                // Try executing as external command from PATH
1079                if let Some(result) = self.try_execute_external(name, args).await? {
1080                    return Ok(result);
1081                }
1082
1083                // Try backend-registered tools (embedder engines, MCP tools, etc.)
1084                let tool_args = self.build_args_async(args).await?;
1085                let mut ctx = self.exec_ctx.write().await;
1086                {
1087                    let scope = self.scope.read().await;
1088                    ctx.scope = scope.clone();
1089                }
1090                let backend = ctx.backend.clone();
1091                match backend.call_tool(name, tool_args, &mut ctx).await {
1092                    Ok(tool_result) => {
1093                        let mut scope = self.scope.write().await;
1094                        *scope = ctx.scope.clone();
1095                        let mut exec = ExecResult::from_output(
1096                            tool_result.code as i64, tool_result.stdout, tool_result.stderr,
1097                        );
1098                        exec.output = tool_result.output;
1099                        return Ok(exec);
1100                    }
1101                    Err(BackendError::ToolNotFound(_)) => {
1102                        // Fall through to "command not found"
1103                    }
1104                    Err(e) => {
1105                        return Ok(ExecResult::failure(1, e.to_string()));
1106                    }
1107                }
1108
1109                return Ok(ExecResult::failure(127, format!("command not found: {}", name)));
1110            }
1111        };
1112
1113        // Build arguments (async to support command substitution)
1114        let mut tool_args = self.build_args_async(args).await?;
1115        let output_format = extract_output_format(&mut tool_args, Some(&tool.schema()));
1116
1117        // Execute
1118        let mut ctx = self.exec_ctx.write().await;
1119        {
1120            let scope = self.scope.read().await;
1121            ctx.scope = scope.clone();
1122        }
1123
1124        let result = tool.execute(tool_args, &mut ctx).await;
1125
1126        // Sync scope changes back (e.g., from cd)
1127        {
1128            let mut scope = self.scope.write().await;
1129            *scope = ctx.scope.clone();
1130        }
1131
1132        let result = match output_format {
1133            Some(format) => apply_output_format(result, format),
1134            None => result,
1135        };
1136
1137        Ok(result)
1138    }
1139
1140    /// Build tool arguments from AST args.
1141    ///
1142    /// Uses async evaluation to support command substitution in arguments.
1143    async fn build_args_async(&self, args: &[Arg]) -> Result<ToolArgs> {
1144        let mut tool_args = ToolArgs::new();
1145
1146        for arg in args {
1147            match arg {
1148                Arg::Positional(expr) => {
1149                    let value = self.eval_expr_async(expr).await?;
1150                    // Apply tilde expansion to string values
1151                    let value = apply_tilde_expansion(value);
1152                    tool_args.positional.push(value);
1153                }
1154                Arg::Named { key, value } => {
1155                    let val = self.eval_expr_async(value).await?;
1156                    // Apply tilde expansion to string values
1157                    let val = apply_tilde_expansion(val);
1158                    tool_args.named.insert(key.clone(), val);
1159                }
1160                Arg::ShortFlag(name) => {
1161                    for c in name.chars() {
1162                        tool_args.flags.insert(c.to_string());
1163                    }
1164                }
1165                Arg::LongFlag(name) => {
1166                    tool_args.flags.insert(name.clone());
1167                }
1168                Arg::DoubleDash => {
1169                    // Marker for end of flags - no action needed here,
1170                    // subsequent flags were converted to positional during parsing
1171                }
1172            }
1173        }
1174
1175        Ok(tool_args)
1176    }
1177
1178    /// Build arguments as flat string list for external commands.
1179    ///
1180    /// Unlike `build_args_async` which separates flags into a HashSet (for schema-aware builtins),
1181    /// this preserves the original flag format as strings for external commands:
1182    /// - `-l` stays as `-l`
1183    /// - `--verbose` stays as `--verbose`
1184    /// - `key=value` stays as `key=value`
1185    ///
1186    /// This is what external commands expect in their argv.
1187    async fn build_args_flat(&self, args: &[Arg]) -> Result<Vec<String>> {
1188        let mut argv = Vec::new();
1189        for arg in args {
1190            match arg {
1191                Arg::Positional(expr) => {
1192                    let value = self.eval_expr_async(expr).await?;
1193                    let value = apply_tilde_expansion(value);
1194                    argv.push(value_to_string(&value));
1195                }
1196                Arg::Named { key, value } => {
1197                    let val = self.eval_expr_async(value).await?;
1198                    let val = apply_tilde_expansion(val);
1199                    argv.push(format!("{}={}", key, value_to_string(&val)));
1200                }
1201                Arg::ShortFlag(name) => {
1202                    // Preserve original format: -l, -la (combined flags)
1203                    argv.push(format!("-{}", name));
1204                }
1205                Arg::LongFlag(name) => {
1206                    // Preserve original format: --verbose
1207                    argv.push(format!("--{}", name));
1208                }
1209                Arg::DoubleDash => {
1210                    // Preserve the -- marker
1211                    argv.push("--".to_string());
1212                }
1213            }
1214        }
1215        Ok(argv)
1216    }
1217
1218    /// Async expression evaluator that supports command substitution.
1219    ///
1220    /// This is used for contexts where expressions may contain `$(...)` command
1221    /// substitution. Unlike the sync `eval_expr`, this can execute pipelines.
1222    fn eval_expr_async<'a>(&'a self, expr: &'a Expr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + 'a>> {
1223        Box::pin(async move {
1224        match expr {
1225            Expr::Literal(value) => Ok(value.clone()),
1226            Expr::VarRef(path) => {
1227                let scope = self.scope.read().await;
1228                scope.resolve_path(path)
1229                    .ok_or_else(|| anyhow::anyhow!("undefined variable"))
1230            }
1231            Expr::Interpolated(parts) => {
1232                let mut result = String::new();
1233                for part in parts {
1234                    result.push_str(&self.eval_string_part_async(part).await?);
1235                }
1236                Ok(Value::String(result))
1237            }
1238            Expr::BinaryOp { left, op, right } => {
1239                match op {
1240                    BinaryOp::And => {
1241                        let left_val = self.eval_expr_async(left).await?;
1242                        if !is_truthy(&left_val) {
1243                            return Ok(left_val);
1244                        }
1245                        self.eval_expr_async(right).await
1246                    }
1247                    BinaryOp::Or => {
1248                        let left_val = self.eval_expr_async(left).await?;
1249                        if is_truthy(&left_val) {
1250                            return Ok(left_val);
1251                        }
1252                        self.eval_expr_async(right).await
1253                    }
1254                    _ => {
1255                        // For other operators, fall back to sync eval
1256                        let mut scope = self.scope.write().await;
1257                        eval_expr(expr, &mut scope).map_err(|e| anyhow::anyhow!("{}", e))
1258                    }
1259                }
1260            }
1261            Expr::CommandSubst(pipeline) => {
1262                // Execute the pipeline and return structured data if available
1263                let result = self.execute_pipeline(pipeline).await?;
1264                self.update_last_result(&result).await;
1265                // Prefer structured data (enables `for i in $(cmd)` iteration)
1266                if let Some(data) = &result.data {
1267                    Ok(data.clone())
1268                } else {
1269                    // Otherwise return stdout as single string (NO implicit splitting)
1270                    Ok(Value::String(result.out.trim_end().to_string()))
1271                }
1272            }
1273            Expr::Test(test_expr) => {
1274                // For test expressions, use the sync evaluator
1275                let expr = Expr::Test(test_expr.clone());
1276                let mut scope = self.scope.write().await;
1277                eval_expr(&expr, &mut scope).map_err(|e| anyhow::anyhow!("{}", e))
1278            }
1279            Expr::Positional(n) => {
1280                let scope = self.scope.read().await;
1281                match scope.get_positional(*n) {
1282                    Some(s) => Ok(Value::String(s.to_string())),
1283                    None => Ok(Value::String(String::new())),
1284                }
1285            }
1286            Expr::AllArgs => {
1287                let scope = self.scope.read().await;
1288                Ok(Value::String(scope.all_args().join(" ")))
1289            }
1290            Expr::ArgCount => {
1291                let scope = self.scope.read().await;
1292                Ok(Value::Int(scope.arg_count() as i64))
1293            }
1294            Expr::VarLength(name) => {
1295                let scope = self.scope.read().await;
1296                match scope.get(name) {
1297                    Some(value) => Ok(Value::Int(value_to_string(value).len() as i64)),
1298                    None => Ok(Value::Int(0)),
1299                }
1300            }
1301            Expr::VarWithDefault { name, default } => {
1302                let scope = self.scope.read().await;
1303                let use_default = match scope.get(name) {
1304                    Some(value) => value_to_string(value).is_empty(),
1305                    None => true,
1306                };
1307                drop(scope); // Release the lock before recursive evaluation
1308                if use_default {
1309                    // Evaluate the default parts (supports nested expansions)
1310                    self.eval_string_parts_async(default).await.map(Value::String)
1311                } else {
1312                    let scope = self.scope.read().await;
1313                    scope.get(name).cloned().ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))
1314                }
1315            }
1316            Expr::Arithmetic(expr_str) => {
1317                let scope = self.scope.read().await;
1318                crate::arithmetic::eval_arithmetic(expr_str, &scope)
1319                    .map(Value::Int)
1320                    .map_err(|e| anyhow::anyhow!("arithmetic error: {}", e))
1321            }
1322            Expr::Command(cmd) => {
1323                // Execute command and return boolean based on exit code
1324                let result = self.execute_command(&cmd.name, &cmd.args).await?;
1325                Ok(Value::Bool(result.code == 0))
1326            }
1327            Expr::LastExitCode => {
1328                let scope = self.scope.read().await;
1329                Ok(Value::Int(scope.last_result().code))
1330            }
1331            Expr::CurrentPid => {
1332                let scope = self.scope.read().await;
1333                Ok(Value::Int(scope.pid() as i64))
1334            }
1335        }
1336        })
1337    }
1338
1339    /// Async helper to evaluate multiple StringParts into a single string.
1340    fn eval_string_parts_async<'a>(&'a self, parts: &'a [StringPart]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + 'a>> {
1341        Box::pin(async move {
1342            let mut result = String::new();
1343            for part in parts {
1344                result.push_str(&self.eval_string_part_async(part).await?);
1345            }
1346            Ok(result)
1347        })
1348    }
1349
1350    /// Async helper to evaluate a StringPart.
1351    fn eval_string_part_async<'a>(&'a self, part: &'a StringPart) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + 'a>> {
1352        Box::pin(async move {
1353            match part {
1354                StringPart::Literal(s) => Ok(s.clone()),
1355                StringPart::Var(path) => {
1356                    let scope = self.scope.read().await;
1357                    match scope.resolve_path(path) {
1358                        Some(value) => Ok(value_to_string(&value)),
1359                        None => Ok(String::new()), // Unset vars expand to empty
1360                    }
1361                }
1362                StringPart::VarWithDefault { name, default } => {
1363                    let scope = self.scope.read().await;
1364                    let use_default = match scope.get(name) {
1365                        Some(value) => value_to_string(value).is_empty(),
1366                        None => true,
1367                    };
1368                    drop(scope); // Release lock before recursive evaluation
1369                    if use_default {
1370                        // Evaluate the default parts (supports nested expansions)
1371                        self.eval_string_parts_async(default).await
1372                    } else {
1373                        let scope = self.scope.read().await;
1374                        Ok(value_to_string(scope.get(name).ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))?))
1375                    }
1376                }
1377            StringPart::VarLength(name) => {
1378                let scope = self.scope.read().await;
1379                match scope.get(name) {
1380                    Some(value) => Ok(value_to_string(value).len().to_string()),
1381                    None => Ok("0".to_string()),
1382                }
1383            }
1384            StringPart::Positional(n) => {
1385                let scope = self.scope.read().await;
1386                match scope.get_positional(*n) {
1387                    Some(s) => Ok(s.to_string()),
1388                    None => Ok(String::new()),
1389                }
1390            }
1391            StringPart::AllArgs => {
1392                let scope = self.scope.read().await;
1393                Ok(scope.all_args().join(" "))
1394            }
1395            StringPart::ArgCount => {
1396                let scope = self.scope.read().await;
1397                Ok(scope.arg_count().to_string())
1398            }
1399            StringPart::Arithmetic(expr) => {
1400                let scope = self.scope.read().await;
1401                match crate::arithmetic::eval_arithmetic(expr, &scope) {
1402                    Ok(value) => Ok(value.to_string()),
1403                    Err(_) => Ok(String::new()),
1404                }
1405            }
1406            StringPart::CommandSubst(pipeline) => {
1407                // Execute the pipeline and capture its output
1408                let result = self.execute_pipeline(pipeline).await?;
1409                Ok(result.out.trim_end_matches('\n').to_string())
1410            }
1411            StringPart::LastExitCode => {
1412                let scope = self.scope.read().await;
1413                Ok(scope.last_result().code.to_string())
1414            }
1415            StringPart::CurrentPid => {
1416                let scope = self.scope.read().await;
1417                Ok(scope.pid().to_string())
1418            }
1419        }
1420        })
1421    }
1422
1423    /// Update the last result in scope.
1424    async fn update_last_result(&self, result: &ExecResult) {
1425        let mut scope = self.scope.write().await;
1426        scope.set_last_result(result.clone());
1427    }
1428
1429    /// Execute a user-defined function with local variable scoping.
1430    ///
1431    /// Functions push a new scope frame for local variables. Variables declared
1432    /// with `local` are scoped to the function; other assignments modify outer
1433    /// scopes (or create in root if new).
1434    async fn execute_user_tool(&self, def: ToolDef, args: &[Arg]) -> Result<ExecResult> {
1435        // 1. Build function args from AST args (async to support command substitution)
1436        let tool_args = self.build_args_async(args).await?;
1437
1438        // 2. Push a new scope frame for local variables
1439        {
1440            let mut scope = self.scope.write().await;
1441            scope.push_frame();
1442        }
1443
1444        // 3. Save current positional parameters and set new ones for this function
1445        let saved_positional = {
1446            let mut scope = self.scope.write().await;
1447            let saved = scope.save_positional();
1448
1449            // Set up new positional parameters ($0 = function name, $1, $2, ... = args)
1450            let positional_args: Vec<String> = tool_args.positional
1451                .iter()
1452                .map(value_to_string)
1453                .collect();
1454            scope.set_positional(&def.name, positional_args);
1455
1456            saved
1457        };
1458
1459        // 3. Execute body statements with control flow handling
1460        // Accumulate output across statements (like sh)
1461        let mut accumulated_out = String::new();
1462        let mut accumulated_err = String::new();
1463        let mut last_code = 0i64;
1464        let mut last_data: Option<Value> = None;
1465
1466        for stmt in &def.body {
1467            match self.execute_stmt_flow(stmt).await {
1468                Ok(flow) => {
1469                    match flow {
1470                        ControlFlow::Normal(r) => {
1471                            accumulated_out.push_str(&r.out);
1472                            accumulated_err.push_str(&r.err);
1473                            last_code = r.code;
1474                            last_data = r.data;
1475                        }
1476                        ControlFlow::Return { value } => {
1477                            // Return from this function with the value
1478                            accumulated_out.push_str(&value.out);
1479                            accumulated_err.push_str(&value.err);
1480                            last_code = value.code;
1481                            last_data = value.data;
1482                            break;
1483                        }
1484                        ControlFlow::Exit { code } => {
1485                            // Exit propagates through - pop frame, restore positional params and return
1486                            let mut scope = self.scope.write().await;
1487                            scope.pop_frame();
1488                            scope.set_positional(saved_positional.0.clone(), saved_positional.1.clone());
1489                            return Ok(ExecResult::failure(code, "exit"));
1490                        }
1491                        ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
1492                            // Break/continue outside a loop - treat as normal
1493                            accumulated_out.push_str(&r.out);
1494                            accumulated_err.push_str(&r.err);
1495                            last_code = r.code;
1496                            last_data = r.data;
1497                        }
1498                    }
1499                }
1500                Err(e) => {
1501                    // Pop frame and restore positional params on error
1502                    let mut scope = self.scope.write().await;
1503                    scope.pop_frame();
1504                    scope.set_positional(saved_positional.0.clone(), saved_positional.1.clone());
1505                    return Err(e);
1506                }
1507            }
1508        }
1509
1510        let result = ExecResult {
1511            code: last_code,
1512            out: accumulated_out,
1513            err: accumulated_err,
1514            data: last_data,
1515            output: None,
1516        };
1517
1518        // 4. Pop scope frame and restore original positional parameters
1519        {
1520            let mut scope = self.scope.write().await;
1521            scope.pop_frame();
1522            scope.set_positional(saved_positional.0, saved_positional.1);
1523        }
1524
1525        // 5. Return final result
1526        Ok(result)
1527    }
1528
1529    /// Execute the `source` / `.` command to include and run a script.
1530    ///
1531    /// Unlike regular tool execution, `source` executes in the CURRENT scope,
1532    /// allowing the sourced script to set variables and modify shell state.
1533    async fn execute_source(&self, args: &[Arg]) -> Result<ExecResult> {
1534        // Get the file path from the first positional argument
1535        let tool_args = self.build_args_async(args).await?;
1536        let path = match tool_args.positional.first() {
1537            Some(Value::String(s)) => s.clone(),
1538            Some(v) => value_to_string(v),
1539            None => {
1540                return Ok(ExecResult::failure(1, "source: missing filename"));
1541            }
1542        };
1543
1544        // Resolve path relative to cwd
1545        let full_path = {
1546            let ctx = self.exec_ctx.read().await;
1547            if path.starts_with('/') {
1548                std::path::PathBuf::from(&path)
1549            } else {
1550                ctx.cwd.join(&path)
1551            }
1552        };
1553
1554        // Read file content via backend
1555        let content = {
1556            let ctx = self.exec_ctx.read().await;
1557            match ctx.backend.read(&full_path, None).await {
1558                Ok(bytes) => {
1559                    String::from_utf8(bytes).map_err(|e| {
1560                        anyhow::anyhow!("source: {}: invalid UTF-8: {}", path, e)
1561                    })?
1562                }
1563                Err(e) => {
1564                    return Ok(ExecResult::failure(
1565                        1,
1566                        format!("source: {}: {}", path, e),
1567                    ));
1568                }
1569            }
1570        };
1571
1572        // Parse the content
1573        let program = match crate::parser::parse(&content) {
1574            Ok(p) => p,
1575            Err(errors) => {
1576                let msg = errors
1577                    .iter()
1578                    .map(|e| format!("{}:{}: {}", path, e.span.start, e.message))
1579                    .collect::<Vec<_>>()
1580                    .join("\n");
1581                return Ok(ExecResult::failure(1, format!("source: {}", msg)));
1582            }
1583        };
1584
1585        // Execute each statement in the CURRENT scope (not isolated)
1586        let mut result = ExecResult::success("");
1587        for stmt in program.statements {
1588            if matches!(stmt, crate::ast::Stmt::Empty) {
1589                continue;
1590            }
1591
1592            match self.execute_stmt_flow(&stmt).await {
1593                Ok(flow) => {
1594                    match flow {
1595                        ControlFlow::Normal(r) => {
1596                            result = r.clone();
1597                            self.update_last_result(&r).await;
1598                        }
1599                        ControlFlow::Break { .. } | ControlFlow::Continue { .. } => {
1600                            // break/continue in sourced file - unusual but propagate
1601                            return Err(anyhow::anyhow!(
1602                                "source: {}: unexpected break/continue outside loop",
1603                                path
1604                            ));
1605                        }
1606                        ControlFlow::Return { value } => {
1607                            // Return from sourced script ends the source
1608                            return Ok(value);
1609                        }
1610                        ControlFlow::Exit { code } => {
1611                            // Exit from sourced script propagates
1612                            return Ok(ExecResult::failure(code, "exit"));
1613                        }
1614                    }
1615                }
1616                Err(e) => {
1617                    return Err(e.context(format!("source: {}", path)));
1618                }
1619            }
1620        }
1621
1622        Ok(result)
1623    }
1624
1625    /// Try to execute a script from PATH directories.
1626    ///
1627    /// Searches PATH for `{name}.kai` files and executes them in isolated scope
1628    /// (like user-defined tools). Returns None if no script is found.
1629    async fn try_execute_script(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
1630        // Get PATH from scope (default to "/bin")
1631        let path_value = {
1632            let scope = self.scope.read().await;
1633            scope
1634                .get("PATH")
1635                .map(value_to_string)
1636                .unwrap_or_else(|| "/bin".to_string())
1637        };
1638
1639        // Search PATH directories for script
1640        for dir in path_value.split(':') {
1641            if dir.is_empty() {
1642                continue;
1643            }
1644
1645            // Build script path: {dir}/{name}.kai
1646            let script_path = PathBuf::from(dir).join(format!("{}.kai", name));
1647
1648            // Check if script exists
1649            let exists = {
1650                let ctx = self.exec_ctx.read().await;
1651                ctx.backend.exists(&script_path).await
1652            };
1653
1654            if !exists {
1655                continue;
1656            }
1657
1658            // Read script content
1659            let content = {
1660                let ctx = self.exec_ctx.read().await;
1661                match ctx.backend.read(&script_path, None).await {
1662                    Ok(bytes) => match String::from_utf8(bytes) {
1663                        Ok(s) => s,
1664                        Err(e) => {
1665                            return Ok(Some(ExecResult::failure(
1666                                1,
1667                                format!("{}: invalid UTF-8: {}", script_path.display(), e),
1668                            )));
1669                        }
1670                    },
1671                    Err(e) => {
1672                        return Ok(Some(ExecResult::failure(
1673                            1,
1674                            format!("{}: {}", script_path.display(), e),
1675                        )));
1676                    }
1677                }
1678            };
1679
1680            // Parse the script
1681            let program = match crate::parser::parse(&content) {
1682                Ok(p) => p,
1683                Err(errors) => {
1684                    let msg = errors
1685                        .iter()
1686                        .map(|e| format!("{}:{}: {}", script_path.display(), e.span.start, e.message))
1687                        .collect::<Vec<_>>()
1688                        .join("\n");
1689                    return Ok(Some(ExecResult::failure(1, msg)));
1690                }
1691            };
1692
1693            // Build tool_args from args (async for command substitution support)
1694            let tool_args = self.build_args_async(args).await?;
1695
1696            // Create isolated scope (like user tools)
1697            let mut isolated_scope = Scope::new();
1698
1699            // Set up positional parameters ($0 = script name, $1, $2, ... = args)
1700            let positional_args: Vec<String> = tool_args.positional
1701                .iter()
1702                .map(value_to_string)
1703                .collect();
1704            isolated_scope.set_positional(name, positional_args);
1705
1706            // Save current scope and swap with isolated scope
1707            let original_scope = {
1708                let mut scope = self.scope.write().await;
1709                std::mem::replace(&mut *scope, isolated_scope)
1710            };
1711
1712            // Execute script statements
1713            let mut result = ExecResult::success("");
1714            for stmt in program.statements {
1715                if matches!(stmt, crate::ast::Stmt::Empty) {
1716                    continue;
1717                }
1718
1719                match self.execute_stmt_flow(&stmt).await {
1720                    Ok(flow) => {
1721                        match flow {
1722                            ControlFlow::Normal(r) => result = r,
1723                            ControlFlow::Return { value } => {
1724                                result = value;
1725                                break;
1726                            }
1727                            ControlFlow::Exit { code } => {
1728                                // Restore scope and return
1729                                let mut scope = self.scope.write().await;
1730                                *scope = original_scope;
1731                                return Ok(Some(ExecResult::failure(code, "exit")));
1732                            }
1733                            ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
1734                                result = r;
1735                            }
1736                        }
1737                    }
1738                    Err(e) => {
1739                        // Restore original scope on error
1740                        let mut scope = self.scope.write().await;
1741                        *scope = original_scope;
1742                        return Err(e.context(format!("script: {}", script_path.display())));
1743                    }
1744                }
1745            }
1746
1747            // Restore original scope
1748            {
1749                let mut scope = self.scope.write().await;
1750                *scope = original_scope;
1751            }
1752
1753            return Ok(Some(result));
1754        }
1755
1756        // No script found
1757        Ok(None)
1758    }
1759
1760    /// Try to execute an external command from PATH.
1761    ///
1762    /// This is the fallback when no builtin or user-defined tool matches.
1763    /// External commands receive a clean argv (flags preserved in their original format).
1764    ///
1765    /// # Requirements
1766    /// - Command must be found in PATH
1767    /// - Current working directory must be on a real filesystem (not virtual like /scratch)
1768    ///
1769    /// # Returns
1770    /// - `Ok(Some(result))` if command was found and executed
1771    /// - `Ok(None)` if command was not found in PATH
1772    /// - `Err` on execution errors
1773    async fn try_execute_external(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
1774        // Skip if name contains path separator (absolute/relative paths handled differently)
1775        if name.contains('/') {
1776            return Ok(None);
1777        }
1778
1779        // Get PATH from scope or environment
1780        let path_var = {
1781            let scope = self.scope.read().await;
1782            scope
1783                .get("PATH")
1784                .map(value_to_string)
1785                .unwrap_or_else(|| std::env::var("PATH").unwrap_or_default())
1786        };
1787
1788        // Resolve command in PATH
1789        let executable = match resolve_in_path(name, &path_var) {
1790            Some(path) => path,
1791            None => return Ok(None), // Not found - let caller handle error
1792        };
1793
1794        // Get current working directory and verify it's on real filesystem
1795        let real_cwd = {
1796            let ctx = self.exec_ctx.read().await;
1797            match ctx.backend.resolve_real_path(&ctx.cwd) {
1798                Some(p) => p,
1799                None => {
1800                    return Ok(Some(ExecResult::failure(
1801                        1,
1802                        format!(
1803                            "{}: cannot run external command from virtual directory '{}'",
1804                            name,
1805                            ctx.cwd.display()
1806                        ),
1807                    )));
1808                }
1809            }
1810        };
1811
1812        // Build flat argv (preserves flag format)
1813        let argv = self.build_args_flat(args).await?;
1814
1815        // Get stdin if available
1816        let stdin_data = {
1817            let mut ctx = self.exec_ctx.write().await;
1818            ctx.take_stdin()
1819        };
1820
1821        // Build and spawn the command
1822        use tokio::process::Command;
1823
1824        let mut cmd = Command::new(&executable);
1825        cmd.args(&argv);
1826        cmd.current_dir(&real_cwd);
1827
1828        // Handle stdin
1829        cmd.stdin(if stdin_data.is_some() {
1830            std::process::Stdio::piped()
1831        } else {
1832            std::process::Stdio::null()
1833        });
1834        cmd.stdout(std::process::Stdio::piped());
1835        cmd.stderr(std::process::Stdio::piped());
1836
1837        // Spawn the process
1838        let mut child = match cmd.spawn() {
1839            Ok(child) => child,
1840            Err(e) => {
1841                return Ok(Some(ExecResult::failure(
1842                    127,
1843                    format!("{}: {}", name, e),
1844                )));
1845            }
1846        };
1847
1848        // Write stdin if present
1849        if let Some(data) = stdin_data
1850            && let Some(mut stdin) = child.stdin.take()
1851        {
1852            use tokio::io::AsyncWriteExt;
1853            if let Err(e) = stdin.write_all(data.as_bytes()).await {
1854                return Ok(Some(ExecResult::failure(
1855                    1,
1856                    format!("{}: failed to write stdin: {}", name, e),
1857                )));
1858            }
1859            // Drop stdin to signal EOF
1860        }
1861
1862        // Create bounded streams for output capture (prevents OOM from large output)
1863        let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
1864        let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
1865
1866        // Take pipes and spawn drain tasks
1867        let stdout_pipe = child.stdout.take();
1868        let stderr_pipe = child.stderr.take();
1869
1870        let stdout_clone = stdout_stream.clone();
1871        let stderr_clone = stderr_stream.clone();
1872
1873        let stdout_task = stdout_pipe.map(|pipe| {
1874            tokio::spawn(async move {
1875                drain_to_stream(pipe, stdout_clone).await;
1876            })
1877        });
1878
1879        let stderr_task = stderr_pipe.map(|pipe| {
1880            tokio::spawn(async move {
1881                drain_to_stream(pipe, stderr_clone).await;
1882            })
1883        });
1884
1885        // Wait for process to exit
1886        let status = match child.wait().await {
1887            Ok(s) => s,
1888            Err(e) => {
1889                return Ok(Some(ExecResult::failure(
1890                    1,
1891                    format!("{}: failed to wait: {}", name, e),
1892                )));
1893            }
1894        };
1895
1896        // Wait for drain tasks to complete
1897        if let Some(task) = stdout_task {
1898            // Ignore join error - the drain task logs its own errors
1899            let _ = task.await;
1900        }
1901        if let Some(task) = stderr_task {
1902            let _ = task.await;
1903        }
1904
1905        // Extract exit code
1906        let code = status.code().unwrap_or_else(|| {
1907            // Process was killed by signal
1908            #[cfg(unix)]
1909            {
1910                use std::os::unix::process::ExitStatusExt;
1911                128 + status.signal().unwrap_or(0)
1912            }
1913            #[cfg(not(unix))]
1914            {
1915                -1
1916            }
1917        }) as i64;
1918
1919        // Read final output from streams
1920        let stdout = stdout_stream.read_string().await;
1921        let stderr = stderr_stream.read_string().await;
1922
1923        Ok(Some(ExecResult::from_output(code, stdout, stderr)))
1924    }
1925
1926    // --- Variable Access ---
1927
1928    /// Get a variable value.
1929    pub async fn get_var(&self, name: &str) -> Option<Value> {
1930        let scope = self.scope.read().await;
1931        scope.get(name).cloned()
1932    }
1933
1934    /// Check if error-exit mode is enabled (for testing).
1935    #[cfg(test)]
1936    pub async fn error_exit_enabled(&self) -> bool {
1937        let scope = self.scope.read().await;
1938        scope.error_exit_enabled()
1939    }
1940
1941    /// Set a variable value.
1942    pub async fn set_var(&self, name: &str, value: Value) {
1943        let mut scope = self.scope.write().await;
1944        scope.set(name.to_string(), value);
1945    }
1946
1947    /// List all variables.
1948    pub async fn list_vars(&self) -> Vec<(String, Value)> {
1949        let scope = self.scope.read().await;
1950        scope.all()
1951    }
1952
1953    // --- CWD ---
1954
1955    /// Get current working directory.
1956    pub async fn cwd(&self) -> PathBuf {
1957        self.exec_ctx.read().await.cwd.clone()
1958    }
1959
1960    /// Set current working directory.
1961    pub async fn set_cwd(&self, path: PathBuf) {
1962        let mut ctx = self.exec_ctx.write().await;
1963        ctx.set_cwd(path);
1964    }
1965
1966    // --- Last Result ---
1967
1968    /// Get the last result ($?).
1969    pub async fn last_result(&self) -> ExecResult {
1970        let scope = self.scope.read().await;
1971        scope.last_result().clone()
1972    }
1973
1974    // --- Tools ---
1975
1976    /// Get available tool schemas.
1977    pub fn tool_schemas(&self) -> Vec<crate::tools::ToolSchema> {
1978        self.tools.schemas()
1979    }
1980
1981    // --- Jobs ---
1982
1983    /// Get job manager.
1984    pub fn jobs(&self) -> Arc<JobManager> {
1985        self.jobs.clone()
1986    }
1987
1988    // --- VFS ---
1989
1990    /// Get VFS router.
1991    pub fn vfs(&self) -> Arc<VfsRouter> {
1992        self.vfs.clone()
1993    }
1994
1995    // --- State ---
1996
1997    /// Reset kernel to initial state.
1998    ///
1999    /// Clears in-memory variables and resets cwd to root.
2000    /// History is not cleared (it persists across resets).
2001    pub async fn reset(&self) -> Result<()> {
2002        {
2003            let mut scope = self.scope.write().await;
2004            *scope = Scope::new();
2005        }
2006        {
2007            let mut ctx = self.exec_ctx.write().await;
2008            ctx.cwd = PathBuf::from("/");
2009        }
2010        Ok(())
2011    }
2012
2013    /// Shutdown the kernel.
2014    pub async fn shutdown(self) -> Result<()> {
2015        // Wait for all background jobs
2016        self.jobs.wait_all().await;
2017        Ok(())
2018    }
2019}
2020
2021/// Accumulate output from one result into another.
2022///
2023/// This appends stdout and stderr (with newlines as separators) and updates
2024/// the exit code to match the new result. Used to preserve output from
2025/// multiple statements, loop iterations, and command chains.
2026fn accumulate_result(accumulated: &mut ExecResult, new: &ExecResult) {
2027    if !accumulated.out.is_empty() && !new.out.is_empty() {
2028        accumulated.out.push('\n');
2029    }
2030    accumulated.out.push_str(&new.out);
2031    if !accumulated.err.is_empty() && !new.err.is_empty() {
2032        accumulated.err.push('\n');
2033    }
2034    accumulated.err.push_str(&new.err);
2035    accumulated.code = new.code;
2036    accumulated.data = new.data.clone();
2037}
2038
2039/// Check if a value is truthy.
2040fn is_truthy(value: &Value) -> bool {
2041    match value {
2042        Value::Null => false,
2043        Value::Bool(b) => *b,
2044        Value::Int(i) => *i != 0,
2045        Value::Float(f) => *f != 0.0,
2046        Value::String(s) => !s.is_empty(),
2047        Value::Json(json) => match json {
2048            serde_json::Value::Null => false,
2049            serde_json::Value::Array(arr) => !arr.is_empty(),
2050            serde_json::Value::Object(obj) => !obj.is_empty(),
2051            serde_json::Value::Bool(b) => *b,
2052            serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
2053            serde_json::Value::String(s) => !s.is_empty(),
2054        },
2055        Value::Blob(_) => true, // Blob references are always truthy
2056    }
2057}
2058
2059/// Apply tilde expansion to a value.
2060///
2061/// Only string values starting with `~` are expanded.
2062fn apply_tilde_expansion(value: Value) -> Value {
2063    match value {
2064        Value::String(s) if s.starts_with('~') => Value::String(expand_tilde(&s)),
2065        _ => value,
2066    }
2067}
2068
2069#[cfg(test)]
2070mod tests {
2071    use super::*;
2072
2073    #[tokio::test]
2074    async fn test_kernel_transient() {
2075        let kernel = Kernel::transient().expect("failed to create kernel");
2076        assert_eq!(kernel.name(), "transient");
2077    }
2078
2079    #[tokio::test]
2080    async fn test_kernel_execute_echo() {
2081        let kernel = Kernel::transient().expect("failed to create kernel");
2082        let result = kernel.execute("echo hello").await.expect("execution failed");
2083        assert!(result.ok());
2084        assert_eq!(result.out.trim(), "hello");
2085    }
2086
2087    #[tokio::test]
2088    async fn test_multiple_statements_accumulate_output() {
2089        let kernel = Kernel::transient().expect("failed to create kernel");
2090        let result = kernel
2091            .execute("echo one\necho two\necho three")
2092            .await
2093            .expect("execution failed");
2094        assert!(result.ok());
2095        // Should have all three outputs separated by newlines
2096        assert!(result.out.contains("one"), "missing 'one': {}", result.out);
2097        assert!(result.out.contains("two"), "missing 'two': {}", result.out);
2098        assert!(result.out.contains("three"), "missing 'three': {}", result.out);
2099    }
2100
2101    #[tokio::test]
2102    async fn test_and_chain_accumulates_output() {
2103        let kernel = Kernel::transient().expect("failed to create kernel");
2104        let result = kernel
2105            .execute("echo first && echo second")
2106            .await
2107            .expect("execution failed");
2108        assert!(result.ok());
2109        assert!(result.out.contains("first"), "missing 'first': {}", result.out);
2110        assert!(result.out.contains("second"), "missing 'second': {}", result.out);
2111    }
2112
2113    #[tokio::test]
2114    async fn test_for_loop_accumulates_output() {
2115        let kernel = Kernel::transient().expect("failed to create kernel");
2116        let result = kernel
2117            .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
2118            .await
2119            .expect("execution failed");
2120        assert!(result.ok());
2121        assert!(result.out.contains("item: a"), "missing 'item: a': {}", result.out);
2122        assert!(result.out.contains("item: b"), "missing 'item: b': {}", result.out);
2123        assert!(result.out.contains("item: c"), "missing 'item: c': {}", result.out);
2124    }
2125
2126    #[tokio::test]
2127    async fn test_while_loop_accumulates_output() {
2128        let kernel = Kernel::transient().expect("failed to create kernel");
2129        let result = kernel
2130            .execute(r#"
2131                N=3
2132                while [[ ${N} -gt 0 ]]; do
2133                    echo "N=${N}"
2134                    N=$((N - 1))
2135                done
2136            "#)
2137            .await
2138            .expect("execution failed");
2139        assert!(result.ok());
2140        assert!(result.out.contains("N=3"), "missing 'N=3': {}", result.out);
2141        assert!(result.out.contains("N=2"), "missing 'N=2': {}", result.out);
2142        assert!(result.out.contains("N=1"), "missing 'N=1': {}", result.out);
2143    }
2144
2145    #[tokio::test]
2146    async fn test_kernel_set_var() {
2147        let kernel = Kernel::transient().expect("failed to create kernel");
2148
2149        kernel.execute("X=42").await.expect("set failed");
2150
2151        let value = kernel.get_var("X").await;
2152        assert_eq!(value, Some(Value::Int(42)));
2153    }
2154
2155    #[tokio::test]
2156    async fn test_kernel_var_expansion() {
2157        let kernel = Kernel::transient().expect("failed to create kernel");
2158
2159        kernel.execute("NAME=\"world\"").await.expect("set failed");
2160        let result = kernel.execute("echo \"hello ${NAME}\"").await.expect("echo failed");
2161
2162        assert!(result.ok());
2163        assert_eq!(result.out.trim(), "hello world");
2164    }
2165
2166    #[tokio::test]
2167    async fn test_kernel_last_result() {
2168        let kernel = Kernel::transient().expect("failed to create kernel");
2169
2170        kernel.execute("echo test").await.expect("echo failed");
2171
2172        let last = kernel.last_result().await;
2173        assert!(last.ok());
2174        assert_eq!(last.out.trim(), "test");
2175    }
2176
2177    #[tokio::test]
2178    async fn test_kernel_tool_not_found() {
2179        let kernel = Kernel::transient().expect("failed to create kernel");
2180
2181        let result = kernel.execute("nonexistent_tool").await.expect("execution failed");
2182        assert!(!result.ok());
2183        assert_eq!(result.code, 127);
2184        assert!(result.err.contains("command not found"));
2185    }
2186
2187    #[tokio::test]
2188    async fn test_external_command_true() {
2189        // Use REPL config for passthrough filesystem access
2190        let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
2191
2192        // /bin/true should be available on any Unix system
2193        let result = kernel.execute("true").await.expect("execution failed");
2194        // This should use the builtin true, which returns 0
2195        assert!(result.ok(), "true should succeed: {:?}", result);
2196    }
2197
2198    #[tokio::test]
2199    async fn test_external_command_basic() {
2200        // Use REPL config for passthrough filesystem access
2201        let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
2202
2203        // Test with /bin/echo which is external
2204        // Note: kaish has a builtin echo, so this will use the builtin
2205        // Let's test with a command that's not a builtin
2206        // Actually, let's just test that PATH resolution works by checking the PATH var
2207        let path_var = std::env::var("PATH").unwrap_or_default();
2208        eprintln!("System PATH: {}", path_var);
2209
2210        // Set PATH in kernel to ensure it's available
2211        kernel.execute(&format!(r#"PATH="{}""#, path_var)).await.expect("set PATH failed");
2212
2213        // Now try an external command like /usr/bin/env
2214        // But env is also a builtin... let's try uname
2215        let result = kernel.execute("uname").await.expect("execution failed");
2216        eprintln!("uname result: {:?}", result);
2217        // uname should succeed if external commands work
2218        assert!(result.ok() || result.code == 127, "uname: {:?}", result);
2219    }
2220
2221    #[tokio::test]
2222    async fn test_kernel_reset() {
2223        let kernel = Kernel::transient().expect("failed to create kernel");
2224
2225        kernel.execute("X=1").await.expect("set failed");
2226        assert!(kernel.get_var("X").await.is_some());
2227
2228        kernel.reset().await.expect("reset failed");
2229        assert!(kernel.get_var("X").await.is_none());
2230    }
2231
2232    #[tokio::test]
2233    async fn test_kernel_cwd() {
2234        let kernel = Kernel::transient().expect("failed to create kernel");
2235
2236        // Transient kernel uses sandboxed mode with cwd=$HOME
2237        let cwd = kernel.cwd().await;
2238        let home = std::env::var("HOME")
2239            .map(PathBuf::from)
2240            .unwrap_or_else(|_| PathBuf::from("/"));
2241        assert_eq!(cwd, home);
2242
2243        kernel.set_cwd(PathBuf::from("/tmp")).await;
2244        assert_eq!(kernel.cwd().await, PathBuf::from("/tmp"));
2245    }
2246
2247    #[tokio::test]
2248    async fn test_kernel_list_vars() {
2249        let kernel = Kernel::transient().expect("failed to create kernel");
2250
2251        kernel.execute("A=1").await.ok();
2252        kernel.execute("B=2").await.ok();
2253
2254        let vars = kernel.list_vars().await;
2255        assert!(vars.iter().any(|(n, v)| n == "A" && *v == Value::Int(1)));
2256        assert!(vars.iter().any(|(n, v)| n == "B" && *v == Value::Int(2)));
2257    }
2258
2259    #[tokio::test]
2260    async fn test_is_truthy() {
2261        assert!(!is_truthy(&Value::Null));
2262        assert!(!is_truthy(&Value::Bool(false)));
2263        assert!(is_truthy(&Value::Bool(true)));
2264        assert!(!is_truthy(&Value::Int(0)));
2265        assert!(is_truthy(&Value::Int(1)));
2266        assert!(!is_truthy(&Value::String("".into())));
2267        assert!(is_truthy(&Value::String("x".into())));
2268    }
2269
2270    #[tokio::test]
2271    async fn test_jq_in_pipeline() {
2272        let kernel = Kernel::transient().expect("failed to create kernel");
2273        // kaish uses double quotes only; escape inner quotes
2274        let result = kernel
2275            .execute(r#"echo "{\"name\": \"Alice\"}" | jq ".name" -r"#)
2276            .await
2277            .expect("execution failed");
2278        assert!(result.ok(), "jq pipeline failed: {}", result.err);
2279        assert_eq!(result.out.trim(), "Alice");
2280    }
2281
2282    #[tokio::test]
2283    async fn test_user_defined_tool() {
2284        let kernel = Kernel::transient().expect("failed to create kernel");
2285
2286        // Define a function
2287        kernel
2288            .execute(r#"greet() { echo "Hello, $1!" }"#)
2289            .await
2290            .expect("function definition failed");
2291
2292        // Call the function
2293        let result = kernel
2294            .execute(r#"greet "World""#)
2295            .await
2296            .expect("function call failed");
2297
2298        assert!(result.ok(), "greet failed: {}", result.err);
2299        assert_eq!(result.out.trim(), "Hello, World!");
2300    }
2301
2302    #[tokio::test]
2303    async fn test_user_tool_positional_args() {
2304        let kernel = Kernel::transient().expect("failed to create kernel");
2305
2306        // Define a function with positional param
2307        kernel
2308            .execute(r#"greet() { echo "Hi $1" }"#)
2309            .await
2310            .expect("function definition failed");
2311
2312        // Call with positional argument
2313        let result = kernel
2314            .execute(r#"greet "Amy""#)
2315            .await
2316            .expect("function call failed");
2317
2318        assert!(result.ok(), "greet failed: {}", result.err);
2319        assert_eq!(result.out.trim(), "Hi Amy");
2320    }
2321
2322    #[tokio::test]
2323    async fn test_function_shared_scope() {
2324        let kernel = Kernel::transient().expect("failed to create kernel");
2325
2326        // Set a variable in parent scope
2327        kernel
2328            .execute(r#"SECRET="hidden""#)
2329            .await
2330            .expect("set failed");
2331
2332        // Define a function that accesses and modifies parent variable
2333        kernel
2334            .execute(r#"access_parent() {
2335                echo "${SECRET}"
2336                SECRET="modified"
2337            }"#)
2338            .await
2339            .expect("function definition failed");
2340
2341        // Call the function - it SHOULD see SECRET (shared scope like sh)
2342        let result = kernel.execute("access_parent").await.expect("function call failed");
2343
2344        // Function should have access to parent scope
2345        assert!(
2346            result.out.contains("hidden"),
2347            "Function should access parent scope, got: {}",
2348            result.out
2349        );
2350
2351        // Function should have modified the parent variable
2352        let secret = kernel.get_var("SECRET").await;
2353        assert_eq!(
2354            secret,
2355            Some(Value::String("modified".into())),
2356            "Function should modify parent scope"
2357        );
2358    }
2359
2360    #[tokio::test]
2361    async fn test_exec_builtin() {
2362        let kernel = Kernel::transient().expect("failed to create kernel");
2363        // argv is now a space-separated string or JSON array string
2364        let result = kernel
2365            .execute(r#"exec command="/bin/echo" argv="hello world""#)
2366            .await
2367            .expect("exec failed");
2368
2369        assert!(result.ok(), "exec failed: {}", result.err);
2370        assert_eq!(result.out.trim(), "hello world");
2371    }
2372
2373    #[tokio::test]
2374    async fn test_while_false_never_runs() {
2375        let kernel = Kernel::transient().expect("failed to create kernel");
2376
2377        // A while loop with false condition should never run
2378        let result = kernel
2379            .execute(r#"
2380                while false; do
2381                    echo "should not run"
2382                done
2383            "#)
2384            .await
2385            .expect("while false failed");
2386
2387        assert!(result.ok());
2388        assert!(result.out.is_empty(), "while false should not execute body: {}", result.out);
2389    }
2390
2391    #[tokio::test]
2392    async fn test_while_string_comparison() {
2393        let kernel = Kernel::transient().expect("failed to create kernel");
2394
2395        // Set a flag
2396        kernel.execute(r#"FLAG="go""#).await.expect("set failed");
2397
2398        // Use string comparison as condition (shell-compatible [[ ]] syntax)
2399        // Note: Put echo last so we can check the output
2400        let result = kernel
2401            .execute(r#"
2402                while [[ ${FLAG} == "go" ]]; do
2403                    FLAG="stop"
2404                    echo "running"
2405                done
2406            "#)
2407            .await
2408            .expect("while with string cmp failed");
2409
2410        assert!(result.ok());
2411        assert!(result.out.contains("running"), "should have run once: {}", result.out);
2412
2413        // Verify flag was changed
2414        let flag = kernel.get_var("FLAG").await;
2415        assert_eq!(flag, Some(Value::String("stop".into())));
2416    }
2417
2418    #[tokio::test]
2419    async fn test_while_numeric_comparison() {
2420        let kernel = Kernel::transient().expect("failed to create kernel");
2421
2422        // Test > comparison (shell-compatible [[ ]] with -gt)
2423        kernel.execute("N=5").await.expect("set failed");
2424
2425        // Note: Put echo last so we can check the output
2426        let result = kernel
2427            .execute(r#"
2428                while [[ ${N} -gt 3 ]]; do
2429                    N=3
2430                    echo "N was greater"
2431                done
2432            "#)
2433            .await
2434            .expect("while with > failed");
2435
2436        assert!(result.ok());
2437        assert!(result.out.contains("N was greater"), "should have run once: {}", result.out);
2438    }
2439
2440    #[tokio::test]
2441    async fn test_break_in_while_loop() {
2442        let kernel = Kernel::transient().expect("failed to create kernel");
2443
2444        let result = kernel
2445            .execute(r#"
2446                I=0
2447                while true; do
2448                    I=1
2449                    echo "before break"
2450                    break
2451                    echo "after break"
2452                done
2453            "#)
2454            .await
2455            .expect("while with break failed");
2456
2457        assert!(result.ok());
2458        assert!(result.out.contains("before break"), "should see before break: {}", result.out);
2459        assert!(!result.out.contains("after break"), "should not see after break: {}", result.out);
2460
2461        // Verify we exited the loop
2462        let i = kernel.get_var("I").await;
2463        assert_eq!(i, Some(Value::Int(1)));
2464    }
2465
2466    #[tokio::test]
2467    async fn test_continue_in_while_loop() {
2468        let kernel = Kernel::transient().expect("failed to create kernel");
2469
2470        // Test continue in a while loop where variables persist
2471        // We use string state transition: "start" -> "middle" -> "end"
2472        // continue on "middle" should skip to next iteration
2473        // Shell-compatible: use [[ ]] for comparisons
2474        let result = kernel
2475            .execute(r#"
2476                STATE="start"
2477                AFTER_CONTINUE="no"
2478                while [[ ${STATE} != "done" ]]; do
2479                    if [[ ${STATE} == "start" ]]; then
2480                        STATE="middle"
2481                        continue
2482                        AFTER_CONTINUE="yes"
2483                    fi
2484                    if [[ ${STATE} == "middle" ]]; then
2485                        STATE="done"
2486                    fi
2487                done
2488            "#)
2489            .await
2490            .expect("while with continue failed");
2491
2492        assert!(result.ok());
2493
2494        // STATE should be "done" (we completed the loop)
2495        let state = kernel.get_var("STATE").await;
2496        assert_eq!(state, Some(Value::String("done".into())));
2497
2498        // AFTER_CONTINUE should still be "no" (continue skipped the assignment)
2499        let after = kernel.get_var("AFTER_CONTINUE").await;
2500        assert_eq!(after, Some(Value::String("no".into())));
2501    }
2502
2503    #[tokio::test]
2504    async fn test_break_with_level() {
2505        let kernel = Kernel::transient().expect("failed to create kernel");
2506
2507        // Nested loop with break 2 to exit both loops
2508        // We verify by checking OUTER value:
2509        // - If break 2 works, OUTER stays at 1 (set before for loop)
2510        // - If break 2 fails, OUTER becomes 2 (set after for loop)
2511        let result = kernel
2512            .execute(r#"
2513                OUTER=0
2514                while true; do
2515                    OUTER=1
2516                    for X in "1 2"; do
2517                        break 2
2518                    done
2519                    OUTER=2
2520                done
2521            "#)
2522            .await
2523            .expect("nested break failed");
2524
2525        assert!(result.ok());
2526
2527        // OUTER should be 1 (set before for loop), not 2 (would be set after for loop)
2528        let outer = kernel.get_var("OUTER").await;
2529        assert_eq!(outer, Some(Value::Int(1)), "break 2 should have skipped OUTER=2");
2530    }
2531
2532    #[tokio::test]
2533    async fn test_return_from_tool() {
2534        let kernel = Kernel::transient().expect("failed to create kernel");
2535
2536        // Define a function that returns early
2537        kernel
2538            .execute(r#"early_return() {
2539                if [[ $1 == 1 ]]; then
2540                    return 42
2541                fi
2542                echo "not returned"
2543            }"#)
2544            .await
2545            .expect("function definition failed");
2546
2547        // Call with arg=1 should return with exit code 42
2548        // (POSIX shell behavior: return N sets exit code, doesn't output N)
2549        let result = kernel
2550            .execute("early_return 1")
2551            .await
2552            .expect("function call failed");
2553
2554        // Exit code should be 42 (non-zero, so not ok())
2555        assert_eq!(result.code, 42);
2556        // Output should be empty (we returned before echo)
2557        assert!(result.out.is_empty());
2558    }
2559
2560    #[tokio::test]
2561    async fn test_return_without_value() {
2562        let kernel = Kernel::transient().expect("failed to create kernel");
2563
2564        // Define a function that returns without a value
2565        kernel
2566            .execute(r#"early_exit() {
2567                if [[ $1 == "stop" ]]; then
2568                    return
2569                fi
2570                echo "continued"
2571            }"#)
2572            .await
2573            .expect("function definition failed");
2574
2575        // Call with arg="stop" should return early
2576        let result = kernel
2577            .execute(r#"early_exit "stop""#)
2578            .await
2579            .expect("function call failed");
2580
2581        assert!(result.ok());
2582        assert!(result.out.is_empty() || result.out.trim().is_empty());
2583    }
2584
2585    #[tokio::test]
2586    async fn test_exit_stops_execution() {
2587        let kernel = Kernel::transient().expect("failed to create kernel");
2588
2589        // exit should stop further execution
2590        kernel
2591            .execute(r#"
2592                BEFORE="yes"
2593                exit 0
2594                AFTER="yes"
2595            "#)
2596            .await
2597            .expect("execution failed");
2598
2599        // BEFORE should be set, AFTER should not
2600        let before = kernel.get_var("BEFORE").await;
2601        assert_eq!(before, Some(Value::String("yes".into())));
2602
2603        let after = kernel.get_var("AFTER").await;
2604        assert!(after.is_none(), "AFTER should not be set after exit");
2605    }
2606
2607    #[tokio::test]
2608    async fn test_exit_with_code() {
2609        let kernel = Kernel::transient().expect("failed to create kernel");
2610
2611        // exit with code should propagate the exit code
2612        let result = kernel
2613            .execute("exit 42")
2614            .await
2615            .expect("exit failed");
2616
2617        // The exit code should be in the output
2618        assert_eq!(result.out, "42");
2619    }
2620
2621    #[tokio::test]
2622    async fn test_set_e_stops_on_failure() {
2623        let kernel = Kernel::transient().expect("failed to create kernel");
2624
2625        // Enable error-exit mode
2626        kernel.execute("set -e").await.expect("set -e failed");
2627
2628        // Run a sequence where the middle command fails
2629        kernel
2630            .execute(r#"
2631                STEP1="done"
2632                false
2633                STEP2="done"
2634            "#)
2635            .await
2636            .expect("execution failed");
2637
2638        // STEP1 should be set, but STEP2 should NOT be set (exit on false)
2639        let step1 = kernel.get_var("STEP1").await;
2640        assert_eq!(step1, Some(Value::String("done".into())));
2641
2642        let step2 = kernel.get_var("STEP2").await;
2643        assert!(step2.is_none(), "STEP2 should not be set after false with set -e");
2644    }
2645
2646    #[tokio::test]
2647    async fn test_set_plus_e_disables_error_exit() {
2648        let kernel = Kernel::transient().expect("failed to create kernel");
2649
2650        // Enable then disable error-exit mode
2651        kernel.execute("set -e").await.expect("set -e failed");
2652        kernel.execute("set +e").await.expect("set +e failed");
2653
2654        // Now failure should NOT stop execution
2655        kernel
2656            .execute(r#"
2657                STEP1="done"
2658                false
2659                STEP2="done"
2660            "#)
2661            .await
2662            .expect("execution failed");
2663
2664        // Both should be set since +e disables error exit
2665        let step1 = kernel.get_var("STEP1").await;
2666        assert_eq!(step1, Some(Value::String("done".into())));
2667
2668        let step2 = kernel.get_var("STEP2").await;
2669        assert_eq!(step2, Some(Value::String("done".into())));
2670    }
2671
2672    #[tokio::test]
2673    async fn test_set_ignores_unknown_options() {
2674        let kernel = Kernel::transient().expect("failed to create kernel");
2675
2676        // Bash idiom: set -euo pipefail (we support -e, ignore the rest)
2677        let result = kernel
2678            .execute("set -e -u -o pipefail")
2679            .await
2680            .expect("set with unknown options failed");
2681
2682        assert!(result.ok(), "set should succeed with unknown options");
2683
2684        // -e should still be enabled
2685        kernel
2686            .execute(r#"
2687                BEFORE="yes"
2688                false
2689                AFTER="yes"
2690            "#)
2691            .await
2692            .ok();
2693
2694        let after = kernel.get_var("AFTER").await;
2695        assert!(after.is_none(), "-e should be enabled despite unknown options");
2696    }
2697
2698    #[tokio::test]
2699    async fn test_set_no_args_shows_settings() {
2700        let kernel = Kernel::transient().expect("failed to create kernel");
2701
2702        // Enable -e
2703        kernel.execute("set -e").await.expect("set -e failed");
2704
2705        // Call set with no args to see settings
2706        let result = kernel.execute("set").await.expect("set failed");
2707
2708        assert!(result.ok());
2709        assert!(result.out.contains("set -e"), "should show -e is enabled: {}", result.out);
2710    }
2711
2712    #[tokio::test]
2713    async fn test_set_e_in_pipeline() {
2714        let kernel = Kernel::transient().expect("failed to create kernel");
2715
2716        kernel.execute("set -e").await.expect("set -e failed");
2717
2718        // Pipeline failure should trigger exit
2719        kernel
2720            .execute(r#"
2721                BEFORE="yes"
2722                false | cat
2723                AFTER="yes"
2724            "#)
2725            .await
2726            .ok();
2727
2728        let before = kernel.get_var("BEFORE").await;
2729        assert_eq!(before, Some(Value::String("yes".into())));
2730
2731        // AFTER should not be set if pipeline failure triggers exit
2732        // Note: The exit code of a pipeline is the exit code of the last command
2733        // So `false | cat` returns 0 (cat succeeds). This is bash-compatible behavior.
2734        // To test pipeline failure, we need the last command to fail.
2735    }
2736
2737    #[tokio::test]
2738    async fn test_set_e_with_and_chain() {
2739        let kernel = Kernel::transient().expect("failed to create kernel");
2740
2741        kernel.execute("set -e").await.expect("set -e failed");
2742
2743        // Commands in && chain should not trigger -e on the first failure
2744        // because && explicitly handles the error
2745        kernel
2746            .execute(r#"
2747                RESULT="initial"
2748                false && RESULT="chained"
2749                RESULT="continued"
2750            "#)
2751            .await
2752            .ok();
2753
2754        // In bash, commands in && don't trigger -e. The chain handles the failure.
2755        // Our implementation may differ - let's verify current behavior.
2756        let result = kernel.get_var("RESULT").await;
2757        // If we follow bash semantics, RESULT should be "continued"
2758        // If we trigger -e on the false, RESULT stays "initial"
2759        assert!(result.is_some(), "RESULT should be set");
2760    }
2761
2762    // ═══════════════════════════════════════════════════════════════════════════
2763    // Source Tests
2764    // ═══════════════════════════════════════════════════════════════════════════
2765
2766    #[tokio::test]
2767    async fn test_source_sets_variables() {
2768        let kernel = Kernel::transient().expect("failed to create kernel");
2769
2770        // Write a script to the VFS
2771        kernel
2772            .execute(r#"write "/test.kai" 'FOO="bar"'"#)
2773            .await
2774            .expect("write failed");
2775
2776        // Source the script
2777        let result = kernel
2778            .execute(r#"source "/test.kai""#)
2779            .await
2780            .expect("source failed");
2781
2782        assert!(result.ok(), "source should succeed");
2783
2784        // Variable should be set in current scope
2785        let foo = kernel.get_var("FOO").await;
2786        assert_eq!(foo, Some(Value::String("bar".into())));
2787    }
2788
2789    #[tokio::test]
2790    async fn test_source_with_dot_alias() {
2791        let kernel = Kernel::transient().expect("failed to create kernel");
2792
2793        // Write a script to the VFS
2794        kernel
2795            .execute(r#"write "/vars.kai" 'X=42'"#)
2796            .await
2797            .expect("write failed");
2798
2799        // Source using . alias
2800        let result = kernel
2801            .execute(r#". "/vars.kai""#)
2802            .await
2803            .expect(". failed");
2804
2805        assert!(result.ok(), ". should succeed");
2806
2807        // Variable should be set in current scope
2808        let x = kernel.get_var("X").await;
2809        assert_eq!(x, Some(Value::Int(42)));
2810    }
2811
2812    #[tokio::test]
2813    async fn test_source_not_found() {
2814        let kernel = Kernel::transient().expect("failed to create kernel");
2815
2816        // Try to source a non-existent file
2817        let result = kernel
2818            .execute(r#"source "/nonexistent.kai""#)
2819            .await
2820            .expect("source should not fail with error");
2821
2822        assert!(!result.ok(), "source of non-existent file should fail");
2823        assert!(result.err.contains("nonexistent.kai"), "error should mention filename");
2824    }
2825
2826    #[tokio::test]
2827    async fn test_source_missing_filename() {
2828        let kernel = Kernel::transient().expect("failed to create kernel");
2829
2830        // Call source with no arguments
2831        let result = kernel
2832            .execute("source")
2833            .await
2834            .expect("source should not fail with error");
2835
2836        assert!(!result.ok(), "source without filename should fail");
2837        assert!(result.err.contains("missing filename"), "error should mention missing filename");
2838    }
2839
2840    #[tokio::test]
2841    async fn test_source_executes_multiple_statements() {
2842        let kernel = Kernel::transient().expect("failed to create kernel");
2843
2844        // Write a script with multiple statements
2845        kernel
2846            .execute(r#"write "/multi.kai" 'A=1
2847B=2
2848C=3'"#)
2849            .await
2850            .expect("write failed");
2851
2852        // Source it
2853        kernel
2854            .execute(r#"source "/multi.kai""#)
2855            .await
2856            .expect("source failed");
2857
2858        // All variables should be set
2859        assert_eq!(kernel.get_var("A").await, Some(Value::Int(1)));
2860        assert_eq!(kernel.get_var("B").await, Some(Value::Int(2)));
2861        assert_eq!(kernel.get_var("C").await, Some(Value::Int(3)));
2862    }
2863
2864    #[tokio::test]
2865    async fn test_source_can_define_functions() {
2866        let kernel = Kernel::transient().expect("failed to create kernel");
2867
2868        // Write a script that defines a function
2869        kernel
2870            .execute(r#"write "/functions.kai" 'greet() {
2871    echo "Hello, $1!"
2872}'"#)
2873            .await
2874            .expect("write failed");
2875
2876        // Source it
2877        kernel
2878            .execute(r#"source "/functions.kai""#)
2879            .await
2880            .expect("source failed");
2881
2882        // Use the defined function
2883        let result = kernel
2884            .execute(r#"greet "World""#)
2885            .await
2886            .expect("greet failed");
2887
2888        assert!(result.ok());
2889        assert!(result.out.contains("Hello, World!"));
2890    }
2891
2892    #[tokio::test]
2893    async fn test_source_inherits_error_exit() {
2894        let kernel = Kernel::transient().expect("failed to create kernel");
2895
2896        // Enable error exit
2897        kernel.execute("set -e").await.expect("set -e failed");
2898
2899        // Write a script that has a failure
2900        kernel
2901            .execute(r#"write "/fail.kai" 'BEFORE="yes"
2902false
2903AFTER="yes"'"#)
2904            .await
2905            .expect("write failed");
2906
2907        // Source it (should exit on false due to set -e)
2908        kernel
2909            .execute(r#"source "/fail.kai""#)
2910            .await
2911            .ok();
2912
2913        // BEFORE should be set, AFTER should NOT be set due to error exit
2914        let before = kernel.get_var("BEFORE").await;
2915        assert_eq!(before, Some(Value::String("yes".into())));
2916
2917        // Note: This test depends on whether error exit is checked within source
2918        // Currently our implementation checks per-statement in the main kernel
2919    }
2920
2921    // ═══════════════════════════════════════════════════════════════════════════
2922    // Case Statement Tests
2923    // ═══════════════════════════════════════════════════════════════════════════
2924
2925    #[tokio::test]
2926    async fn test_case_simple_match() {
2927        let kernel = Kernel::transient().expect("failed to create kernel");
2928
2929        let result = kernel
2930            .execute(r#"
2931                case "hello" in
2932                    hello) echo "matched hello" ;;
2933                    world) echo "matched world" ;;
2934                esac
2935            "#)
2936            .await
2937            .expect("case failed");
2938
2939        assert!(result.ok());
2940        assert_eq!(result.out.trim(), "matched hello");
2941    }
2942
2943    #[tokio::test]
2944    async fn test_case_wildcard_match() {
2945        let kernel = Kernel::transient().expect("failed to create kernel");
2946
2947        let result = kernel
2948            .execute(r#"
2949                case "main.rs" in
2950                    "*.py") echo "Python" ;;
2951                    "*.rs") echo "Rust" ;;
2952                    "*") echo "Unknown" ;;
2953                esac
2954            "#)
2955            .await
2956            .expect("case failed");
2957
2958        assert!(result.ok());
2959        assert_eq!(result.out.trim(), "Rust");
2960    }
2961
2962    #[tokio::test]
2963    async fn test_case_default_match() {
2964        let kernel = Kernel::transient().expect("failed to create kernel");
2965
2966        let result = kernel
2967            .execute(r#"
2968                case "unknown.xyz" in
2969                    "*.py") echo "Python" ;;
2970                    "*.rs") echo "Rust" ;;
2971                    "*") echo "Default" ;;
2972                esac
2973            "#)
2974            .await
2975            .expect("case failed");
2976
2977        assert!(result.ok());
2978        assert_eq!(result.out.trim(), "Default");
2979    }
2980
2981    #[tokio::test]
2982    async fn test_case_no_match() {
2983        let kernel = Kernel::transient().expect("failed to create kernel");
2984
2985        // Case with no default branch and no match
2986        let result = kernel
2987            .execute(r#"
2988                case "nope" in
2989                    "yes") echo "yes" ;;
2990                    "no") echo "no" ;;
2991                esac
2992            "#)
2993            .await
2994            .expect("case failed");
2995
2996        assert!(result.ok());
2997        assert!(result.out.is_empty(), "no match should produce empty output");
2998    }
2999
3000    #[tokio::test]
3001    async fn test_case_with_variable() {
3002        let kernel = Kernel::transient().expect("failed to create kernel");
3003
3004        kernel.execute(r#"LANG="rust""#).await.expect("set failed");
3005
3006        let result = kernel
3007            .execute(r#"
3008                case ${LANG} in
3009                    python) echo "snake" ;;
3010                    rust) echo "crab" ;;
3011                    go) echo "gopher" ;;
3012                esac
3013            "#)
3014            .await
3015            .expect("case failed");
3016
3017        assert!(result.ok());
3018        assert_eq!(result.out.trim(), "crab");
3019    }
3020
3021    #[tokio::test]
3022    async fn test_case_multiple_patterns() {
3023        let kernel = Kernel::transient().expect("failed to create kernel");
3024
3025        let result = kernel
3026            .execute(r#"
3027                case "yes" in
3028                    "y"|"yes"|"Y"|"YES") echo "affirmative" ;;
3029                    "n"|"no"|"N"|"NO") echo "negative" ;;
3030                esac
3031            "#)
3032            .await
3033            .expect("case failed");
3034
3035        assert!(result.ok());
3036        assert_eq!(result.out.trim(), "affirmative");
3037    }
3038
3039    #[tokio::test]
3040    async fn test_case_glob_question_mark() {
3041        let kernel = Kernel::transient().expect("failed to create kernel");
3042
3043        let result = kernel
3044            .execute(r#"
3045                case "test1" in
3046                    "test?") echo "matched test?" ;;
3047                    "*") echo "default" ;;
3048                esac
3049            "#)
3050            .await
3051            .expect("case failed");
3052
3053        assert!(result.ok());
3054        assert_eq!(result.out.trim(), "matched test?");
3055    }
3056
3057    #[tokio::test]
3058    async fn test_case_char_class() {
3059        let kernel = Kernel::transient().expect("failed to create kernel");
3060
3061        let result = kernel
3062            .execute(r#"
3063                case "Yes" in
3064                    "[Yy]*") echo "yes-like" ;;
3065                    "[Nn]*") echo "no-like" ;;
3066                esac
3067            "#)
3068            .await
3069            .expect("case failed");
3070
3071        assert!(result.ok());
3072        assert_eq!(result.out.trim(), "yes-like");
3073    }
3074
3075    // ═══════════════════════════════════════════════════════════════════════════
3076    // Cat Stdin Tests
3077    // ═══════════════════════════════════════════════════════════════════════════
3078
3079    #[tokio::test]
3080    async fn test_cat_from_pipeline() {
3081        let kernel = Kernel::transient().expect("failed to create kernel");
3082
3083        let result = kernel
3084            .execute(r#"echo "piped text" | cat"#)
3085            .await
3086            .expect("cat pipeline failed");
3087
3088        assert!(result.ok(), "cat failed: {}", result.err);
3089        assert_eq!(result.out.trim(), "piped text");
3090    }
3091
3092    #[tokio::test]
3093    async fn test_cat_from_pipeline_multiline() {
3094        let kernel = Kernel::transient().expect("failed to create kernel");
3095
3096        let result = kernel
3097            .execute(r#"echo "line1\nline2" | cat -n"#)
3098            .await
3099            .expect("cat pipeline failed");
3100
3101        assert!(result.ok(), "cat failed: {}", result.err);
3102        assert!(result.out.contains("1\t"), "output: {}", result.out);
3103    }
3104
3105    // ═══════════════════════════════════════════════════════════════════════════
3106    // Heredoc Tests
3107    // ═══════════════════════════════════════════════════════════════════════════
3108
3109    #[tokio::test]
3110    async fn test_heredoc_basic() {
3111        let kernel = Kernel::transient().expect("failed to create kernel");
3112
3113        let result = kernel
3114            .execute("cat <<EOF\nhello\nEOF")
3115            .await
3116            .expect("heredoc failed");
3117
3118        assert!(result.ok(), "cat with heredoc failed: {}", result.err);
3119        assert_eq!(result.out.trim(), "hello");
3120    }
3121
3122    #[tokio::test]
3123    async fn test_arithmetic_in_string() {
3124        let kernel = Kernel::transient().expect("failed to create kernel");
3125
3126        let result = kernel
3127            .execute(r#"echo "result: $((1 + 2))""#)
3128            .await
3129            .expect("arithmetic in string failed");
3130
3131        assert!(result.ok(), "echo failed: {}", result.err);
3132        assert_eq!(result.out.trim(), "result: 3");
3133    }
3134
3135    #[tokio::test]
3136    async fn test_heredoc_multiline() {
3137        let kernel = Kernel::transient().expect("failed to create kernel");
3138
3139        let result = kernel
3140            .execute("cat <<EOF\nline1\nline2\nline3\nEOF")
3141            .await
3142            .expect("heredoc failed");
3143
3144        assert!(result.ok(), "cat with heredoc failed: {}", result.err);
3145        assert!(result.out.contains("line1"), "output: {}", result.out);
3146        assert!(result.out.contains("line2"), "output: {}", result.out);
3147        assert!(result.out.contains("line3"), "output: {}", result.out);
3148    }
3149
3150    // ═══════════════════════════════════════════════════════════════════════════
3151    // Read Builtin Tests
3152    // ═══════════════════════════════════════════════════════════════════════════
3153
3154    #[tokio::test]
3155    async fn test_read_from_pipeline() {
3156        let kernel = Kernel::transient().expect("failed to create kernel");
3157
3158        // Pipe input to read
3159        let result = kernel
3160            .execute(r#"echo "Alice" | read NAME; echo "Hello, ${NAME}""#)
3161            .await
3162            .expect("read pipeline failed");
3163
3164        assert!(result.ok(), "read failed: {}", result.err);
3165        assert!(result.out.contains("Hello, Alice"), "output: {}", result.out);
3166    }
3167
3168    #[tokio::test]
3169    async fn test_read_multiple_vars_from_pipeline() {
3170        let kernel = Kernel::transient().expect("failed to create kernel");
3171
3172        let result = kernel
3173            .execute(r#"echo "John Doe 42" | read FIRST LAST AGE; echo "${FIRST} is ${AGE}""#)
3174            .await
3175            .expect("read pipeline failed");
3176
3177        assert!(result.ok(), "read failed: {}", result.err);
3178        assert!(result.out.contains("John is 42"), "output: {}", result.out);
3179    }
3180
3181    // ═══════════════════════════════════════════════════════════════════════════
3182    // Shell-Style Function Tests
3183    // ═══════════════════════════════════════════════════════════════════════════
3184
3185    #[tokio::test]
3186    async fn test_posix_function_with_positional_params() {
3187        let kernel = Kernel::transient().expect("failed to create kernel");
3188
3189        // Define POSIX-style function
3190        kernel
3191            .execute(r#"greet() { echo "Hello, $1!" }"#)
3192            .await
3193            .expect("function definition failed");
3194
3195        // Call the function
3196        let result = kernel
3197            .execute(r#"greet "Amy""#)
3198            .await
3199            .expect("function call failed");
3200
3201        assert!(result.ok(), "greet failed: {}", result.err);
3202        assert_eq!(result.out.trim(), "Hello, Amy!");
3203    }
3204
3205    #[tokio::test]
3206    async fn test_posix_function_multiple_args() {
3207        let kernel = Kernel::transient().expect("failed to create kernel");
3208
3209        // Define function using $1 and $2
3210        kernel
3211            .execute(r#"add_greeting() { echo "$1 $2!" }"#)
3212            .await
3213            .expect("function definition failed");
3214
3215        // Call the function
3216        let result = kernel
3217            .execute(r#"add_greeting "Hello" "World""#)
3218            .await
3219            .expect("function call failed");
3220
3221        assert!(result.ok(), "function failed: {}", result.err);
3222        assert_eq!(result.out.trim(), "Hello World!");
3223    }
3224
3225    #[tokio::test]
3226    async fn test_bash_function_with_positional_params() {
3227        let kernel = Kernel::transient().expect("failed to create kernel");
3228
3229        // Define bash-style function (function keyword, no parens)
3230        kernel
3231            .execute(r#"function greet { echo "Hi $1" }"#)
3232            .await
3233            .expect("function definition failed");
3234
3235        // Call the function
3236        let result = kernel
3237            .execute(r#"greet "Bob""#)
3238            .await
3239            .expect("function call failed");
3240
3241        assert!(result.ok(), "greet failed: {}", result.err);
3242        assert_eq!(result.out.trim(), "Hi Bob");
3243    }
3244
3245    #[tokio::test]
3246    async fn test_shell_function_with_all_args() {
3247        let kernel = Kernel::transient().expect("failed to create kernel");
3248
3249        // Define function using $@ (all args)
3250        kernel
3251            .execute(r#"echo_all() { echo "args: $@" }"#)
3252            .await
3253            .expect("function definition failed");
3254
3255        // Call with multiple args
3256        let result = kernel
3257            .execute(r#"echo_all "a" "b" "c""#)
3258            .await
3259            .expect("function call failed");
3260
3261        assert!(result.ok(), "function failed: {}", result.err);
3262        assert_eq!(result.out.trim(), "args: a b c");
3263    }
3264
3265    #[tokio::test]
3266    async fn test_shell_function_with_arg_count() {
3267        let kernel = Kernel::transient().expect("failed to create kernel");
3268
3269        // Define function using $# (arg count)
3270        kernel
3271            .execute(r#"count_args() { echo "count: $#" }"#)
3272            .await
3273            .expect("function definition failed");
3274
3275        // Call with three args
3276        let result = kernel
3277            .execute(r#"count_args "x" "y" "z""#)
3278            .await
3279            .expect("function call failed");
3280
3281        assert!(result.ok(), "function failed: {}", result.err);
3282        assert_eq!(result.out.trim(), "count: 3");
3283    }
3284
3285    #[tokio::test]
3286    async fn test_shell_function_shared_scope() {
3287        let kernel = Kernel::transient().expect("failed to create kernel");
3288
3289        // Set a variable in parent scope
3290        kernel
3291            .execute(r#"PARENT_VAR="visible""#)
3292            .await
3293            .expect("set failed");
3294
3295        // Define shell function that reads and writes parent variable
3296        kernel
3297            .execute(r#"modify_parent() {
3298                echo "saw: ${PARENT_VAR}"
3299                PARENT_VAR="changed by function"
3300            }"#)
3301            .await
3302            .expect("function definition failed");
3303
3304        // Call the function - it SHOULD see PARENT_VAR (bash-compatible shared scope)
3305        let result = kernel.execute("modify_parent").await.expect("function failed");
3306
3307        assert!(
3308            result.out.contains("visible"),
3309            "Shell function should access parent scope, got: {}",
3310            result.out
3311        );
3312
3313        // Parent variable should be modified
3314        let var = kernel.get_var("PARENT_VAR").await;
3315        assert_eq!(
3316            var,
3317            Some(Value::String("changed by function".into())),
3318            "Shell function should modify parent scope"
3319        );
3320    }
3321
3322    // ═══════════════════════════════════════════════════════════════════════════
3323    // Script Execution via PATH Tests
3324    // ═══════════════════════════════════════════════════════════════════════════
3325
3326    #[tokio::test]
3327    async fn test_script_execution_from_path() {
3328        let kernel = Kernel::transient().expect("failed to create kernel");
3329
3330        // Create /bin directory and script
3331        kernel.execute(r#"mkdir "/bin""#).await.ok();
3332        kernel
3333            .execute(r#"write "/bin/hello.kai" 'echo "Hello from script!"'"#)
3334            .await
3335            .expect("write script failed");
3336
3337        // Set PATH to /bin
3338        kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
3339
3340        // Call script by name (without .kai extension)
3341        let result = kernel
3342            .execute("hello")
3343            .await
3344            .expect("script execution failed");
3345
3346        assert!(result.ok(), "script failed: {}", result.err);
3347        assert_eq!(result.out.trim(), "Hello from script!");
3348    }
3349
3350    #[tokio::test]
3351    async fn test_script_with_args() {
3352        let kernel = Kernel::transient().expect("failed to create kernel");
3353
3354        // Create script that uses positional params
3355        kernel.execute(r#"mkdir "/bin""#).await.ok();
3356        kernel
3357            .execute(r#"write "/bin/greet.kai" 'echo "Hello, $1!"'"#)
3358            .await
3359            .expect("write script failed");
3360
3361        // Set PATH
3362        kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
3363
3364        // Call script with arg
3365        let result = kernel
3366            .execute(r#"greet "World""#)
3367            .await
3368            .expect("script execution failed");
3369
3370        assert!(result.ok(), "script failed: {}", result.err);
3371        assert_eq!(result.out.trim(), "Hello, World!");
3372    }
3373
3374    #[tokio::test]
3375    async fn test_script_not_found() {
3376        let kernel = Kernel::transient().expect("failed to create kernel");
3377
3378        // Set empty PATH
3379        kernel.execute(r#"PATH="/nonexistent""#).await.expect("set PATH failed");
3380
3381        // Call non-existent script
3382        let result = kernel
3383            .execute("noscript")
3384            .await
3385            .expect("execution failed");
3386
3387        assert!(!result.ok(), "should fail with command not found");
3388        assert_eq!(result.code, 127);
3389        assert!(result.err.contains("command not found"));
3390    }
3391
3392    #[tokio::test]
3393    async fn test_script_path_search_order() {
3394        let kernel = Kernel::transient().expect("failed to create kernel");
3395
3396        // Create two directories with same-named script
3397        // Note: using "myscript" not "test" to avoid conflict with test builtin
3398        kernel.execute(r#"mkdir "/first""#).await.ok();
3399        kernel.execute(r#"mkdir "/second""#).await.ok();
3400        kernel
3401            .execute(r#"write "/first/myscript.kai" 'echo "from first"'"#)
3402            .await
3403            .expect("write failed");
3404        kernel
3405            .execute(r#"write "/second/myscript.kai" 'echo "from second"'"#)
3406            .await
3407            .expect("write failed");
3408
3409        // Set PATH with first before second
3410        kernel.execute(r#"PATH="/first:/second""#).await.expect("set PATH failed");
3411
3412        // Should find first one
3413        let result = kernel
3414            .execute("myscript")
3415            .await
3416            .expect("script execution failed");
3417
3418        assert!(result.ok(), "script failed: {}", result.err);
3419        assert_eq!(result.out.trim(), "from first");
3420    }
3421
3422    // ═══════════════════════════════════════════════════════════════════════════
3423    // Special Variable Tests ($?, $$, unset vars)
3424    // ═══════════════════════════════════════════════════════════════════════════
3425
3426    #[tokio::test]
3427    async fn test_last_exit_code_success() {
3428        let kernel = Kernel::transient().expect("failed to create kernel");
3429
3430        // true exits with 0
3431        let result = kernel.execute("true; echo $?").await.expect("execution failed");
3432        assert!(result.out.contains("0"), "expected 0, got: {}", result.out);
3433    }
3434
3435    #[tokio::test]
3436    async fn test_last_exit_code_failure() {
3437        let kernel = Kernel::transient().expect("failed to create kernel");
3438
3439        // false exits with 1
3440        let result = kernel.execute("false; echo $?").await.expect("execution failed");
3441        assert!(result.out.contains("1"), "expected 1, got: {}", result.out);
3442    }
3443
3444    #[tokio::test]
3445    async fn test_current_pid() {
3446        let kernel = Kernel::transient().expect("failed to create kernel");
3447
3448        let result = kernel.execute("echo $$").await.expect("execution failed");
3449        // PID should be a positive number
3450        let pid: u32 = result.out.trim().parse().expect("PID should be a number");
3451        assert!(pid > 0, "PID should be positive");
3452    }
3453
3454    #[tokio::test]
3455    async fn test_unset_variable_expands_to_empty() {
3456        let kernel = Kernel::transient().expect("failed to create kernel");
3457
3458        // Unset variable in interpolation should be empty
3459        let result = kernel.execute(r#"echo "prefix:${UNSET_VAR}:suffix""#).await.expect("execution failed");
3460        assert_eq!(result.out.trim(), "prefix::suffix");
3461    }
3462
3463    #[tokio::test]
3464    async fn test_eq_ne_operators() {
3465        let kernel = Kernel::transient().expect("failed to create kernel");
3466
3467        // Test -eq operator
3468        let result = kernel.execute(r#"if [[ 5 -eq 5 ]]; then echo "eq works"; fi"#).await.expect("execution failed");
3469        assert_eq!(result.out.trim(), "eq works");
3470
3471        // Test -ne operator
3472        let result = kernel.execute(r#"if [[ 5 -ne 3 ]]; then echo "ne works"; fi"#).await.expect("execution failed");
3473        assert_eq!(result.out.trim(), "ne works");
3474
3475        // Test -eq with different values
3476        let result = kernel.execute(r#"if [[ 5 -eq 3 ]]; then echo "wrong"; else echo "correct"; fi"#).await.expect("execution failed");
3477        assert_eq!(result.out.trim(), "correct");
3478    }
3479
3480    #[tokio::test]
3481    async fn test_escaped_dollar_in_string() {
3482        let kernel = Kernel::transient().expect("failed to create kernel");
3483
3484        // \$ should produce literal $
3485        let result = kernel.execute(r#"echo "\$100""#).await.expect("execution failed");
3486        assert_eq!(result.out.trim(), "$100");
3487    }
3488
3489    #[tokio::test]
3490    async fn test_special_vars_in_interpolation() {
3491        let kernel = Kernel::transient().expect("failed to create kernel");
3492
3493        // Test $? in string interpolation
3494        let result = kernel.execute(r#"true; echo "exit: $?""#).await.expect("execution failed");
3495        assert_eq!(result.out.trim(), "exit: 0");
3496
3497        // Test $$ in string interpolation
3498        let result = kernel.execute(r#"echo "pid: $$""#).await.expect("execution failed");
3499        assert!(result.out.starts_with("pid: "), "unexpected output: {}", result.out);
3500        let pid_part = result.out.trim().strip_prefix("pid: ").unwrap();
3501        let _pid: u32 = pid_part.parse().expect("PID in string should be a number");
3502    }
3503
3504    // ═══════════════════════════════════════════════════════════════════════════
3505    // Command Substitution Tests
3506    // ═══════════════════════════════════════════════════════════════════════════
3507
3508    #[tokio::test]
3509    async fn test_command_subst_assignment() {
3510        let kernel = Kernel::transient().expect("failed to create kernel");
3511
3512        // Command substitution in assignment
3513        let result = kernel.execute(r#"X=$(echo hello); echo "$X""#).await.expect("execution failed");
3514        assert_eq!(result.out.trim(), "hello");
3515    }
3516
3517    #[tokio::test]
3518    async fn test_command_subst_with_args() {
3519        let kernel = Kernel::transient().expect("failed to create kernel");
3520
3521        // Command substitution with string argument
3522        let result = kernel.execute(r#"X=$(echo "a b c"); echo "$X""#).await.expect("execution failed");
3523        assert_eq!(result.out.trim(), "a b c");
3524    }
3525
3526    #[tokio::test]
3527    async fn test_command_subst_nested_vars() {
3528        let kernel = Kernel::transient().expect("failed to create kernel");
3529
3530        // Variables inside command substitution
3531        let result = kernel.execute(r#"Y=world; X=$(echo "hello $Y"); echo "$X""#).await.expect("execution failed");
3532        assert_eq!(result.out.trim(), "hello world");
3533    }
3534
3535    #[tokio::test]
3536    async fn test_background_job_basic() {
3537        use std::time::Duration;
3538
3539        let kernel = Kernel::new(KernelConfig::isolated()).expect("failed to create kernel");
3540
3541        // Run a simple background command
3542        let result = kernel.execute("echo hello &").await.expect("execution failed");
3543        assert!(result.ok(), "background command should succeed: {}", result.err);
3544        assert!(result.out.contains("[1]"), "should return job ID: {}", result.out);
3545
3546        // Give the job time to complete
3547        tokio::time::sleep(Duration::from_millis(100)).await;
3548
3549        // Check job status
3550        let status = kernel.execute("cat /v/jobs/1/status").await.expect("status check failed");
3551        assert!(status.ok(), "status should succeed: {}", status.err);
3552        assert!(
3553            status.out.contains("done:") || status.out.contains("running"),
3554            "should have valid status: {}",
3555            status.out
3556        );
3557
3558        // Check stdout
3559        let stdout = kernel.execute("cat /v/jobs/1/stdout").await.expect("stdout check failed");
3560        assert!(stdout.ok());
3561        assert!(stdout.out.contains("hello"));
3562    }
3563
3564}