1use std::collections::HashMap;
26use std::path::PathBuf;
27use std::sync::Arc;
28
29use anyhow::{Context, Result};
30use tokio::sync::RwLock;
31
32use async_trait::async_trait;
33
34use crate::ast::{Arg, Command, Expr, Stmt, StringPart, ToolDef, Value, BinaryOp};
35use crate::backend::{BackendError, KernelBackend};
36use kaish_glob::glob_match;
37use crate::dispatch::{CommandDispatcher, PipelinePosition};
38use crate::interpreter::{apply_output_format, eval_expr, expand_tilde, json_to_value, value_to_string, ControlFlow, ExecResult, Scope};
39use crate::parser::parse;
40use crate::scheduler::{drain_to_stream, is_bool_type, schema_param_lookup, stderr_stream, BoundedStream, JobManager, PipelineRunner, StderrReceiver, DEFAULT_STREAM_MAX_SIZE};
41use crate::tools::{extract_output_format, register_builtins, resolve_in_path, ExecContext, ToolArgs, ToolRegistry};
42use crate::validator::{Severity, Validator};
43use crate::vfs::{BuiltinFs, JobFs, LocalFs, MemoryFs, VfsRouter};
44
45#[derive(Debug, Clone)]
52pub enum VfsMountMode {
53 Passthrough,
62
63 Sandboxed {
75 root: Option<PathBuf>,
78 },
79
80 NoLocal,
90}
91
92impl Default for VfsMountMode {
93 fn default() -> Self {
94 VfsMountMode::Sandboxed { root: None }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct KernelConfig {
101 pub name: String,
103
104 pub vfs_mode: VfsMountMode,
106
107 pub cwd: PathBuf,
109
110 pub skip_validation: bool,
116
117 pub interactive: bool,
122}
123
124fn default_sandbox_root() -> PathBuf {
126 std::env::var("HOME")
127 .map(PathBuf::from)
128 .unwrap_or_else(|_| PathBuf::from("/"))
129}
130
131impl Default for KernelConfig {
132 fn default() -> Self {
133 let home = default_sandbox_root();
134 Self {
135 name: "default".to_string(),
136 vfs_mode: VfsMountMode::Sandboxed { root: None },
137 cwd: home,
138 skip_validation: false,
139 interactive: false,
140 }
141 }
142}
143
144impl KernelConfig {
145 pub fn transient() -> Self {
147 let home = default_sandbox_root();
148 Self {
149 name: "transient".to_string(),
150 vfs_mode: VfsMountMode::Sandboxed { root: None },
151 cwd: home,
152 skip_validation: false,
153 interactive: false,
154 }
155 }
156
157 pub fn named(name: &str) -> Self {
159 let home = default_sandbox_root();
160 Self {
161 name: name.to_string(),
162 vfs_mode: VfsMountMode::Sandboxed { root: None },
163 cwd: home,
164 skip_validation: false,
165 interactive: false,
166 }
167 }
168
169 pub fn repl() -> Self {
174 let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
175 Self {
176 name: "repl".to_string(),
177 vfs_mode: VfsMountMode::Passthrough,
178 cwd,
179 skip_validation: false,
180 interactive: false,
181 }
182 }
183
184 pub fn mcp() -> Self {
189 let home = default_sandbox_root();
190 Self {
191 name: "mcp".to_string(),
192 vfs_mode: VfsMountMode::Sandboxed { root: None },
193 cwd: home,
194 skip_validation: false,
195 interactive: false,
196 }
197 }
198
199 pub fn mcp_with_root(root: PathBuf) -> Self {
203 Self {
204 name: "mcp".to_string(),
205 vfs_mode: VfsMountMode::Sandboxed { root: Some(root.clone()) },
206 cwd: root,
207 skip_validation: false,
208 interactive: false,
209 }
210 }
211
212 pub fn isolated() -> Self {
216 Self {
217 name: "isolated".to_string(),
218 vfs_mode: VfsMountMode::NoLocal,
219 cwd: PathBuf::from("/"),
220 skip_validation: false,
221 interactive: false,
222 }
223 }
224
225 pub fn with_vfs_mode(mut self, mode: VfsMountMode) -> Self {
227 self.vfs_mode = mode;
228 self
229 }
230
231 pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
233 self.cwd = cwd;
234 self
235 }
236
237 pub fn with_skip_validation(mut self, skip: bool) -> Self {
239 self.skip_validation = skip;
240 self
241 }
242
243 pub fn with_interactive(mut self, interactive: bool) -> Self {
245 self.interactive = interactive;
246 self
247 }
248}
249
250pub struct Kernel {
255 name: String,
257 scope: RwLock<Scope>,
259 tools: Arc<ToolRegistry>,
261 user_tools: RwLock<HashMap<String, ToolDef>>,
263 vfs: Arc<VfsRouter>,
265 jobs: Arc<JobManager>,
267 runner: PipelineRunner,
269 exec_ctx: RwLock<ExecContext>,
271 skip_validation: bool,
273 interactive: bool,
275 stderr_receiver: tokio::sync::Mutex<StderrReceiver>,
280 #[cfg(unix)]
282 terminal_state: Option<Arc<crate::terminal::TerminalState>>,
283}
284
285impl Kernel {
286 pub fn new(config: KernelConfig) -> Result<Self> {
288 let mut vfs = Self::setup_vfs(&config);
289 let jobs = Arc::new(JobManager::new());
290
291 vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
293
294 Self::assemble(config, vfs, jobs, |vfs_ref, tools| {
295 ExecContext::with_vfs_and_tools(vfs_ref.clone(), tools.clone())
296 })
297 }
298
299 fn setup_vfs(config: &KernelConfig) -> VfsRouter {
301 let mut vfs = VfsRouter::new();
302
303 match &config.vfs_mode {
304 VfsMountMode::Passthrough => {
305 vfs.mount("/", LocalFs::new(PathBuf::from("/")));
307 vfs.mount("/v", MemoryFs::new());
309 }
310 VfsMountMode::Sandboxed { root } => {
311 vfs.mount("/", MemoryFs::new());
313 vfs.mount("/v", MemoryFs::new());
314
315 vfs.mount("/tmp", LocalFs::new(PathBuf::from("/tmp")));
317
318 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 let mount_point = local_root.to_string_lossy().to_string();
329 vfs.mount(&mount_point, LocalFs::new(local_root));
330 }
331 VfsMountMode::NoLocal => {
332 vfs.mount("/", MemoryFs::new());
334 vfs.mount("/tmp", MemoryFs::new());
335 vfs.mount("/v", MemoryFs::new());
336 }
337 }
338
339 vfs
340 }
341
342 pub fn transient() -> Result<Self> {
344 Self::new(KernelConfig::transient())
345 }
346
347 pub fn with_backend(
376 backend: Arc<dyn KernelBackend>,
377 config: KernelConfig,
378 configure_vfs: impl FnOnce(&mut VfsRouter),
379 ) -> Result<Self> {
380 use crate::backend::VirtualOverlayBackend;
381
382 let mut vfs = VfsRouter::new();
383 let jobs = Arc::new(JobManager::new());
384
385 vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
386 vfs.mount("/v/blobs", MemoryFs::new());
387
388 configure_vfs(&mut vfs);
390
391 Self::assemble(config, vfs, jobs, |vfs_arc: &Arc<VfsRouter>, _: &Arc<ToolRegistry>| {
392 let overlay: Arc<dyn KernelBackend> =
393 Arc::new(VirtualOverlayBackend::new(backend, vfs_arc.clone()));
394 ExecContext::with_backend(overlay)
395 })
396 }
397
398 fn assemble(
404 config: KernelConfig,
405 mut vfs: VfsRouter,
406 jobs: Arc<JobManager>,
407 make_ctx: impl FnOnce(&Arc<VfsRouter>, &Arc<ToolRegistry>) -> ExecContext,
408 ) -> Result<Self> {
409 let KernelConfig { name, cwd, skip_validation, interactive, .. } = config;
410
411 let mut tools = ToolRegistry::new();
412 register_builtins(&mut tools);
413 let tools = Arc::new(tools);
414
415 vfs.mount("/v/bin", BuiltinFs::new(tools.clone()));
417
418 let vfs = Arc::new(vfs);
419
420 let runner = PipelineRunner::new(tools.clone());
421
422 let (stderr_writer, stderr_receiver) = stderr_stream();
423
424 let mut exec_ctx = make_ctx(&vfs, &tools);
425 exec_ctx.set_cwd(cwd);
426 exec_ctx.set_job_manager(jobs.clone());
427 exec_ctx.set_tool_schemas(tools.schemas());
428 exec_ctx.set_tools(tools.clone());
429 exec_ctx.stderr = Some(stderr_writer);
430
431 Ok(Self {
432 name,
433 scope: RwLock::new({
434 let mut scope = Scope::new();
435 if let Ok(home) = std::env::var("HOME") {
436 scope.set("HOME", Value::String(home));
437 }
438 scope
439 }),
440 tools,
441 user_tools: RwLock::new(HashMap::new()),
442 vfs,
443 jobs,
444 runner,
445 exec_ctx: RwLock::new(exec_ctx),
446 skip_validation,
447 interactive,
448 stderr_receiver: tokio::sync::Mutex::new(stderr_receiver),
449 #[cfg(unix)]
450 terminal_state: None,
451 })
452 }
453
454 pub fn name(&self) -> &str {
456 &self.name
457 }
458
459 #[cfg(unix)]
464 pub fn init_terminal(&mut self) {
465 if !self.interactive {
466 return;
467 }
468 match crate::terminal::TerminalState::init() {
469 Ok(state) => {
470 let state = Arc::new(state);
471 self.terminal_state = Some(state.clone());
472 self.exec_ctx.get_mut().terminal_state = Some(state);
474 tracing::debug!("terminal job control initialized");
475 }
476 Err(e) => {
477 tracing::warn!("failed to initialize terminal job control: {}", e);
478 }
479 }
480 }
481
482 pub async fn execute(&self, input: &str) -> Result<ExecResult> {
486 self.execute_streaming(input, &mut |_| {}).await
487 }
488
489 #[tracing::instrument(level = "info", skip(self, on_output), fields(input_len = input.len()))]
498 pub async fn execute_streaming(
499 &self,
500 input: &str,
501 on_output: &mut dyn FnMut(&ExecResult),
502 ) -> Result<ExecResult> {
503 let program = parse(input).map_err(|errors| {
504 let msg = errors
505 .iter()
506 .map(|e| e.to_string())
507 .collect::<Vec<_>>()
508 .join("; ");
509 anyhow::anyhow!("parse error: {}", msg)
510 })?;
511
512 {
514 let scope = self.scope.read().await;
515 if scope.show_ast() {
516 let output = format!("{:#?}\n", program);
517 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(output)));
518 }
519 }
520
521 if !self.skip_validation {
523 let user_tools = self.user_tools.read().await;
524 let validator = Validator::new(&self.tools, &user_tools);
525 let issues = validator.validate(&program);
526
527 let errors: Vec<_> = issues
529 .iter()
530 .filter(|i| i.severity == Severity::Error)
531 .collect();
532
533 if !errors.is_empty() {
534 let error_msg = errors
535 .iter()
536 .map(|e| e.format(input))
537 .collect::<Vec<_>>()
538 .join("\n");
539 return Err(anyhow::anyhow!("validation failed:\n{}", error_msg));
540 }
541
542 for warning in issues.iter().filter(|i| i.severity == Severity::Warning) {
544 tracing::trace!("validation: {}", warning.format(input));
545 }
546 }
547
548 let mut result = ExecResult::success("");
549
550 for stmt in program.statements {
551 if matches!(stmt, Stmt::Empty) {
552 continue;
553 }
554 let flow = self.execute_stmt_flow(&stmt).await?;
555
556 let drained_stderr = {
560 let mut receiver = self.stderr_receiver.lock().await;
561 receiver.drain_lossy()
562 };
563
564 match flow {
565 ControlFlow::Normal(mut r) => {
566 if !drained_stderr.is_empty() {
567 if !r.err.is_empty() && !r.err.ends_with('\n') {
568 r.err.push('\n');
569 }
570 let combined = format!("{}{}", drained_stderr, r.err);
572 r.err = combined;
573 }
574 on_output(&r);
575 let last_output = r.output.clone();
579 accumulate_result(&mut result, &r);
580 result.output = last_output;
581 }
582 ControlFlow::Exit { code } => {
583 if !drained_stderr.is_empty() {
584 result.err.push_str(&drained_stderr);
585 }
586 result.code = code;
587 return Ok(result);
588 }
589 ControlFlow::Return { mut value } => {
590 if !drained_stderr.is_empty() {
591 value.err = format!("{}{}", drained_stderr, value.err);
592 }
593 on_output(&value);
594 result = value;
595 }
596 ControlFlow::Break { result: mut r, .. } | ControlFlow::Continue { result: mut r, .. } => {
597 if !drained_stderr.is_empty() {
598 r.err = format!("{}{}", drained_stderr, r.err);
599 }
600 on_output(&r);
601 result = r;
602 }
603 }
604 }
605
606 Ok(result)
607 }
608
609 fn execute_stmt_flow<'a>(
611 &'a self,
612 stmt: &'a Stmt,
613 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ControlFlow>> + Send + 'a>> {
614 use tracing::Instrument;
615 let span = tracing::debug_span!("execute_stmt_flow", stmt_type = %stmt.kind_name());
616 Box::pin(async move {
617 match stmt {
618 Stmt::Assignment(assign) => {
619 let value = self.eval_expr_async(&assign.value).await
621 .context("failed to evaluate assignment")?;
622 let mut scope = self.scope.write().await;
623 if assign.local {
624 scope.set(&assign.name, value.clone());
626 } else {
627 scope.set_global(&assign.name, value.clone());
629 }
630 drop(scope);
631
632 Ok(ControlFlow::ok(ExecResult::success("")))
634 }
635 Stmt::Command(cmd) => {
636 let pipeline = crate::ast::Pipeline {
639 commands: vec![cmd.clone()],
640 background: false,
641 };
642 let result = self.execute_pipeline(&pipeline).await?;
643 self.update_last_result(&result).await;
644
645 if !result.ok() {
647 let scope = self.scope.read().await;
648 if scope.error_exit_enabled() {
649 return Ok(ControlFlow::exit_code(result.code));
650 }
651 }
652
653 Ok(ControlFlow::ok(result))
654 }
655 Stmt::Pipeline(pipeline) => {
656 let result = self.execute_pipeline(pipeline).await?;
657 self.update_last_result(&result).await;
658
659 if !result.ok() {
661 let scope = self.scope.read().await;
662 if scope.error_exit_enabled() {
663 return Ok(ControlFlow::exit_code(result.code));
664 }
665 }
666
667 Ok(ControlFlow::ok(result))
668 }
669 Stmt::If(if_stmt) => {
670 let cond_value = self.eval_expr_async(&if_stmt.condition).await?;
672
673 let branch = if is_truthy(&cond_value) {
674 &if_stmt.then_branch
675 } else {
676 if_stmt.else_branch.as_deref().unwrap_or(&[])
677 };
678
679 let mut result = ExecResult::success("");
680 for stmt in branch {
681 let flow = self.execute_stmt_flow(stmt).await?;
682 match flow {
683 ControlFlow::Normal(r) => accumulate_result(&mut result, &r),
684 _ => return Ok(flow),
685 }
686 }
687 Ok(ControlFlow::ok(result))
688 }
689 Stmt::For(for_loop) => {
690 let mut items: Vec<Value> = Vec::new();
693 for item_expr in &for_loop.items {
694 let item = self.eval_expr_async(item_expr).await?;
695 match &item {
697 Value::Json(serde_json::Value::Array(arr)) => {
699 for elem in arr {
700 items.push(json_to_value(elem.clone()));
701 }
702 }
703 Value::String(_) => {
706 items.push(item);
707 }
708 _ => items.push(item),
710 }
711 }
712
713 let mut result = ExecResult::success("");
714 {
715 let mut scope = self.scope.write().await;
716 scope.push_frame();
717 }
718
719 'outer: for item in items {
720 {
721 let mut scope = self.scope.write().await;
722 scope.set(&for_loop.variable, item);
723 }
724 for stmt in &for_loop.body {
725 let mut flow = self.execute_stmt_flow(stmt).await?;
726 match &mut flow {
727 ControlFlow::Normal(r) => {
728 accumulate_result(&mut result, r);
729 if !r.ok() {
730 let scope = self.scope.read().await;
731 if scope.error_exit_enabled() {
732 drop(scope);
733 let mut scope = self.scope.write().await;
734 scope.pop_frame();
735 return Ok(ControlFlow::exit_code(r.code));
736 }
737 }
738 }
739 ControlFlow::Break { .. } => {
740 if flow.decrement_level() {
741 break 'outer;
743 }
744 let mut scope = self.scope.write().await;
746 scope.pop_frame();
747 return Ok(flow);
748 }
749 ControlFlow::Continue { .. } => {
750 if flow.decrement_level() {
751 continue 'outer;
753 }
754 let mut scope = self.scope.write().await;
756 scope.pop_frame();
757 return Ok(flow);
758 }
759 ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
760 let mut scope = self.scope.write().await;
761 scope.pop_frame();
762 return Ok(flow);
763 }
764 }
765 }
766 }
767
768 {
769 let mut scope = self.scope.write().await;
770 scope.pop_frame();
771 }
772 Ok(ControlFlow::ok(result))
773 }
774 Stmt::While(while_loop) => {
775 let mut result = ExecResult::success("");
776
777 'outer: loop {
778 let cond_value = self.eval_expr_async(&while_loop.condition).await?;
780
781 if !is_truthy(&cond_value) {
782 break;
783 }
784
785 for stmt in &while_loop.body {
787 let mut flow = self.execute_stmt_flow(stmt).await?;
788 match &mut flow {
789 ControlFlow::Normal(r) => {
790 accumulate_result(&mut result, r);
791 if !r.ok() {
792 let scope = self.scope.read().await;
793 if scope.error_exit_enabled() {
794 return Ok(ControlFlow::exit_code(r.code));
795 }
796 }
797 }
798 ControlFlow::Break { .. } => {
799 if flow.decrement_level() {
800 break 'outer;
802 }
803 return Ok(flow);
805 }
806 ControlFlow::Continue { .. } => {
807 if flow.decrement_level() {
808 continue 'outer;
810 }
811 return Ok(flow);
813 }
814 ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
815 return Ok(flow);
816 }
817 }
818 }
819 }
820
821 Ok(ControlFlow::ok(result))
822 }
823 Stmt::Case(case_stmt) => {
824 let match_value = {
826 let mut scope = self.scope.write().await;
827 let value = eval_expr(&case_stmt.expr, &mut scope)?;
828 value_to_string(&value)
829 };
830
831 for branch in &case_stmt.branches {
833 let matched = branch.patterns.iter().any(|pattern| {
834 glob_match(pattern, &match_value)
835 });
836
837 if matched {
838 let mut result = ExecResult::success("");
840 for stmt in &branch.body {
841 let flow = self.execute_stmt_flow(stmt).await?;
842 match flow {
843 ControlFlow::Normal(r) => accumulate_result(&mut result, &r),
844 _ => return Ok(flow),
845 }
846 }
847 return Ok(ControlFlow::ok(result));
848 }
849 }
850
851 Ok(ControlFlow::ok(ExecResult::success("")))
853 }
854 Stmt::Break(levels) => {
855 Ok(ControlFlow::break_n(levels.unwrap_or(1)))
856 }
857 Stmt::Continue(levels) => {
858 Ok(ControlFlow::continue_n(levels.unwrap_or(1)))
859 }
860 Stmt::Return(expr) => {
861 let result = if let Some(e) = expr {
864 let mut scope = self.scope.write().await;
865 let val = eval_expr(e, &mut scope)?;
866 let code = match val {
868 Value::Int(n) => n,
869 Value::Bool(b) => if b { 0 } else { 1 },
870 _ => 0,
871 };
872 ExecResult {
873 code,
874 out: String::new(),
875 err: String::new(),
876 data: None,
877 output: None,
878 }
879 } else {
880 ExecResult::success("")
881 };
882 Ok(ControlFlow::return_value(result))
883 }
884 Stmt::Exit(expr) => {
885 let code = if let Some(e) = expr {
886 let mut scope = self.scope.write().await;
887 let val = eval_expr(e, &mut scope)?;
888 match val {
889 Value::Int(n) => n,
890 _ => 0,
891 }
892 } else {
893 0
894 };
895 Ok(ControlFlow::exit_code(code))
896 }
897 Stmt::ToolDef(tool_def) => {
898 let mut user_tools = self.user_tools.write().await;
899 user_tools.insert(tool_def.name.clone(), tool_def.clone());
900 Ok(ControlFlow::ok(ExecResult::success("")))
901 }
902 Stmt::AndChain { left, right } => {
903 let left_flow = self.execute_stmt_flow(left).await?;
905 match left_flow {
906 ControlFlow::Normal(left_result) => {
907 self.update_last_result(&left_result).await;
908 if left_result.ok() {
909 let right_flow = self.execute_stmt_flow(right).await?;
910 match right_flow {
911 ControlFlow::Normal(right_result) => {
912 self.update_last_result(&right_result).await;
913 let mut combined = left_result;
915 accumulate_result(&mut combined, &right_result);
916 Ok(ControlFlow::ok(combined))
917 }
918 other => Ok(other), }
920 } else {
921 Ok(ControlFlow::ok(left_result))
922 }
923 }
924 _ => Ok(left_flow), }
926 }
927 Stmt::OrChain { left, right } => {
928 let left_flow = self.execute_stmt_flow(left).await?;
930 match left_flow {
931 ControlFlow::Normal(left_result) => {
932 self.update_last_result(&left_result).await;
933 if !left_result.ok() {
934 let right_flow = self.execute_stmt_flow(right).await?;
935 match right_flow {
936 ControlFlow::Normal(right_result) => {
937 self.update_last_result(&right_result).await;
938 let mut combined = left_result;
940 accumulate_result(&mut combined, &right_result);
941 Ok(ControlFlow::ok(combined))
942 }
943 other => Ok(other), }
945 } else {
946 Ok(ControlFlow::ok(left_result))
947 }
948 }
949 _ => Ok(left_flow), }
951 }
952 Stmt::Test(test_expr) => {
953 let expr = crate::ast::Expr::Test(Box::new(test_expr.clone()));
955 let mut scope = self.scope.write().await;
956 let value = eval_expr(&expr, &mut scope)?;
957 drop(scope);
958 let is_true = match value {
959 crate::ast::Value::Bool(b) => b,
960 _ => false,
961 };
962 if is_true {
963 Ok(ControlFlow::ok(ExecResult::success("")))
964 } else {
965 Ok(ControlFlow::ok(ExecResult::failure(1, "")))
966 }
967 }
968 Stmt::Empty => Ok(ControlFlow::ok(ExecResult::success(""))),
969 }
970 }.instrument(span))
971 }
972
973 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(background = pipeline.background, command_count = pipeline.commands.len()))]
975 async fn execute_pipeline(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
976 if pipeline.commands.is_empty() {
977 return Ok(ExecResult::success(""));
978 }
979
980 if pipeline.background {
982 return self.execute_background(pipeline).await;
983 }
984
985 let mut ctx = {
993 let ec = self.exec_ctx.read().await;
994 let scope = self.scope.read().await;
995 ExecContext {
996 backend: ec.backend.clone(),
997 scope: scope.clone(),
998 cwd: ec.cwd.clone(),
999 prev_cwd: ec.prev_cwd.clone(),
1000 stdin: None,
1001 stdin_data: None,
1002 pipe_stdin: None,
1003 pipe_stdout: None,
1004 stderr: ec.stderr.clone(),
1005 tool_schemas: ec.tool_schemas.clone(),
1006 tools: ec.tools.clone(),
1007 job_manager: ec.job_manager.clone(),
1008 pipeline_position: PipelinePosition::Only,
1009 interactive: self.interactive,
1010 aliases: ec.aliases.clone(),
1011 #[cfg(unix)]
1012 terminal_state: ec.terminal_state.clone(),
1013 }
1014 }; let result = self.runner.run(&pipeline.commands, &mut ctx, self).await;
1017
1018 {
1020 let mut ec = self.exec_ctx.write().await;
1021 ec.cwd = ctx.cwd.clone();
1022 ec.prev_cwd = ctx.prev_cwd.clone();
1023 ec.aliases = ctx.aliases.clone();
1024 }
1025 {
1026 let mut scope = self.scope.write().await;
1027 *scope = ctx.scope.clone();
1028 }
1029
1030 Ok(result)
1031 }
1032
1033 #[tracing::instrument(level = "debug", skip(self, pipeline), fields(command_count = pipeline.commands.len()))]
1041 async fn execute_background(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
1042 use tokio::sync::oneshot;
1043
1044 let command_str = self.format_pipeline(pipeline);
1046
1047 let stdout = Arc::new(BoundedStream::default_size());
1049 let stderr = Arc::new(BoundedStream::default_size());
1050
1051 let (tx, rx) = oneshot::channel();
1053
1054 let job_id = self.jobs.register_with_streams(
1056 command_str.clone(),
1057 rx,
1058 stdout.clone(),
1059 stderr.clone(),
1060 ).await;
1061
1062 let runner = self.runner.clone();
1064 let commands = pipeline.commands.clone();
1065 let backend = {
1066 let ctx = self.exec_ctx.read().await;
1067 ctx.backend.clone()
1068 };
1069 let scope = {
1070 let scope = self.scope.read().await;
1071 scope.clone()
1072 };
1073 let cwd = {
1074 let ctx = self.exec_ctx.read().await;
1075 ctx.cwd.clone()
1076 };
1077 let tools = self.tools.clone();
1078 let tool_schemas = self.tools.schemas();
1079
1080 tokio::spawn(async move {
1082 let mut bg_ctx = ExecContext::with_backend(backend);
1085 bg_ctx.scope = scope;
1086 bg_ctx.cwd = cwd;
1087 bg_ctx.set_tools(tools.clone());
1088 bg_ctx.set_tool_schemas(tool_schemas);
1089
1090 let dispatcher = crate::dispatch::BackendDispatcher::new(tools);
1093
1094 let result = runner.run(&commands, &mut bg_ctx, &dispatcher).await;
1096
1097 if !result.out.is_empty() {
1099 stdout.write(result.out.as_bytes()).await;
1100 }
1101 if !result.err.is_empty() {
1102 stderr.write(result.err.as_bytes()).await;
1103 }
1104
1105 stdout.close().await;
1107 stderr.close().await;
1108
1109 let _ = tx.send(result);
1111 });
1112
1113 Ok(ExecResult::success(format!("[{}]", job_id)))
1114 }
1115
1116 fn format_pipeline(&self, pipeline: &crate::ast::Pipeline) -> String {
1118 pipeline.commands
1119 .iter()
1120 .map(|cmd| {
1121 let mut parts = vec![cmd.name.clone()];
1122 for arg in &cmd.args {
1123 match arg {
1124 Arg::Positional(expr) => {
1125 parts.push(self.format_expr(expr));
1126 }
1127 Arg::Named { key, value } => {
1128 parts.push(format!("{}={}", key, self.format_expr(value)));
1129 }
1130 Arg::ShortFlag(name) => {
1131 parts.push(format!("-{}", name));
1132 }
1133 Arg::LongFlag(name) => {
1134 parts.push(format!("--{}", name));
1135 }
1136 Arg::DoubleDash => {
1137 parts.push("--".to_string());
1138 }
1139 }
1140 }
1141 parts.join(" ")
1142 })
1143 .collect::<Vec<_>>()
1144 .join(" | ")
1145 }
1146
1147 fn format_expr(&self, expr: &Expr) -> String {
1149 match expr {
1150 Expr::Literal(Value::String(s)) => {
1151 if s.contains(' ') || s.contains('"') {
1152 format!("'{}'", s.replace('\'', "\\'"))
1153 } else {
1154 s.clone()
1155 }
1156 }
1157 Expr::Literal(Value::Int(i)) => i.to_string(),
1158 Expr::Literal(Value::Float(f)) => f.to_string(),
1159 Expr::Literal(Value::Bool(b)) => b.to_string(),
1160 Expr::Literal(Value::Null) => "null".to_string(),
1161 Expr::VarRef(path) => {
1162 let name = path.segments.iter()
1163 .map(|seg| match seg {
1164 crate::ast::VarSegment::Field(f) => f.clone(),
1165 })
1166 .collect::<Vec<_>>()
1167 .join(".");
1168 format!("${{{}}}", name)
1169 }
1170 Expr::Interpolated(_) => "\"...\"".to_string(),
1171 _ => "...".to_string(),
1172 }
1173 }
1174
1175 async fn execute_command(&self, name: &str, args: &[Arg]) -> Result<ExecResult> {
1177 self.execute_command_depth(name, args, 0).await
1178 }
1179
1180 #[tracing::instrument(level = "info", skip(self, args, alias_depth), fields(command = %name), err)]
1181 async fn execute_command_depth(&self, name: &str, args: &[Arg], alias_depth: u8) -> Result<ExecResult> {
1182 match name {
1184 "true" => return Ok(ExecResult::success("")),
1185 "false" => return Ok(ExecResult::failure(1, "")),
1186 "source" | "." => return self.execute_source(args).await,
1187 _ => {}
1188 }
1189
1190 if alias_depth < 10 {
1192 let alias_value = {
1193 let ctx = self.exec_ctx.read().await;
1194 ctx.aliases.get(name).cloned()
1195 };
1196 if let Some(alias_val) = alias_value {
1197 let parts: Vec<&str> = alias_val.split_whitespace().collect();
1199 if let Some((alias_cmd, alias_args)) = parts.split_first() {
1200 let mut new_args: Vec<Arg> = alias_args
1201 .iter()
1202 .map(|a| Arg::Positional(Expr::Literal(Value::String(a.to_string()))))
1203 .collect();
1204 new_args.extend_from_slice(args);
1205 return Box::pin(self.execute_command_depth(alias_cmd, &new_args, alias_depth + 1)).await;
1206 }
1207 }
1208 }
1209
1210 if let Some(builtin_name) = name.strip_prefix("/v/bin/") {
1212 return match self.tools.get(builtin_name) {
1213 Some(_) => Box::pin(self.execute_command_depth(builtin_name, args, alias_depth)).await,
1214 None => Ok(ExecResult::failure(127, format!("command not found: {}", name))),
1215 };
1216 }
1217
1218 {
1220 let user_tools = self.user_tools.read().await;
1221 if let Some(tool_def) = user_tools.get(name) {
1222 let tool_def = tool_def.clone();
1223 drop(user_tools);
1224 return self.execute_user_tool(tool_def, args).await;
1225 }
1226 }
1227
1228 let tool = match self.tools.get(name) {
1230 Some(t) => t,
1231 None => {
1232 if let Some(result) = self.try_execute_script(name, args).await? {
1234 return Ok(result);
1235 }
1236 if let Some(result) = self.try_execute_external(name, args).await? {
1238 return Ok(result);
1239 }
1240
1241 let backend = self.exec_ctx.read().await.backend.clone();
1246 let tool_schema = backend.get_tool(name).await.ok().flatten().map(|t| {
1247 let mut s = t.schema;
1248 s.map_positionals = true;
1249 s
1250 });
1251 let tool_args = self.build_args_async(args, tool_schema.as_ref()).await?;
1252 let mut ctx = self.exec_ctx.write().await;
1253 {
1254 let scope = self.scope.read().await;
1255 ctx.scope = scope.clone();
1256 }
1257 let backend = ctx.backend.clone();
1258 match backend.call_tool(name, tool_args, &mut ctx).await {
1259 Ok(tool_result) => {
1260 let mut scope = self.scope.write().await;
1261 *scope = ctx.scope.clone();
1262 let mut exec = ExecResult::from_output(
1263 tool_result.code as i64, tool_result.stdout, tool_result.stderr,
1264 );
1265 exec.output = tool_result.output;
1266 return Ok(exec);
1267 }
1268 Err(BackendError::ToolNotFound(_)) => {
1269 }
1271 Err(e) => {
1272 tracing::debug!("backend error for {name}: {e}");
1275 }
1276 }
1277
1278 return Ok(ExecResult::failure(127, format!("command not found: {}", name)));
1279 }
1280 };
1281
1282 let schema = tool.schema();
1284 let mut tool_args = self.build_args_async(args, Some(&schema)).await?;
1285 let output_format = extract_output_format(&mut tool_args, Some(&schema));
1286
1287 let schema_claims = |flag: &str| -> bool {
1289 let bare = flag.trim_start_matches('-');
1290 schema.params.iter().any(|p| p.matches_flag(flag) || p.matches_flag(bare))
1291 };
1292 let wants_help =
1293 (tool_args.flags.contains("help") && !schema_claims("help"))
1294 || (tool_args.flags.contains("h") && !schema_claims("-h"));
1295 if wants_help {
1296 let help_topic = crate::help::HelpTopic::Tool(name.to_string());
1297 let ctx = self.exec_ctx.read().await;
1298 let content = crate::help::get_help(&help_topic, &ctx.tool_schemas);
1299 return Ok(ExecResult::with_output(crate::interpreter::OutputData::text(content)));
1300 }
1301
1302 let mut ctx = self.exec_ctx.write().await;
1304 {
1305 let scope = self.scope.read().await;
1306 ctx.scope = scope.clone();
1307 }
1308
1309 let result = tool.execute(tool_args, &mut ctx).await;
1310
1311 {
1313 let mut scope = self.scope.write().await;
1314 *scope = ctx.scope.clone();
1315 }
1316
1317 let result = match output_format {
1318 Some(format) => apply_output_format(result, format),
1319 None => result,
1320 };
1321
1322 Ok(result)
1323 }
1324
1325 async fn build_args_async(&self, args: &[Arg], schema: Option<&crate::tools::ToolSchema>) -> Result<ToolArgs> {
1329 let mut tool_args = ToolArgs::new();
1330 let param_lookup = schema.map(schema_param_lookup).unwrap_or_default();
1331
1332 let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
1334 let mut past_double_dash = false;
1335
1336 let positional_indices: Vec<usize> = args.iter().enumerate()
1338 .filter_map(|(i, a)| matches!(a, Arg::Positional(_)).then_some(i))
1339 .collect();
1340
1341 let mut i = 0;
1342 while i < args.len() {
1343 match &args[i] {
1344 Arg::DoubleDash => {
1345 past_double_dash = true;
1346 }
1347 Arg::Positional(expr) => {
1348 if !consumed.contains(&i) {
1349 let value = self.eval_expr_async(expr).await?;
1350 let value = apply_tilde_expansion(value);
1351 tool_args.positional.push(value);
1352 }
1353 }
1354 Arg::Named { key, value } => {
1355 let val = self.eval_expr_async(value).await?;
1356 let val = apply_tilde_expansion(val);
1357 tool_args.named.insert(key.clone(), val);
1358 }
1359 Arg::ShortFlag(name) => {
1360 if past_double_dash {
1361 tool_args.positional.push(Value::String(format!("-{name}")));
1362 } else if name.len() == 1 {
1363 let flag_name = name.as_str();
1364 let lookup = param_lookup.get(flag_name);
1365 let is_bool = lookup.map(|(_, typ)| is_bool_type(typ)).unwrap_or(true);
1366
1367 if is_bool {
1368 tool_args.flags.insert(flag_name.to_string());
1369 } else {
1370 let canonical = lookup.map(|(name, _)| *name).unwrap_or(flag_name);
1372 let next_pos = positional_indices.iter()
1373 .find(|idx| **idx > i && !consumed.contains(idx));
1374
1375 if let Some(&pos_idx) = next_pos {
1376 if let Arg::Positional(expr) = &args[pos_idx] {
1377 let value = self.eval_expr_async(expr).await?;
1378 let value = apply_tilde_expansion(value);
1379 tool_args.named.insert(canonical.to_string(), value);
1380 consumed.insert(pos_idx);
1381 }
1382 } else {
1383 tool_args.flags.insert(flag_name.to_string());
1384 }
1385 }
1386 } else {
1387 for c in name.chars() {
1389 tool_args.flags.insert(c.to_string());
1390 }
1391 }
1392 }
1393 Arg::LongFlag(name) => {
1394 if past_double_dash {
1395 tool_args.positional.push(Value::String(format!("--{name}")));
1396 } else {
1397 let lookup = param_lookup.get(name.as_str());
1398 let is_bool = lookup.map(|(_, typ)| is_bool_type(typ)).unwrap_or(true);
1399
1400 if is_bool {
1401 tool_args.flags.insert(name.clone());
1402 } else {
1403 let canonical = lookup.map(|(name, _)| *name).unwrap_or(name.as_str());
1404 let next_pos = positional_indices.iter()
1405 .find(|idx| **idx > i && !consumed.contains(idx));
1406
1407 if let Some(&pos_idx) = next_pos {
1408 if let Arg::Positional(expr) = &args[pos_idx] {
1409 let value = self.eval_expr_async(expr).await?;
1410 let value = apply_tilde_expansion(value);
1411 tool_args.named.insert(canonical.to_string(), value);
1412 consumed.insert(pos_idx);
1413 }
1414 } else {
1415 tool_args.flags.insert(name.clone());
1416 }
1417 }
1418 }
1419 }
1420 }
1421 i += 1;
1422 }
1423
1424 if let Some(schema) = schema.filter(|s| s.map_positionals) {
1429 let pre_dash_count = if past_double_dash {
1430 let dash_pos = args.iter().position(|a| matches!(a, Arg::DoubleDash)).unwrap_or(args.len());
1431 positional_indices.iter()
1432 .filter(|idx| **idx < dash_pos && !consumed.contains(idx))
1433 .count()
1434 } else {
1435 tool_args.positional.len()
1436 };
1437
1438 let mut remaining = Vec::new();
1439 let mut positional_iter = tool_args.positional.drain(..).enumerate();
1440
1441 for param in &schema.params {
1442 if tool_args.named.contains_key(¶m.name) || tool_args.flags.contains(¶m.name) {
1443 continue;
1444 }
1445 if is_bool_type(¶m.param_type) {
1446 continue;
1447 }
1448 loop {
1449 match positional_iter.next() {
1450 Some((idx, val)) if idx < pre_dash_count => {
1451 tool_args.named.insert(param.name.clone(), val);
1452 break;
1453 }
1454 Some((_, val)) => {
1455 remaining.push(val);
1456 }
1457 None => break,
1458 }
1459 }
1460 }
1461
1462 remaining.extend(positional_iter.map(|(_, v)| v));
1463 tool_args.positional = remaining;
1464 }
1465
1466 Ok(tool_args)
1467 }
1468
1469 async fn build_args_flat(&self, args: &[Arg]) -> Result<Vec<String>> {
1479 let mut argv = Vec::new();
1480 for arg in args {
1481 match arg {
1482 Arg::Positional(expr) => {
1483 let value = self.eval_expr_async(expr).await?;
1484 let value = apply_tilde_expansion(value);
1485 argv.push(value_to_string(&value));
1486 }
1487 Arg::Named { key, value } => {
1488 let val = self.eval_expr_async(value).await?;
1489 let val = apply_tilde_expansion(val);
1490 argv.push(format!("{}={}", key, value_to_string(&val)));
1491 }
1492 Arg::ShortFlag(name) => {
1493 argv.push(format!("-{}", name));
1495 }
1496 Arg::LongFlag(name) => {
1497 argv.push(format!("--{}", name));
1499 }
1500 Arg::DoubleDash => {
1501 argv.push("--".to_string());
1503 }
1504 }
1505 }
1506 Ok(argv)
1507 }
1508
1509 fn eval_expr_async<'a>(&'a self, expr: &'a Expr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + Send + 'a>> {
1514 Box::pin(async move {
1515 match expr {
1516 Expr::Literal(value) => Ok(value.clone()),
1517 Expr::VarRef(path) => {
1518 let scope = self.scope.read().await;
1519 scope.resolve_path(path)
1520 .ok_or_else(|| anyhow::anyhow!("undefined variable"))
1521 }
1522 Expr::Interpolated(parts) => {
1523 let mut result = String::new();
1524 for part in parts {
1525 result.push_str(&self.eval_string_part_async(part).await?);
1526 }
1527 Ok(Value::String(result))
1528 }
1529 Expr::BinaryOp { left, op, right } => {
1530 match op {
1531 BinaryOp::And => {
1532 let left_val = self.eval_expr_async(left).await?;
1533 if !is_truthy(&left_val) {
1534 return Ok(left_val);
1535 }
1536 self.eval_expr_async(right).await
1537 }
1538 BinaryOp::Or => {
1539 let left_val = self.eval_expr_async(left).await?;
1540 if is_truthy(&left_val) {
1541 return Ok(left_val);
1542 }
1543 self.eval_expr_async(right).await
1544 }
1545 _ => {
1546 let mut scope = self.scope.write().await;
1548 eval_expr(expr, &mut scope).map_err(|e| anyhow::anyhow!("{}", e))
1549 }
1550 }
1551 }
1552 Expr::CommandSubst(pipeline) => {
1553 let saved_scope = { self.scope.read().await.clone() };
1556 let saved_cwd = {
1557 let ec = self.exec_ctx.read().await;
1558 (ec.cwd.clone(), ec.prev_cwd.clone())
1559 };
1560
1561 let run_result = self.execute_pipeline(pipeline).await;
1563
1564 {
1566 let mut scope = self.scope.write().await;
1567 *scope = saved_scope;
1568 if let Ok(ref r) = run_result {
1569 scope.set_last_result(r.clone());
1570 }
1571 }
1572 {
1573 let mut ec = self.exec_ctx.write().await;
1574 ec.cwd = saved_cwd.0;
1575 ec.prev_cwd = saved_cwd.1;
1576 }
1577
1578 let result = run_result?;
1580
1581 if let Some(data) = &result.data {
1583 Ok(data.clone())
1584 } else if let Some(ref output) = result.output {
1585 if output.is_flat() && !output.is_simple_text() && !output.root.is_empty() {
1587 let items: Vec<serde_json::Value> = output.root.iter()
1588 .map(|n| serde_json::Value::String(n.display_name().to_string()))
1589 .collect();
1590 Ok(Value::Json(serde_json::Value::Array(items)))
1591 } else {
1592 Ok(Value::String(result.out.trim_end().to_string()))
1593 }
1594 } else {
1595 Ok(Value::String(result.out.trim_end().to_string()))
1597 }
1598 }
1599 Expr::Test(test_expr) => {
1600 let expr = Expr::Test(test_expr.clone());
1602 let mut scope = self.scope.write().await;
1603 eval_expr(&expr, &mut scope).map_err(|e| anyhow::anyhow!("{}", e))
1604 }
1605 Expr::Positional(n) => {
1606 let scope = self.scope.read().await;
1607 match scope.get_positional(*n) {
1608 Some(s) => Ok(Value::String(s.to_string())),
1609 None => Ok(Value::String(String::new())),
1610 }
1611 }
1612 Expr::AllArgs => {
1613 let scope = self.scope.read().await;
1614 Ok(Value::String(scope.all_args().join(" ")))
1615 }
1616 Expr::ArgCount => {
1617 let scope = self.scope.read().await;
1618 Ok(Value::Int(scope.arg_count() as i64))
1619 }
1620 Expr::VarLength(name) => {
1621 let scope = self.scope.read().await;
1622 match scope.get(name) {
1623 Some(value) => Ok(Value::Int(value_to_string(value).len() as i64)),
1624 None => Ok(Value::Int(0)),
1625 }
1626 }
1627 Expr::VarWithDefault { name, default } => {
1628 let scope = self.scope.read().await;
1629 let use_default = match scope.get(name) {
1630 Some(value) => value_to_string(value).is_empty(),
1631 None => true,
1632 };
1633 drop(scope); if use_default {
1635 self.eval_string_parts_async(default).await.map(Value::String)
1637 } else {
1638 let scope = self.scope.read().await;
1639 scope.get(name).cloned().ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))
1640 }
1641 }
1642 Expr::Arithmetic(expr_str) => {
1643 let scope = self.scope.read().await;
1644 crate::arithmetic::eval_arithmetic(expr_str, &scope)
1645 .map(Value::Int)
1646 .map_err(|e| anyhow::anyhow!("arithmetic error: {}", e))
1647 }
1648 Expr::Command(cmd) => {
1649 let result = self.execute_command(&cmd.name, &cmd.args).await?;
1651 Ok(Value::Bool(result.code == 0))
1652 }
1653 Expr::LastExitCode => {
1654 let scope = self.scope.read().await;
1655 Ok(Value::Int(scope.last_result().code))
1656 }
1657 Expr::CurrentPid => {
1658 let scope = self.scope.read().await;
1659 Ok(Value::Int(scope.pid() as i64))
1660 }
1661 }
1662 })
1663 }
1664
1665 fn eval_string_parts_async<'a>(&'a self, parts: &'a [StringPart]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
1667 Box::pin(async move {
1668 let mut result = String::new();
1669 for part in parts {
1670 result.push_str(&self.eval_string_part_async(part).await?);
1671 }
1672 Ok(result)
1673 })
1674 }
1675
1676 fn eval_string_part_async<'a>(&'a self, part: &'a StringPart) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
1678 Box::pin(async move {
1679 match part {
1680 StringPart::Literal(s) => Ok(s.clone()),
1681 StringPart::Var(path) => {
1682 let scope = self.scope.read().await;
1683 match scope.resolve_path(path) {
1684 Some(value) => Ok(value_to_string(&value)),
1685 None => Ok(String::new()), }
1687 }
1688 StringPart::VarWithDefault { name, default } => {
1689 let scope = self.scope.read().await;
1690 let use_default = match scope.get(name) {
1691 Some(value) => value_to_string(value).is_empty(),
1692 None => true,
1693 };
1694 drop(scope); if use_default {
1696 self.eval_string_parts_async(default).await
1698 } else {
1699 let scope = self.scope.read().await;
1700 Ok(value_to_string(scope.get(name).ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))?))
1701 }
1702 }
1703 StringPart::VarLength(name) => {
1704 let scope = self.scope.read().await;
1705 match scope.get(name) {
1706 Some(value) => Ok(value_to_string(value).len().to_string()),
1707 None => Ok("0".to_string()),
1708 }
1709 }
1710 StringPart::Positional(n) => {
1711 let scope = self.scope.read().await;
1712 match scope.get_positional(*n) {
1713 Some(s) => Ok(s.to_string()),
1714 None => Ok(String::new()),
1715 }
1716 }
1717 StringPart::AllArgs => {
1718 let scope = self.scope.read().await;
1719 Ok(scope.all_args().join(" "))
1720 }
1721 StringPart::ArgCount => {
1722 let scope = self.scope.read().await;
1723 Ok(scope.arg_count().to_string())
1724 }
1725 StringPart::Arithmetic(expr) => {
1726 let scope = self.scope.read().await;
1727 match crate::arithmetic::eval_arithmetic(expr, &scope) {
1728 Ok(value) => Ok(value.to_string()),
1729 Err(_) => Ok(String::new()),
1730 }
1731 }
1732 StringPart::CommandSubst(pipeline) => {
1733 let saved_scope = { self.scope.read().await.clone() };
1736 let saved_cwd = {
1737 let ec = self.exec_ctx.read().await;
1738 (ec.cwd.clone(), ec.prev_cwd.clone())
1739 };
1740
1741 let run_result = self.execute_pipeline(pipeline).await;
1743
1744 {
1746 let mut scope = self.scope.write().await;
1747 *scope = saved_scope;
1748 if let Ok(ref r) = run_result {
1749 scope.set_last_result(r.clone());
1750 }
1751 }
1752 {
1753 let mut ec = self.exec_ctx.write().await;
1754 ec.cwd = saved_cwd.0;
1755 ec.prev_cwd = saved_cwd.1;
1756 }
1757
1758 let result = run_result?;
1760
1761 Ok(result.out.trim_end_matches('\n').to_string())
1762 }
1763 StringPart::LastExitCode => {
1764 let scope = self.scope.read().await;
1765 Ok(scope.last_result().code.to_string())
1766 }
1767 StringPart::CurrentPid => {
1768 let scope = self.scope.read().await;
1769 Ok(scope.pid().to_string())
1770 }
1771 }
1772 })
1773 }
1774
1775 async fn update_last_result(&self, result: &ExecResult) {
1777 let mut scope = self.scope.write().await;
1778 scope.set_last_result(result.clone());
1779 }
1780
1781 async fn execute_user_tool(&self, def: ToolDef, args: &[Arg]) -> Result<ExecResult> {
1787 let tool_args = self.build_args_async(args, None).await?;
1789
1790 {
1792 let mut scope = self.scope.write().await;
1793 scope.push_frame();
1794 }
1795
1796 let saved_positional = {
1798 let mut scope = self.scope.write().await;
1799 let saved = scope.save_positional();
1800
1801 let positional_args: Vec<String> = tool_args.positional
1803 .iter()
1804 .map(value_to_string)
1805 .collect();
1806 scope.set_positional(&def.name, positional_args);
1807
1808 saved
1809 };
1810
1811 let mut accumulated_out = String::new();
1814 let mut accumulated_err = String::new();
1815 let mut last_code = 0i64;
1816 let mut last_data: Option<Value> = None;
1817
1818 let mut exec_error: Option<anyhow::Error> = None;
1820 let mut exit_code: Option<i64> = None;
1821
1822 for stmt in &def.body {
1823 match self.execute_stmt_flow(stmt).await {
1824 Ok(flow) => {
1825 match flow {
1826 ControlFlow::Normal(r) => {
1827 accumulated_out.push_str(&r.out);
1828 accumulated_err.push_str(&r.err);
1829 last_code = r.code;
1830 last_data = r.data;
1831 }
1832 ControlFlow::Return { value } => {
1833 accumulated_out.push_str(&value.out);
1834 accumulated_err.push_str(&value.err);
1835 last_code = value.code;
1836 last_data = value.data;
1837 break;
1838 }
1839 ControlFlow::Exit { code } => {
1840 exit_code = Some(code);
1841 break;
1842 }
1843 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
1844 accumulated_out.push_str(&r.out);
1845 accumulated_err.push_str(&r.err);
1846 last_code = r.code;
1847 last_data = r.data;
1848 }
1849 }
1850 }
1851 Err(e) => {
1852 exec_error = Some(e);
1853 break;
1854 }
1855 }
1856 }
1857
1858 {
1860 let mut scope = self.scope.write().await;
1861 scope.pop_frame();
1862 scope.set_positional(saved_positional.0, saved_positional.1);
1863 }
1864
1865 if let Some(e) = exec_error {
1867 return Err(e);
1868 }
1869 if let Some(code) = exit_code {
1870 return Ok(ExecResult {
1871 code,
1872 out: accumulated_out,
1873 err: accumulated_err,
1874 data: last_data,
1875 output: None,
1876 });
1877 }
1878
1879 Ok(ExecResult {
1880 code: last_code,
1881 out: accumulated_out,
1882 err: accumulated_err,
1883 data: last_data,
1884 output: None,
1885 })
1886 }
1887
1888 async fn execute_source(&self, args: &[Arg]) -> Result<ExecResult> {
1893 let tool_args = self.build_args_async(args, None).await?;
1895 let path = match tool_args.positional.first() {
1896 Some(Value::String(s)) => s.clone(),
1897 Some(v) => value_to_string(v),
1898 None => {
1899 return Ok(ExecResult::failure(1, "source: missing filename"));
1900 }
1901 };
1902
1903 let full_path = {
1905 let ctx = self.exec_ctx.read().await;
1906 if path.starts_with('/') {
1907 std::path::PathBuf::from(&path)
1908 } else {
1909 ctx.cwd.join(&path)
1910 }
1911 };
1912
1913 let content = {
1915 let ctx = self.exec_ctx.read().await;
1916 match ctx.backend.read(&full_path, None).await {
1917 Ok(bytes) => {
1918 String::from_utf8(bytes).map_err(|e| {
1919 anyhow::anyhow!("source: {}: invalid UTF-8: {}", path, e)
1920 })?
1921 }
1922 Err(e) => {
1923 return Ok(ExecResult::failure(
1924 1,
1925 format!("source: {}: {}", path, e),
1926 ));
1927 }
1928 }
1929 };
1930
1931 let program = match crate::parser::parse(&content) {
1933 Ok(p) => p,
1934 Err(errors) => {
1935 let msg = errors
1936 .iter()
1937 .map(|e| format!("{}:{}: {}", path, e.span.start, e.message))
1938 .collect::<Vec<_>>()
1939 .join("\n");
1940 return Ok(ExecResult::failure(1, format!("source: {}", msg)));
1941 }
1942 };
1943
1944 let mut result = ExecResult::success("");
1946 for stmt in program.statements {
1947 if matches!(stmt, crate::ast::Stmt::Empty) {
1948 continue;
1949 }
1950
1951 match self.execute_stmt_flow(&stmt).await {
1952 Ok(flow) => {
1953 match flow {
1954 ControlFlow::Normal(r) => {
1955 result = r.clone();
1956 self.update_last_result(&r).await;
1957 }
1958 ControlFlow::Break { .. } | ControlFlow::Continue { .. } => {
1959 return Err(anyhow::anyhow!(
1961 "source: {}: unexpected break/continue outside loop",
1962 path
1963 ));
1964 }
1965 ControlFlow::Return { value } => {
1966 return Ok(value);
1968 }
1969 ControlFlow::Exit { code } => {
1970 result.code = code;
1972 return Ok(result);
1973 }
1974 }
1975 }
1976 Err(e) => {
1977 return Err(e.context(format!("source: {}", path)));
1978 }
1979 }
1980 }
1981
1982 Ok(result)
1983 }
1984
1985 async fn try_execute_script(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
1990 let path_value = {
1992 let scope = self.scope.read().await;
1993 scope
1994 .get("PATH")
1995 .map(value_to_string)
1996 .unwrap_or_else(|| "/bin".to_string())
1997 };
1998
1999 for dir in path_value.split(':') {
2001 if dir.is_empty() {
2002 continue;
2003 }
2004
2005 let script_path = PathBuf::from(dir).join(format!("{}.kai", name));
2007
2008 let exists = {
2010 let ctx = self.exec_ctx.read().await;
2011 ctx.backend.exists(&script_path).await
2012 };
2013
2014 if !exists {
2015 continue;
2016 }
2017
2018 let content = {
2020 let ctx = self.exec_ctx.read().await;
2021 match ctx.backend.read(&script_path, None).await {
2022 Ok(bytes) => match String::from_utf8(bytes) {
2023 Ok(s) => s,
2024 Err(e) => {
2025 return Ok(Some(ExecResult::failure(
2026 1,
2027 format!("{}: invalid UTF-8: {}", script_path.display(), e),
2028 )));
2029 }
2030 },
2031 Err(e) => {
2032 return Ok(Some(ExecResult::failure(
2033 1,
2034 format!("{}: {}", script_path.display(), e),
2035 )));
2036 }
2037 }
2038 };
2039
2040 let program = match crate::parser::parse(&content) {
2042 Ok(p) => p,
2043 Err(errors) => {
2044 let msg = errors
2045 .iter()
2046 .map(|e| format!("{}:{}: {}", script_path.display(), e.span.start, e.message))
2047 .collect::<Vec<_>>()
2048 .join("\n");
2049 return Ok(Some(ExecResult::failure(1, msg)));
2050 }
2051 };
2052
2053 let tool_args = self.build_args_async(args, None).await?;
2055
2056 let mut isolated_scope = Scope::new();
2058
2059 let positional_args: Vec<String> = tool_args.positional
2061 .iter()
2062 .map(value_to_string)
2063 .collect();
2064 isolated_scope.set_positional(name, positional_args);
2065
2066 let original_scope = {
2068 let mut scope = self.scope.write().await;
2069 std::mem::replace(&mut *scope, isolated_scope)
2070 };
2071
2072 let mut result = ExecResult::success("");
2074 let mut exec_error: Option<anyhow::Error> = None;
2075 let mut exit_code: Option<i64> = None;
2076
2077 for stmt in program.statements {
2078 if matches!(stmt, crate::ast::Stmt::Empty) {
2079 continue;
2080 }
2081
2082 match self.execute_stmt_flow(&stmt).await {
2083 Ok(flow) => {
2084 match flow {
2085 ControlFlow::Normal(r) => result = r,
2086 ControlFlow::Return { value } => {
2087 result = value;
2088 break;
2089 }
2090 ControlFlow::Exit { code } => {
2091 exit_code = Some(code);
2092 break;
2093 }
2094 ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
2095 result = r;
2096 }
2097 }
2098 }
2099 Err(e) => {
2100 exec_error = Some(e);
2101 break;
2102 }
2103 }
2104 }
2105
2106 {
2108 let mut scope = self.scope.write().await;
2109 *scope = original_scope;
2110 }
2111
2112 if let Some(e) = exec_error {
2114 return Err(e.context(format!("script: {}", script_path.display())));
2115 }
2116 if let Some(code) = exit_code {
2117 result.code = code;
2118 return Ok(Some(result));
2119 }
2120
2121 return Ok(Some(result));
2122 }
2123
2124 Ok(None)
2126 }
2127
2128 #[tracing::instrument(level = "debug", skip(self, args), fields(command = %name))]
2142 async fn try_execute_external(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
2143 let real_cwd = {
2145 let ctx = self.exec_ctx.read().await;
2146 match ctx.backend.resolve_real_path(&ctx.cwd) {
2147 Some(p) => p,
2148 None => {
2149 return Ok(Some(ExecResult::failure(
2150 1,
2151 format!(
2152 "{}: cannot run external command from virtual directory '{}'",
2153 name,
2154 ctx.cwd.display()
2155 ),
2156 )));
2157 }
2158 }
2159 };
2160
2161 let executable = if name.contains('/') {
2162 let resolved = if std::path::Path::new(name).is_absolute() {
2164 std::path::PathBuf::from(name)
2165 } else {
2166 real_cwd.join(name)
2167 };
2168 if !resolved.exists() {
2169 return Ok(Some(ExecResult::failure(
2170 127,
2171 format!("{}: No such file or directory", name),
2172 )));
2173 }
2174 if !resolved.is_file() {
2175 return Ok(Some(ExecResult::failure(
2176 126,
2177 format!("{}: Is a directory", name),
2178 )));
2179 }
2180 #[cfg(unix)]
2181 {
2182 use std::os::unix::fs::PermissionsExt;
2183 let mode = std::fs::metadata(&resolved)
2184 .map(|m| m.permissions().mode())
2185 .unwrap_or(0);
2186 if mode & 0o111 == 0 {
2187 return Ok(Some(ExecResult::failure(
2188 126,
2189 format!("{}: Permission denied", name),
2190 )));
2191 }
2192 }
2193 resolved.to_string_lossy().into_owned()
2194 } else {
2195 let path_var = {
2197 let scope = self.scope.read().await;
2198 scope
2199 .get("PATH")
2200 .map(value_to_string)
2201 .unwrap_or_else(|| std::env::var("PATH").unwrap_or_default())
2202 };
2203
2204 match resolve_in_path(name, &path_var) {
2206 Some(path) => path,
2207 None => return Ok(None), }
2209 };
2210
2211 tracing::debug!(executable = %executable, "resolved external command");
2212
2213 let argv = self.build_args_flat(args).await?;
2215
2216 let stdin_data = {
2218 let mut ctx = self.exec_ctx.write().await;
2219 ctx.take_stdin()
2220 };
2221
2222 use tokio::process::Command;
2224
2225 let mut cmd = Command::new(&executable);
2226 cmd.args(&argv);
2227 cmd.current_dir(&real_cwd);
2228
2229 cmd.stdin(if stdin_data.is_some() {
2231 std::process::Stdio::piped()
2232 } else if self.interactive {
2233 std::process::Stdio::inherit()
2234 } else {
2235 std::process::Stdio::null()
2236 });
2237
2238 let pipeline_position = {
2242 let ctx = self.exec_ctx.read().await;
2243 ctx.pipeline_position
2244 };
2245 let inherit_output = self.interactive
2246 && matches!(pipeline_position, PipelinePosition::Only | PipelinePosition::Last);
2247
2248 if inherit_output {
2249 cmd.stdout(std::process::Stdio::inherit());
2250 cmd.stderr(std::process::Stdio::inherit());
2251 } else {
2252 cmd.stdout(std::process::Stdio::piped());
2253 cmd.stderr(std::process::Stdio::piped());
2254 }
2255
2256 #[cfg(unix)]
2260 if self.terminal_state.is_some() && inherit_output {
2261 #[allow(unsafe_code)]
2263 unsafe {
2264 cmd.pre_exec(|| {
2265 nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))
2267 .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
2268 use nix::libc::{sigaction, SIGTSTP, SIGTTOU, SIGTTIN, SIGINT, SIG_DFL};
2270 let mut sa: nix::libc::sigaction = std::mem::zeroed();
2271 sa.sa_sigaction = SIG_DFL;
2272 sigaction(SIGTSTP, &sa, std::ptr::null_mut());
2273 sigaction(SIGTTOU, &sa, std::ptr::null_mut());
2274 sigaction(SIGTTIN, &sa, std::ptr::null_mut());
2275 sigaction(SIGINT, &sa, std::ptr::null_mut());
2276 Ok(())
2277 });
2278 }
2279 }
2280
2281 let mut child = match cmd.spawn() {
2283 Ok(child) => child,
2284 Err(e) => {
2285 return Ok(Some(ExecResult::failure(
2286 127,
2287 format!("{}: {}", name, e),
2288 )));
2289 }
2290 };
2291
2292 if let Some(data) = stdin_data
2294 && let Some(mut stdin) = child.stdin.take()
2295 {
2296 use tokio::io::AsyncWriteExt;
2297 if let Err(e) = stdin.write_all(data.as_bytes()).await {
2298 return Ok(Some(ExecResult::failure(
2299 1,
2300 format!("{}: failed to write stdin: {}", name, e),
2301 )));
2302 }
2303 }
2305
2306 if inherit_output {
2307 #[cfg(unix)]
2309 if let Some(ref term) = self.terminal_state {
2310 let child_id = child.id().unwrap_or(0);
2311 let pid = nix::unistd::Pid::from_raw(child_id as i32);
2312 let pgid = pid; if let Err(e) = term.give_terminal_to(pgid) {
2316 tracing::warn!("failed to give terminal to child: {}", e);
2317 }
2318
2319 let term_clone = term.clone();
2320 let cmd_name = name.to_string();
2321 let cmd_display = format!("{} {}", name, argv.join(" "));
2322 let jobs = self.jobs.clone();
2323
2324 let code = tokio::task::block_in_place(move || {
2325 let result = term_clone.wait_for_foreground(pid);
2326
2327 if let Err(e) = term_clone.reclaim_terminal() {
2329 tracing::warn!("failed to reclaim terminal: {}", e);
2330 }
2331
2332 match result {
2333 crate::terminal::WaitResult::Exited(code) => code as i64,
2334 crate::terminal::WaitResult::Signaled(sig) => 128 + sig as i64,
2335 crate::terminal::WaitResult::Stopped(_sig) => {
2336 let rt = tokio::runtime::Handle::current();
2338 let job_id = rt.block_on(jobs.register_stopped(
2339 cmd_display,
2340 child_id,
2341 child_id, ));
2343 eprintln!("\n[{}]+ Stopped\t{}", job_id, cmd_name);
2344 148 }
2346 }
2347 });
2348
2349 return Ok(Some(ExecResult::from_output(code, String::new(), String::new())));
2350 }
2351
2352 let status = match child.wait().await {
2354 Ok(s) => s,
2355 Err(e) => {
2356 return Ok(Some(ExecResult::failure(
2357 1,
2358 format!("{}: failed to wait: {}", name, e),
2359 )));
2360 }
2361 };
2362
2363 let code = status.code().unwrap_or_else(|| {
2364 #[cfg(unix)]
2365 {
2366 use std::os::unix::process::ExitStatusExt;
2367 128 + status.signal().unwrap_or(0)
2368 }
2369 #[cfg(not(unix))]
2370 {
2371 -1
2372 }
2373 }) as i64;
2374
2375 Ok(Some(ExecResult::from_output(code, String::new(), String::new())))
2377 } else {
2378 let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
2380 let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
2381
2382 let stdout_pipe = child.stdout.take();
2383 let stderr_pipe = child.stderr.take();
2384
2385 let stdout_clone = stdout_stream.clone();
2386 let stderr_clone = stderr_stream.clone();
2387
2388 let stdout_task = stdout_pipe.map(|pipe| {
2389 tokio::spawn(async move {
2390 drain_to_stream(pipe, stdout_clone).await;
2391 })
2392 });
2393
2394 let stderr_task = stderr_pipe.map(|pipe| {
2395 tokio::spawn(async move {
2396 drain_to_stream(pipe, stderr_clone).await;
2397 })
2398 });
2399
2400 let status = match child.wait().await {
2401 Ok(s) => s,
2402 Err(e) => {
2403 return Ok(Some(ExecResult::failure(
2404 1,
2405 format!("{}: failed to wait: {}", name, e),
2406 )));
2407 }
2408 };
2409
2410 if let Some(task) = stdout_task {
2411 let _ = task.await;
2413 }
2414 if let Some(task) = stderr_task {
2415 let _ = task.await;
2416 }
2417
2418 let code = status.code().unwrap_or_else(|| {
2419 #[cfg(unix)]
2420 {
2421 use std::os::unix::process::ExitStatusExt;
2422 128 + status.signal().unwrap_or(0)
2423 }
2424 #[cfg(not(unix))]
2425 {
2426 -1
2427 }
2428 }) as i64;
2429
2430 let stdout = stdout_stream.read_string().await;
2431 let stderr = stderr_stream.read_string().await;
2432
2433 Ok(Some(ExecResult::from_output(code, stdout, stderr)))
2434 }
2435 }
2436
2437 pub async fn get_var(&self, name: &str) -> Option<Value> {
2441 let scope = self.scope.read().await;
2442 scope.get(name).cloned()
2443 }
2444
2445 #[cfg(test)]
2447 pub async fn error_exit_enabled(&self) -> bool {
2448 let scope = self.scope.read().await;
2449 scope.error_exit_enabled()
2450 }
2451
2452 pub async fn set_var(&self, name: &str, value: Value) {
2454 let mut scope = self.scope.write().await;
2455 scope.set(name.to_string(), value);
2456 }
2457
2458 pub async fn set_positional(&self, script_name: impl Into<String>, args: Vec<String>) {
2460 let mut scope = self.scope.write().await;
2461 scope.set_positional(script_name, args);
2462 }
2463
2464 pub async fn list_vars(&self) -> Vec<(String, Value)> {
2466 let scope = self.scope.read().await;
2467 scope.all()
2468 }
2469
2470 pub async fn cwd(&self) -> PathBuf {
2474 self.exec_ctx.read().await.cwd.clone()
2475 }
2476
2477 pub async fn set_cwd(&self, path: PathBuf) {
2479 let mut ctx = self.exec_ctx.write().await;
2480 ctx.set_cwd(path);
2481 }
2482
2483 pub async fn last_result(&self) -> ExecResult {
2487 let scope = self.scope.read().await;
2488 scope.last_result().clone()
2489 }
2490
2491 pub async fn has_function(&self, name: &str) -> bool {
2495 self.user_tools.read().await.contains_key(name)
2496 }
2497
2498 pub fn tool_schemas(&self) -> Vec<crate::tools::ToolSchema> {
2500 self.tools.schemas()
2501 }
2502
2503 pub fn jobs(&self) -> Arc<JobManager> {
2507 self.jobs.clone()
2508 }
2509
2510 pub fn vfs(&self) -> Arc<VfsRouter> {
2514 self.vfs.clone()
2515 }
2516
2517 pub async fn reset(&self) -> Result<()> {
2524 {
2525 let mut scope = self.scope.write().await;
2526 *scope = Scope::new();
2527 }
2528 {
2529 let mut ctx = self.exec_ctx.write().await;
2530 ctx.cwd = PathBuf::from("/");
2531 }
2532 Ok(())
2533 }
2534
2535 pub async fn shutdown(self) -> Result<()> {
2537 self.jobs.wait_all().await;
2539 Ok(())
2540 }
2541
2542 async fn dispatch_command(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
2553 {
2555 let mut scope = self.scope.write().await;
2556 *scope = ctx.scope.clone();
2557 }
2558 {
2559 let mut ec = self.exec_ctx.write().await;
2560 ec.cwd = ctx.cwd.clone();
2561 ec.prev_cwd = ctx.prev_cwd.clone();
2562 ec.stdin = ctx.stdin.take();
2563 ec.stdin_data = ctx.stdin_data.take();
2564 ec.aliases = ctx.aliases.clone();
2565 ec.pipeline_position = ctx.pipeline_position;
2566 }
2567
2568 let result = self.execute_command(&cmd.name, &cmd.args).await?;
2570
2571 {
2573 let scope = self.scope.read().await;
2574 ctx.scope = scope.clone();
2575 }
2576 {
2577 let ec = self.exec_ctx.read().await;
2578 ctx.cwd = ec.cwd.clone();
2579 ctx.prev_cwd = ec.prev_cwd.clone();
2580 ctx.aliases = ec.aliases.clone();
2581 }
2582
2583 Ok(result)
2584 }
2585}
2586
2587#[async_trait]
2588impl CommandDispatcher for Kernel {
2589 async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
2595 self.dispatch_command(cmd, ctx).await
2596 }
2597}
2598
2599fn accumulate_result(accumulated: &mut ExecResult, new: &ExecResult) {
2605 if !accumulated.out.is_empty() && !new.out.is_empty() && !accumulated.out.ends_with('\n') {
2606 accumulated.out.push('\n');
2607 }
2608 accumulated.out.push_str(&new.out);
2609 if !accumulated.err.is_empty() && !new.err.is_empty() && !accumulated.err.ends_with('\n') {
2610 accumulated.err.push('\n');
2611 }
2612 accumulated.err.push_str(&new.err);
2613 accumulated.code = new.code;
2614 accumulated.data = new.data.clone();
2615}
2616
2617fn is_truthy(value: &Value) -> bool {
2619 match value {
2620 Value::Null => false,
2621 Value::Bool(b) => *b,
2622 Value::Int(i) => *i != 0,
2623 Value::Float(f) => *f != 0.0,
2624 Value::String(s) => !s.is_empty(),
2625 Value::Json(json) => match json {
2626 serde_json::Value::Null => false,
2627 serde_json::Value::Array(arr) => !arr.is_empty(),
2628 serde_json::Value::Object(obj) => !obj.is_empty(),
2629 serde_json::Value::Bool(b) => *b,
2630 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
2631 serde_json::Value::String(s) => !s.is_empty(),
2632 },
2633 Value::Blob(_) => true, }
2635}
2636
2637fn apply_tilde_expansion(value: Value) -> Value {
2641 match value {
2642 Value::String(s) if s.starts_with('~') => Value::String(expand_tilde(&s)),
2643 _ => value,
2644 }
2645}
2646
2647#[cfg(test)]
2648mod tests {
2649 use super::*;
2650
2651 #[tokio::test]
2652 async fn test_kernel_transient() {
2653 let kernel = Kernel::transient().expect("failed to create kernel");
2654 assert_eq!(kernel.name(), "transient");
2655 }
2656
2657 #[tokio::test]
2658 async fn test_kernel_execute_echo() {
2659 let kernel = Kernel::transient().expect("failed to create kernel");
2660 let result = kernel.execute("echo hello").await.expect("execution failed");
2661 assert!(result.ok());
2662 assert_eq!(result.out.trim(), "hello");
2663 }
2664
2665 #[tokio::test]
2666 async fn test_multiple_statements_accumulate_output() {
2667 let kernel = Kernel::transient().expect("failed to create kernel");
2668 let result = kernel
2669 .execute("echo one\necho two\necho three")
2670 .await
2671 .expect("execution failed");
2672 assert!(result.ok());
2673 assert!(result.out.contains("one"), "missing 'one': {}", result.out);
2675 assert!(result.out.contains("two"), "missing 'two': {}", result.out);
2676 assert!(result.out.contains("three"), "missing 'three': {}", result.out);
2677 }
2678
2679 #[tokio::test]
2680 async fn test_and_chain_accumulates_output() {
2681 let kernel = Kernel::transient().expect("failed to create kernel");
2682 let result = kernel
2683 .execute("echo first && echo second")
2684 .await
2685 .expect("execution failed");
2686 assert!(result.ok());
2687 assert!(result.out.contains("first"), "missing 'first': {}", result.out);
2688 assert!(result.out.contains("second"), "missing 'second': {}", result.out);
2689 }
2690
2691 #[tokio::test]
2692 async fn test_for_loop_accumulates_output() {
2693 let kernel = Kernel::transient().expect("failed to create kernel");
2694 let result = kernel
2695 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
2696 .await
2697 .expect("execution failed");
2698 assert!(result.ok());
2699 assert!(result.out.contains("item: a"), "missing 'item: a': {}", result.out);
2700 assert!(result.out.contains("item: b"), "missing 'item: b': {}", result.out);
2701 assert!(result.out.contains("item: c"), "missing 'item: c': {}", result.out);
2702 }
2703
2704 #[tokio::test]
2705 async fn test_while_loop_accumulates_output() {
2706 let kernel = Kernel::transient().expect("failed to create kernel");
2707 let result = kernel
2708 .execute(r#"
2709 N=3
2710 while [[ ${N} -gt 0 ]]; do
2711 echo "N=${N}"
2712 N=$((N - 1))
2713 done
2714 "#)
2715 .await
2716 .expect("execution failed");
2717 assert!(result.ok());
2718 assert!(result.out.contains("N=3"), "missing 'N=3': {}", result.out);
2719 assert!(result.out.contains("N=2"), "missing 'N=2': {}", result.out);
2720 assert!(result.out.contains("N=1"), "missing 'N=1': {}", result.out);
2721 }
2722
2723 #[tokio::test]
2724 async fn test_kernel_set_var() {
2725 let kernel = Kernel::transient().expect("failed to create kernel");
2726
2727 kernel.execute("X=42").await.expect("set failed");
2728
2729 let value = kernel.get_var("X").await;
2730 assert_eq!(value, Some(Value::Int(42)));
2731 }
2732
2733 #[tokio::test]
2734 async fn test_kernel_var_expansion() {
2735 let kernel = Kernel::transient().expect("failed to create kernel");
2736
2737 kernel.execute("NAME=\"world\"").await.expect("set failed");
2738 let result = kernel.execute("echo \"hello ${NAME}\"").await.expect("echo failed");
2739
2740 assert!(result.ok());
2741 assert_eq!(result.out.trim(), "hello world");
2742 }
2743
2744 #[tokio::test]
2745 async fn test_kernel_last_result() {
2746 let kernel = Kernel::transient().expect("failed to create kernel");
2747
2748 kernel.execute("echo test").await.expect("echo failed");
2749
2750 let last = kernel.last_result().await;
2751 assert!(last.ok());
2752 assert_eq!(last.out.trim(), "test");
2753 }
2754
2755 #[tokio::test]
2756 async fn test_kernel_tool_not_found() {
2757 let kernel = Kernel::transient().expect("failed to create kernel");
2758
2759 let result = kernel.execute("nonexistent_tool").await.expect("execution failed");
2760 assert!(!result.ok());
2761 assert_eq!(result.code, 127);
2762 assert!(result.err.contains("command not found"));
2763 }
2764
2765 #[tokio::test]
2766 async fn test_external_command_true() {
2767 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
2769
2770 let result = kernel.execute("true").await.expect("execution failed");
2772 assert!(result.ok(), "true should succeed: {:?}", result);
2774 }
2775
2776 #[tokio::test]
2777 async fn test_external_command_basic() {
2778 let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
2780
2781 let path_var = std::env::var("PATH").unwrap_or_default();
2786 eprintln!("System PATH: {}", path_var);
2787
2788 kernel.execute(&format!(r#"PATH="{}""#, path_var)).await.expect("set PATH failed");
2790
2791 let result = kernel.execute("uname").await.expect("execution failed");
2794 eprintln!("uname result: {:?}", result);
2795 assert!(result.ok() || result.code == 127, "uname: {:?}", result);
2797 }
2798
2799 #[tokio::test]
2800 async fn test_kernel_reset() {
2801 let kernel = Kernel::transient().expect("failed to create kernel");
2802
2803 kernel.execute("X=1").await.expect("set failed");
2804 assert!(kernel.get_var("X").await.is_some());
2805
2806 kernel.reset().await.expect("reset failed");
2807 assert!(kernel.get_var("X").await.is_none());
2808 }
2809
2810 #[tokio::test]
2811 async fn test_kernel_cwd() {
2812 let kernel = Kernel::transient().expect("failed to create kernel");
2813
2814 let cwd = kernel.cwd().await;
2816 let home = std::env::var("HOME")
2817 .map(PathBuf::from)
2818 .unwrap_or_else(|_| PathBuf::from("/"));
2819 assert_eq!(cwd, home);
2820
2821 kernel.set_cwd(PathBuf::from("/tmp")).await;
2822 assert_eq!(kernel.cwd().await, PathBuf::from("/tmp"));
2823 }
2824
2825 #[tokio::test]
2826 async fn test_kernel_list_vars() {
2827 let kernel = Kernel::transient().expect("failed to create kernel");
2828
2829 kernel.execute("A=1").await.ok();
2830 kernel.execute("B=2").await.ok();
2831
2832 let vars = kernel.list_vars().await;
2833 assert!(vars.iter().any(|(n, v)| n == "A" && *v == Value::Int(1)));
2834 assert!(vars.iter().any(|(n, v)| n == "B" && *v == Value::Int(2)));
2835 }
2836
2837 #[tokio::test]
2838 async fn test_is_truthy() {
2839 assert!(!is_truthy(&Value::Null));
2840 assert!(!is_truthy(&Value::Bool(false)));
2841 assert!(is_truthy(&Value::Bool(true)));
2842 assert!(!is_truthy(&Value::Int(0)));
2843 assert!(is_truthy(&Value::Int(1)));
2844 assert!(!is_truthy(&Value::String("".into())));
2845 assert!(is_truthy(&Value::String("x".into())));
2846 }
2847
2848 #[tokio::test]
2849 async fn test_jq_in_pipeline() {
2850 let kernel = Kernel::transient().expect("failed to create kernel");
2851 let result = kernel
2853 .execute(r#"echo "{\"name\": \"Alice\"}" | jq ".name" -r"#)
2854 .await
2855 .expect("execution failed");
2856 assert!(result.ok(), "jq pipeline failed: {}", result.err);
2857 assert_eq!(result.out.trim(), "Alice");
2858 }
2859
2860 #[tokio::test]
2861 async fn test_user_defined_tool() {
2862 let kernel = Kernel::transient().expect("failed to create kernel");
2863
2864 kernel
2866 .execute(r#"greet() { echo "Hello, $1!" }"#)
2867 .await
2868 .expect("function definition failed");
2869
2870 let result = kernel
2872 .execute(r#"greet "World""#)
2873 .await
2874 .expect("function call failed");
2875
2876 assert!(result.ok(), "greet failed: {}", result.err);
2877 assert_eq!(result.out.trim(), "Hello, World!");
2878 }
2879
2880 #[tokio::test]
2881 async fn test_user_tool_positional_args() {
2882 let kernel = Kernel::transient().expect("failed to create kernel");
2883
2884 kernel
2886 .execute(r#"greet() { echo "Hi $1" }"#)
2887 .await
2888 .expect("function definition failed");
2889
2890 let result = kernel
2892 .execute(r#"greet "Amy""#)
2893 .await
2894 .expect("function call failed");
2895
2896 assert!(result.ok(), "greet failed: {}", result.err);
2897 assert_eq!(result.out.trim(), "Hi Amy");
2898 }
2899
2900 #[tokio::test]
2901 async fn test_function_shared_scope() {
2902 let kernel = Kernel::transient().expect("failed to create kernel");
2903
2904 kernel
2906 .execute(r#"SECRET="hidden""#)
2907 .await
2908 .expect("set failed");
2909
2910 kernel
2912 .execute(r#"access_parent() {
2913 echo "${SECRET}"
2914 SECRET="modified"
2915 }"#)
2916 .await
2917 .expect("function definition failed");
2918
2919 let result = kernel.execute("access_parent").await.expect("function call failed");
2921
2922 assert!(
2924 result.out.contains("hidden"),
2925 "Function should access parent scope, got: {}",
2926 result.out
2927 );
2928
2929 let secret = kernel.get_var("SECRET").await;
2931 assert_eq!(
2932 secret,
2933 Some(Value::String("modified".into())),
2934 "Function should modify parent scope"
2935 );
2936 }
2937
2938 #[tokio::test]
2939 async fn test_exec_builtin() {
2940 let kernel = Kernel::transient().expect("failed to create kernel");
2941 let result = kernel
2943 .execute(r#"exec command="/bin/echo" argv="hello world""#)
2944 .await
2945 .expect("exec failed");
2946
2947 assert!(result.ok(), "exec failed: {}", result.err);
2948 assert_eq!(result.out.trim(), "hello world");
2949 }
2950
2951 #[tokio::test]
2952 async fn test_while_false_never_runs() {
2953 let kernel = Kernel::transient().expect("failed to create kernel");
2954
2955 let result = kernel
2957 .execute(r#"
2958 while false; do
2959 echo "should not run"
2960 done
2961 "#)
2962 .await
2963 .expect("while false failed");
2964
2965 assert!(result.ok());
2966 assert!(result.out.is_empty(), "while false should not execute body: {}", result.out);
2967 }
2968
2969 #[tokio::test]
2970 async fn test_while_string_comparison() {
2971 let kernel = Kernel::transient().expect("failed to create kernel");
2972
2973 kernel.execute(r#"FLAG="go""#).await.expect("set failed");
2975
2976 let result = kernel
2979 .execute(r#"
2980 while [[ ${FLAG} == "go" ]]; do
2981 FLAG="stop"
2982 echo "running"
2983 done
2984 "#)
2985 .await
2986 .expect("while with string cmp failed");
2987
2988 assert!(result.ok());
2989 assert!(result.out.contains("running"), "should have run once: {}", result.out);
2990
2991 let flag = kernel.get_var("FLAG").await;
2993 assert_eq!(flag, Some(Value::String("stop".into())));
2994 }
2995
2996 #[tokio::test]
2997 async fn test_while_numeric_comparison() {
2998 let kernel = Kernel::transient().expect("failed to create kernel");
2999
3000 kernel.execute("N=5").await.expect("set failed");
3002
3003 let result = kernel
3005 .execute(r#"
3006 while [[ ${N} -gt 3 ]]; do
3007 N=3
3008 echo "N was greater"
3009 done
3010 "#)
3011 .await
3012 .expect("while with > failed");
3013
3014 assert!(result.ok());
3015 assert!(result.out.contains("N was greater"), "should have run once: {}", result.out);
3016 }
3017
3018 #[tokio::test]
3019 async fn test_break_in_while_loop() {
3020 let kernel = Kernel::transient().expect("failed to create kernel");
3021
3022 let result = kernel
3023 .execute(r#"
3024 I=0
3025 while true; do
3026 I=1
3027 echo "before break"
3028 break
3029 echo "after break"
3030 done
3031 "#)
3032 .await
3033 .expect("while with break failed");
3034
3035 assert!(result.ok());
3036 assert!(result.out.contains("before break"), "should see before break: {}", result.out);
3037 assert!(!result.out.contains("after break"), "should not see after break: {}", result.out);
3038
3039 let i = kernel.get_var("I").await;
3041 assert_eq!(i, Some(Value::Int(1)));
3042 }
3043
3044 #[tokio::test]
3045 async fn test_continue_in_while_loop() {
3046 let kernel = Kernel::transient().expect("failed to create kernel");
3047
3048 let result = kernel
3053 .execute(r#"
3054 STATE="start"
3055 AFTER_CONTINUE="no"
3056 while [[ ${STATE} != "done" ]]; do
3057 if [[ ${STATE} == "start" ]]; then
3058 STATE="middle"
3059 continue
3060 AFTER_CONTINUE="yes"
3061 fi
3062 if [[ ${STATE} == "middle" ]]; then
3063 STATE="done"
3064 fi
3065 done
3066 "#)
3067 .await
3068 .expect("while with continue failed");
3069
3070 assert!(result.ok());
3071
3072 let state = kernel.get_var("STATE").await;
3074 assert_eq!(state, Some(Value::String("done".into())));
3075
3076 let after = kernel.get_var("AFTER_CONTINUE").await;
3078 assert_eq!(after, Some(Value::String("no".into())));
3079 }
3080
3081 #[tokio::test]
3082 async fn test_break_with_level() {
3083 let kernel = Kernel::transient().expect("failed to create kernel");
3084
3085 let result = kernel
3090 .execute(r#"
3091 OUTER=0
3092 while true; do
3093 OUTER=1
3094 for X in "1 2"; do
3095 break 2
3096 done
3097 OUTER=2
3098 done
3099 "#)
3100 .await
3101 .expect("nested break failed");
3102
3103 assert!(result.ok());
3104
3105 let outer = kernel.get_var("OUTER").await;
3107 assert_eq!(outer, Some(Value::Int(1)), "break 2 should have skipped OUTER=2");
3108 }
3109
3110 #[tokio::test]
3111 async fn test_return_from_tool() {
3112 let kernel = Kernel::transient().expect("failed to create kernel");
3113
3114 kernel
3116 .execute(r#"early_return() {
3117 if [[ $1 == 1 ]]; then
3118 return 42
3119 fi
3120 echo "not returned"
3121 }"#)
3122 .await
3123 .expect("function definition failed");
3124
3125 let result = kernel
3128 .execute("early_return 1")
3129 .await
3130 .expect("function call failed");
3131
3132 assert_eq!(result.code, 42);
3134 assert!(result.out.is_empty());
3136 }
3137
3138 #[tokio::test]
3139 async fn test_return_without_value() {
3140 let kernel = Kernel::transient().expect("failed to create kernel");
3141
3142 kernel
3144 .execute(r#"early_exit() {
3145 if [[ $1 == "stop" ]]; then
3146 return
3147 fi
3148 echo "continued"
3149 }"#)
3150 .await
3151 .expect("function definition failed");
3152
3153 let result = kernel
3155 .execute(r#"early_exit "stop""#)
3156 .await
3157 .expect("function call failed");
3158
3159 assert!(result.ok());
3160 assert!(result.out.is_empty() || result.out.trim().is_empty());
3161 }
3162
3163 #[tokio::test]
3164 async fn test_exit_stops_execution() {
3165 let kernel = Kernel::transient().expect("failed to create kernel");
3166
3167 kernel
3169 .execute(r#"
3170 BEFORE="yes"
3171 exit 0
3172 AFTER="yes"
3173 "#)
3174 .await
3175 .expect("execution failed");
3176
3177 let before = kernel.get_var("BEFORE").await;
3179 assert_eq!(before, Some(Value::String("yes".into())));
3180
3181 let after = kernel.get_var("AFTER").await;
3182 assert!(after.is_none(), "AFTER should not be set after exit");
3183 }
3184
3185 #[tokio::test]
3186 async fn test_exit_with_code() {
3187 let kernel = Kernel::transient().expect("failed to create kernel");
3188
3189 let result = kernel
3191 .execute("exit 42")
3192 .await
3193 .expect("exit failed");
3194
3195 assert_eq!(result.code, 42);
3196 assert!(result.out.is_empty(), "exit should not produce stdout");
3197 }
3198
3199 #[tokio::test]
3200 async fn test_set_e_stops_on_failure() {
3201 let kernel = Kernel::transient().expect("failed to create kernel");
3202
3203 kernel.execute("set -e").await.expect("set -e failed");
3205
3206 kernel
3208 .execute(r#"
3209 STEP1="done"
3210 false
3211 STEP2="done"
3212 "#)
3213 .await
3214 .expect("execution failed");
3215
3216 let step1 = kernel.get_var("STEP1").await;
3218 assert_eq!(step1, Some(Value::String("done".into())));
3219
3220 let step2 = kernel.get_var("STEP2").await;
3221 assert!(step2.is_none(), "STEP2 should not be set after false with set -e");
3222 }
3223
3224 #[tokio::test]
3225 async fn test_set_plus_e_disables_error_exit() {
3226 let kernel = Kernel::transient().expect("failed to create kernel");
3227
3228 kernel.execute("set -e").await.expect("set -e failed");
3230 kernel.execute("set +e").await.expect("set +e failed");
3231
3232 kernel
3234 .execute(r#"
3235 STEP1="done"
3236 false
3237 STEP2="done"
3238 "#)
3239 .await
3240 .expect("execution failed");
3241
3242 let step1 = kernel.get_var("STEP1").await;
3244 assert_eq!(step1, Some(Value::String("done".into())));
3245
3246 let step2 = kernel.get_var("STEP2").await;
3247 assert_eq!(step2, Some(Value::String("done".into())));
3248 }
3249
3250 #[tokio::test]
3251 async fn test_set_ignores_unknown_options() {
3252 let kernel = Kernel::transient().expect("failed to create kernel");
3253
3254 let result = kernel
3256 .execute("set -e -u -o pipefail")
3257 .await
3258 .expect("set with unknown options failed");
3259
3260 assert!(result.ok(), "set should succeed with unknown options");
3261
3262 kernel
3264 .execute(r#"
3265 BEFORE="yes"
3266 false
3267 AFTER="yes"
3268 "#)
3269 .await
3270 .ok();
3271
3272 let after = kernel.get_var("AFTER").await;
3273 assert!(after.is_none(), "-e should be enabled despite unknown options");
3274 }
3275
3276 #[tokio::test]
3277 async fn test_set_no_args_shows_settings() {
3278 let kernel = Kernel::transient().expect("failed to create kernel");
3279
3280 kernel.execute("set -e").await.expect("set -e failed");
3282
3283 let result = kernel.execute("set").await.expect("set failed");
3285
3286 assert!(result.ok());
3287 assert!(result.out.contains("set -e"), "should show -e is enabled: {}", result.out);
3288 }
3289
3290 #[tokio::test]
3291 async fn test_set_e_in_pipeline() {
3292 let kernel = Kernel::transient().expect("failed to create kernel");
3293
3294 kernel.execute("set -e").await.expect("set -e failed");
3295
3296 kernel
3298 .execute(r#"
3299 BEFORE="yes"
3300 false | cat
3301 AFTER="yes"
3302 "#)
3303 .await
3304 .ok();
3305
3306 let before = kernel.get_var("BEFORE").await;
3307 assert_eq!(before, Some(Value::String("yes".into())));
3308
3309 }
3314
3315 #[tokio::test]
3316 async fn test_set_e_with_and_chain() {
3317 let kernel = Kernel::transient().expect("failed to create kernel");
3318
3319 kernel.execute("set -e").await.expect("set -e failed");
3320
3321 kernel
3324 .execute(r#"
3325 RESULT="initial"
3326 false && RESULT="chained"
3327 RESULT="continued"
3328 "#)
3329 .await
3330 .ok();
3331
3332 let result = kernel.get_var("RESULT").await;
3335 assert!(result.is_some(), "RESULT should be set");
3338 }
3339
3340 #[tokio::test]
3341 async fn test_set_e_exits_in_for_loop() {
3342 let kernel = Kernel::transient().expect("failed to create kernel");
3343
3344 kernel.execute("set -e").await.expect("set -e failed");
3345
3346 kernel
3347 .execute(r#"
3348 REACHED="no"
3349 for x in 1 2 3; do
3350 false
3351 REACHED="yes"
3352 done
3353 "#)
3354 .await
3355 .ok();
3356
3357 let reached = kernel.get_var("REACHED").await;
3359 assert_eq!(reached, Some(Value::String("no".into())),
3360 "set -e should exit on failure in for loop body");
3361 }
3362
3363 #[tokio::test]
3364 async fn test_for_loop_continues_without_set_e() {
3365 let kernel = Kernel::transient().expect("failed to create kernel");
3366
3367 kernel
3369 .execute(r#"
3370 COUNT=0
3371 for x in 1 2 3; do
3372 false
3373 COUNT=$((COUNT + 1))
3374 done
3375 "#)
3376 .await
3377 .ok();
3378
3379 let count = kernel.get_var("COUNT").await;
3380 let count_val = match &count {
3382 Some(Value::Int(n)) => *n,
3383 Some(Value::String(s)) => s.parse().unwrap_or(-1),
3384 _ => -1,
3385 };
3386 assert_eq!(count_val, 3,
3387 "without set -e, loop should complete all iterations (got {:?})", count);
3388 }
3389
3390 #[tokio::test]
3395 async fn test_source_sets_variables() {
3396 let kernel = Kernel::transient().expect("failed to create kernel");
3397
3398 kernel
3400 .execute(r#"write "/test.kai" 'FOO="bar"'"#)
3401 .await
3402 .expect("write failed");
3403
3404 let result = kernel
3406 .execute(r#"source "/test.kai""#)
3407 .await
3408 .expect("source failed");
3409
3410 assert!(result.ok(), "source should succeed");
3411
3412 let foo = kernel.get_var("FOO").await;
3414 assert_eq!(foo, Some(Value::String("bar".into())));
3415 }
3416
3417 #[tokio::test]
3418 async fn test_source_with_dot_alias() {
3419 let kernel = Kernel::transient().expect("failed to create kernel");
3420
3421 kernel
3423 .execute(r#"write "/vars.kai" 'X=42'"#)
3424 .await
3425 .expect("write failed");
3426
3427 let result = kernel
3429 .execute(r#". "/vars.kai""#)
3430 .await
3431 .expect(". failed");
3432
3433 assert!(result.ok(), ". should succeed");
3434
3435 let x = kernel.get_var("X").await;
3437 assert_eq!(x, Some(Value::Int(42)));
3438 }
3439
3440 #[tokio::test]
3441 async fn test_source_not_found() {
3442 let kernel = Kernel::transient().expect("failed to create kernel");
3443
3444 let result = kernel
3446 .execute(r#"source "/nonexistent.kai""#)
3447 .await
3448 .expect("source should not fail with error");
3449
3450 assert!(!result.ok(), "source of non-existent file should fail");
3451 assert!(result.err.contains("nonexistent.kai"), "error should mention filename");
3452 }
3453
3454 #[tokio::test]
3455 async fn test_source_missing_filename() {
3456 let kernel = Kernel::transient().expect("failed to create kernel");
3457
3458 let result = kernel
3460 .execute("source")
3461 .await
3462 .expect("source should not fail with error");
3463
3464 assert!(!result.ok(), "source without filename should fail");
3465 assert!(result.err.contains("missing filename"), "error should mention missing filename");
3466 }
3467
3468 #[tokio::test]
3469 async fn test_source_executes_multiple_statements() {
3470 let kernel = Kernel::transient().expect("failed to create kernel");
3471
3472 kernel
3474 .execute(r#"write "/multi.kai" 'A=1
3475B=2
3476C=3'"#)
3477 .await
3478 .expect("write failed");
3479
3480 kernel
3482 .execute(r#"source "/multi.kai""#)
3483 .await
3484 .expect("source failed");
3485
3486 assert_eq!(kernel.get_var("A").await, Some(Value::Int(1)));
3488 assert_eq!(kernel.get_var("B").await, Some(Value::Int(2)));
3489 assert_eq!(kernel.get_var("C").await, Some(Value::Int(3)));
3490 }
3491
3492 #[tokio::test]
3493 async fn test_source_can_define_functions() {
3494 let kernel = Kernel::transient().expect("failed to create kernel");
3495
3496 kernel
3498 .execute(r#"write "/functions.kai" 'greet() {
3499 echo "Hello, $1!"
3500}'"#)
3501 .await
3502 .expect("write failed");
3503
3504 kernel
3506 .execute(r#"source "/functions.kai""#)
3507 .await
3508 .expect("source failed");
3509
3510 let result = kernel
3512 .execute(r#"greet "World""#)
3513 .await
3514 .expect("greet failed");
3515
3516 assert!(result.ok());
3517 assert!(result.out.contains("Hello, World!"));
3518 }
3519
3520 #[tokio::test]
3521 async fn test_source_inherits_error_exit() {
3522 let kernel = Kernel::transient().expect("failed to create kernel");
3523
3524 kernel.execute("set -e").await.expect("set -e failed");
3526
3527 kernel
3529 .execute(r#"write "/fail.kai" 'BEFORE="yes"
3530false
3531AFTER="yes"'"#)
3532 .await
3533 .expect("write failed");
3534
3535 kernel
3537 .execute(r#"source "/fail.kai""#)
3538 .await
3539 .ok();
3540
3541 let before = kernel.get_var("BEFORE").await;
3543 assert_eq!(before, Some(Value::String("yes".into())));
3544
3545 }
3548
3549 #[tokio::test]
3554 async fn test_case_simple_match() {
3555 let kernel = Kernel::transient().expect("failed to create kernel");
3556
3557 let result = kernel
3558 .execute(r#"
3559 case "hello" in
3560 hello) echo "matched hello" ;;
3561 world) echo "matched world" ;;
3562 esac
3563 "#)
3564 .await
3565 .expect("case failed");
3566
3567 assert!(result.ok());
3568 assert_eq!(result.out.trim(), "matched hello");
3569 }
3570
3571 #[tokio::test]
3572 async fn test_case_wildcard_match() {
3573 let kernel = Kernel::transient().expect("failed to create kernel");
3574
3575 let result = kernel
3576 .execute(r#"
3577 case "main.rs" in
3578 "*.py") echo "Python" ;;
3579 "*.rs") echo "Rust" ;;
3580 "*") echo "Unknown" ;;
3581 esac
3582 "#)
3583 .await
3584 .expect("case failed");
3585
3586 assert!(result.ok());
3587 assert_eq!(result.out.trim(), "Rust");
3588 }
3589
3590 #[tokio::test]
3591 async fn test_case_default_match() {
3592 let kernel = Kernel::transient().expect("failed to create kernel");
3593
3594 let result = kernel
3595 .execute(r#"
3596 case "unknown.xyz" in
3597 "*.py") echo "Python" ;;
3598 "*.rs") echo "Rust" ;;
3599 "*") echo "Default" ;;
3600 esac
3601 "#)
3602 .await
3603 .expect("case failed");
3604
3605 assert!(result.ok());
3606 assert_eq!(result.out.trim(), "Default");
3607 }
3608
3609 #[tokio::test]
3610 async fn test_case_no_match() {
3611 let kernel = Kernel::transient().expect("failed to create kernel");
3612
3613 let result = kernel
3615 .execute(r#"
3616 case "nope" in
3617 "yes") echo "yes" ;;
3618 "no") echo "no" ;;
3619 esac
3620 "#)
3621 .await
3622 .expect("case failed");
3623
3624 assert!(result.ok());
3625 assert!(result.out.is_empty(), "no match should produce empty output");
3626 }
3627
3628 #[tokio::test]
3629 async fn test_case_with_variable() {
3630 let kernel = Kernel::transient().expect("failed to create kernel");
3631
3632 kernel.execute(r#"LANG="rust""#).await.expect("set failed");
3633
3634 let result = kernel
3635 .execute(r#"
3636 case ${LANG} in
3637 python) echo "snake" ;;
3638 rust) echo "crab" ;;
3639 go) echo "gopher" ;;
3640 esac
3641 "#)
3642 .await
3643 .expect("case failed");
3644
3645 assert!(result.ok());
3646 assert_eq!(result.out.trim(), "crab");
3647 }
3648
3649 #[tokio::test]
3650 async fn test_case_multiple_patterns() {
3651 let kernel = Kernel::transient().expect("failed to create kernel");
3652
3653 let result = kernel
3654 .execute(r#"
3655 case "yes" in
3656 "y"|"yes"|"Y"|"YES") echo "affirmative" ;;
3657 "n"|"no"|"N"|"NO") echo "negative" ;;
3658 esac
3659 "#)
3660 .await
3661 .expect("case failed");
3662
3663 assert!(result.ok());
3664 assert_eq!(result.out.trim(), "affirmative");
3665 }
3666
3667 #[tokio::test]
3668 async fn test_case_glob_question_mark() {
3669 let kernel = Kernel::transient().expect("failed to create kernel");
3670
3671 let result = kernel
3672 .execute(r#"
3673 case "test1" in
3674 "test?") echo "matched test?" ;;
3675 "*") echo "default" ;;
3676 esac
3677 "#)
3678 .await
3679 .expect("case failed");
3680
3681 assert!(result.ok());
3682 assert_eq!(result.out.trim(), "matched test?");
3683 }
3684
3685 #[tokio::test]
3686 async fn test_case_char_class() {
3687 let kernel = Kernel::transient().expect("failed to create kernel");
3688
3689 let result = kernel
3690 .execute(r#"
3691 case "Yes" in
3692 "[Yy]*") echo "yes-like" ;;
3693 "[Nn]*") echo "no-like" ;;
3694 esac
3695 "#)
3696 .await
3697 .expect("case failed");
3698
3699 assert!(result.ok());
3700 assert_eq!(result.out.trim(), "yes-like");
3701 }
3702
3703 #[tokio::test]
3708 async fn test_cat_from_pipeline() {
3709 let kernel = Kernel::transient().expect("failed to create kernel");
3710
3711 let result = kernel
3712 .execute(r#"echo "piped text" | cat"#)
3713 .await
3714 .expect("cat pipeline failed");
3715
3716 assert!(result.ok(), "cat failed: {}", result.err);
3717 assert_eq!(result.out.trim(), "piped text");
3718 }
3719
3720 #[tokio::test]
3721 async fn test_cat_from_pipeline_multiline() {
3722 let kernel = Kernel::transient().expect("failed to create kernel");
3723
3724 let result = kernel
3725 .execute(r#"echo "line1\nline2" | cat -n"#)
3726 .await
3727 .expect("cat pipeline failed");
3728
3729 assert!(result.ok(), "cat failed: {}", result.err);
3730 assert!(result.out.contains("1\t"), "output: {}", result.out);
3731 }
3732
3733 #[tokio::test]
3738 async fn test_heredoc_basic() {
3739 let kernel = Kernel::transient().expect("failed to create kernel");
3740
3741 let result = kernel
3742 .execute("cat <<EOF\nhello\nEOF")
3743 .await
3744 .expect("heredoc failed");
3745
3746 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
3747 assert_eq!(result.out.trim(), "hello");
3748 }
3749
3750 #[tokio::test]
3751 async fn test_arithmetic_in_string() {
3752 let kernel = Kernel::transient().expect("failed to create kernel");
3753
3754 let result = kernel
3755 .execute(r#"echo "result: $((1 + 2))""#)
3756 .await
3757 .expect("arithmetic in string failed");
3758
3759 assert!(result.ok(), "echo failed: {}", result.err);
3760 assert_eq!(result.out.trim(), "result: 3");
3761 }
3762
3763 #[tokio::test]
3764 async fn test_heredoc_multiline() {
3765 let kernel = Kernel::transient().expect("failed to create kernel");
3766
3767 let result = kernel
3768 .execute("cat <<EOF\nline1\nline2\nline3\nEOF")
3769 .await
3770 .expect("heredoc failed");
3771
3772 assert!(result.ok(), "cat with heredoc failed: {}", result.err);
3773 assert!(result.out.contains("line1"), "output: {}", result.out);
3774 assert!(result.out.contains("line2"), "output: {}", result.out);
3775 assert!(result.out.contains("line3"), "output: {}", result.out);
3776 }
3777
3778 #[tokio::test]
3779 async fn test_heredoc_variable_expansion() {
3780 let kernel = Kernel::transient().expect("failed to create kernel");
3782
3783 kernel.execute("GREETING=hello").await.expect("set var");
3784
3785 let result = kernel
3786 .execute("cat <<EOF\n$GREETING world\nEOF")
3787 .await
3788 .expect("heredoc expansion failed");
3789
3790 assert!(result.ok(), "heredoc expansion failed: {}", result.err);
3791 assert_eq!(result.out.trim(), "hello world");
3792 }
3793
3794 #[tokio::test]
3795 async fn test_heredoc_quoted_no_expansion() {
3796 let kernel = Kernel::transient().expect("failed to create kernel");
3798
3799 kernel.execute("GREETING=hello").await.expect("set var");
3800
3801 let result = kernel
3802 .execute("cat <<'EOF'\n$GREETING world\nEOF")
3803 .await
3804 .expect("quoted heredoc failed");
3805
3806 assert!(result.ok(), "quoted heredoc failed: {}", result.err);
3807 assert_eq!(result.out.trim(), "$GREETING world");
3808 }
3809
3810 #[tokio::test]
3811 async fn test_heredoc_default_value_expansion() {
3812 let kernel = Kernel::transient().expect("failed to create kernel");
3814
3815 let result = kernel
3816 .execute("cat <<EOF\n${UNSET:-fallback}\nEOF")
3817 .await
3818 .expect("heredoc default expansion failed");
3819
3820 assert!(result.ok(), "heredoc default expansion failed: {}", result.err);
3821 assert_eq!(result.out.trim(), "fallback");
3822 }
3823
3824 #[tokio::test]
3829 async fn test_read_from_pipeline() {
3830 let kernel = Kernel::transient().expect("failed to create kernel");
3831
3832 let result = kernel
3834 .execute(r#"echo "Alice" | read NAME; echo "Hello, ${NAME}""#)
3835 .await
3836 .expect("read pipeline failed");
3837
3838 assert!(result.ok(), "read failed: {}", result.err);
3839 assert!(result.out.contains("Hello, Alice"), "output: {}", result.out);
3840 }
3841
3842 #[tokio::test]
3843 async fn test_read_multiple_vars_from_pipeline() {
3844 let kernel = Kernel::transient().expect("failed to create kernel");
3845
3846 let result = kernel
3847 .execute(r#"echo "John Doe 42" | read FIRST LAST AGE; echo "${FIRST} is ${AGE}""#)
3848 .await
3849 .expect("read pipeline failed");
3850
3851 assert!(result.ok(), "read failed: {}", result.err);
3852 assert!(result.out.contains("John is 42"), "output: {}", result.out);
3853 }
3854
3855 #[tokio::test]
3860 async fn test_posix_function_with_positional_params() {
3861 let kernel = Kernel::transient().expect("failed to create kernel");
3862
3863 kernel
3865 .execute(r#"greet() { echo "Hello, $1!" }"#)
3866 .await
3867 .expect("function definition failed");
3868
3869 let result = kernel
3871 .execute(r#"greet "Amy""#)
3872 .await
3873 .expect("function call failed");
3874
3875 assert!(result.ok(), "greet failed: {}", result.err);
3876 assert_eq!(result.out.trim(), "Hello, Amy!");
3877 }
3878
3879 #[tokio::test]
3880 async fn test_posix_function_multiple_args() {
3881 let kernel = Kernel::transient().expect("failed to create kernel");
3882
3883 kernel
3885 .execute(r#"add_greeting() { echo "$1 $2!" }"#)
3886 .await
3887 .expect("function definition failed");
3888
3889 let result = kernel
3891 .execute(r#"add_greeting "Hello" "World""#)
3892 .await
3893 .expect("function call failed");
3894
3895 assert!(result.ok(), "function failed: {}", result.err);
3896 assert_eq!(result.out.trim(), "Hello World!");
3897 }
3898
3899 #[tokio::test]
3900 async fn test_bash_function_with_positional_params() {
3901 let kernel = Kernel::transient().expect("failed to create kernel");
3902
3903 kernel
3905 .execute(r#"function greet { echo "Hi $1" }"#)
3906 .await
3907 .expect("function definition failed");
3908
3909 let result = kernel
3911 .execute(r#"greet "Bob""#)
3912 .await
3913 .expect("function call failed");
3914
3915 assert!(result.ok(), "greet failed: {}", result.err);
3916 assert_eq!(result.out.trim(), "Hi Bob");
3917 }
3918
3919 #[tokio::test]
3920 async fn test_shell_function_with_all_args() {
3921 let kernel = Kernel::transient().expect("failed to create kernel");
3922
3923 kernel
3925 .execute(r#"echo_all() { echo "args: $@" }"#)
3926 .await
3927 .expect("function definition failed");
3928
3929 let result = kernel
3931 .execute(r#"echo_all "a" "b" "c""#)
3932 .await
3933 .expect("function call failed");
3934
3935 assert!(result.ok(), "function failed: {}", result.err);
3936 assert_eq!(result.out.trim(), "args: a b c");
3937 }
3938
3939 #[tokio::test]
3940 async fn test_shell_function_with_arg_count() {
3941 let kernel = Kernel::transient().expect("failed to create kernel");
3942
3943 kernel
3945 .execute(r#"count_args() { echo "count: $#" }"#)
3946 .await
3947 .expect("function definition failed");
3948
3949 let result = kernel
3951 .execute(r#"count_args "x" "y" "z""#)
3952 .await
3953 .expect("function call failed");
3954
3955 assert!(result.ok(), "function failed: {}", result.err);
3956 assert_eq!(result.out.trim(), "count: 3");
3957 }
3958
3959 #[tokio::test]
3960 async fn test_shell_function_shared_scope() {
3961 let kernel = Kernel::transient().expect("failed to create kernel");
3962
3963 kernel
3965 .execute(r#"PARENT_VAR="visible""#)
3966 .await
3967 .expect("set failed");
3968
3969 kernel
3971 .execute(r#"modify_parent() {
3972 echo "saw: ${PARENT_VAR}"
3973 PARENT_VAR="changed by function"
3974 }"#)
3975 .await
3976 .expect("function definition failed");
3977
3978 let result = kernel.execute("modify_parent").await.expect("function failed");
3980
3981 assert!(
3982 result.out.contains("visible"),
3983 "Shell function should access parent scope, got: {}",
3984 result.out
3985 );
3986
3987 let var = kernel.get_var("PARENT_VAR").await;
3989 assert_eq!(
3990 var,
3991 Some(Value::String("changed by function".into())),
3992 "Shell function should modify parent scope"
3993 );
3994 }
3995
3996 #[tokio::test]
4001 async fn test_script_execution_from_path() {
4002 let kernel = Kernel::transient().expect("failed to create kernel");
4003
4004 kernel.execute(r#"mkdir "/bin""#).await.ok();
4006 kernel
4007 .execute(r#"write "/bin/hello.kai" 'echo "Hello from script!"'"#)
4008 .await
4009 .expect("write script failed");
4010
4011 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
4013
4014 let result = kernel
4016 .execute("hello")
4017 .await
4018 .expect("script execution failed");
4019
4020 assert!(result.ok(), "script failed: {}", result.err);
4021 assert_eq!(result.out.trim(), "Hello from script!");
4022 }
4023
4024 #[tokio::test]
4025 async fn test_script_with_args() {
4026 let kernel = Kernel::transient().expect("failed to create kernel");
4027
4028 kernel.execute(r#"mkdir "/bin""#).await.ok();
4030 kernel
4031 .execute(r#"write "/bin/greet.kai" 'echo "Hello, $1!"'"#)
4032 .await
4033 .expect("write script failed");
4034
4035 kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
4037
4038 let result = kernel
4040 .execute(r#"greet "World""#)
4041 .await
4042 .expect("script execution failed");
4043
4044 assert!(result.ok(), "script failed: {}", result.err);
4045 assert_eq!(result.out.trim(), "Hello, World!");
4046 }
4047
4048 #[tokio::test]
4049 async fn test_script_not_found() {
4050 let kernel = Kernel::transient().expect("failed to create kernel");
4051
4052 kernel.execute(r#"PATH="/nonexistent""#).await.expect("set PATH failed");
4054
4055 let result = kernel
4057 .execute("noscript")
4058 .await
4059 .expect("execution failed");
4060
4061 assert!(!result.ok(), "should fail with command not found");
4062 assert_eq!(result.code, 127);
4063 assert!(result.err.contains("command not found"));
4064 }
4065
4066 #[tokio::test]
4067 async fn test_script_path_search_order() {
4068 let kernel = Kernel::transient().expect("failed to create kernel");
4069
4070 kernel.execute(r#"mkdir "/first""#).await.ok();
4073 kernel.execute(r#"mkdir "/second""#).await.ok();
4074 kernel
4075 .execute(r#"write "/first/myscript.kai" 'echo "from first"'"#)
4076 .await
4077 .expect("write failed");
4078 kernel
4079 .execute(r#"write "/second/myscript.kai" 'echo "from second"'"#)
4080 .await
4081 .expect("write failed");
4082
4083 kernel.execute(r#"PATH="/first:/second""#).await.expect("set PATH failed");
4085
4086 let result = kernel
4088 .execute("myscript")
4089 .await
4090 .expect("script execution failed");
4091
4092 assert!(result.ok(), "script failed: {}", result.err);
4093 assert_eq!(result.out.trim(), "from first");
4094 }
4095
4096 #[tokio::test]
4101 async fn test_last_exit_code_success() {
4102 let kernel = Kernel::transient().expect("failed to create kernel");
4103
4104 let result = kernel.execute("true; echo $?").await.expect("execution failed");
4106 assert!(result.out.contains("0"), "expected 0, got: {}", result.out);
4107 }
4108
4109 #[tokio::test]
4110 async fn test_last_exit_code_failure() {
4111 let kernel = Kernel::transient().expect("failed to create kernel");
4112
4113 let result = kernel.execute("false; echo $?").await.expect("execution failed");
4115 assert!(result.out.contains("1"), "expected 1, got: {}", result.out);
4116 }
4117
4118 #[tokio::test]
4119 async fn test_current_pid() {
4120 let kernel = Kernel::transient().expect("failed to create kernel");
4121
4122 let result = kernel.execute("echo $$").await.expect("execution failed");
4123 let pid: u32 = result.out.trim().parse().expect("PID should be a number");
4125 assert!(pid > 0, "PID should be positive");
4126 }
4127
4128 #[tokio::test]
4129 async fn test_unset_variable_expands_to_empty() {
4130 let kernel = Kernel::transient().expect("failed to create kernel");
4131
4132 let result = kernel.execute(r#"echo "prefix:${UNSET_VAR}:suffix""#).await.expect("execution failed");
4134 assert_eq!(result.out.trim(), "prefix::suffix");
4135 }
4136
4137 #[tokio::test]
4138 async fn test_eq_ne_operators() {
4139 let kernel = Kernel::transient().expect("failed to create kernel");
4140
4141 let result = kernel.execute(r#"if [[ 5 -eq 5 ]]; then echo "eq works"; fi"#).await.expect("execution failed");
4143 assert_eq!(result.out.trim(), "eq works");
4144
4145 let result = kernel.execute(r#"if [[ 5 -ne 3 ]]; then echo "ne works"; fi"#).await.expect("execution failed");
4147 assert_eq!(result.out.trim(), "ne works");
4148
4149 let result = kernel.execute(r#"if [[ 5 -eq 3 ]]; then echo "wrong"; else echo "correct"; fi"#).await.expect("execution failed");
4151 assert_eq!(result.out.trim(), "correct");
4152 }
4153
4154 #[tokio::test]
4155 async fn test_escaped_dollar_in_string() {
4156 let kernel = Kernel::transient().expect("failed to create kernel");
4157
4158 let result = kernel.execute(r#"echo "\$100""#).await.expect("execution failed");
4160 assert_eq!(result.out.trim(), "$100");
4161 }
4162
4163 #[tokio::test]
4164 async fn test_special_vars_in_interpolation() {
4165 let kernel = Kernel::transient().expect("failed to create kernel");
4166
4167 let result = kernel.execute(r#"true; echo "exit: $?""#).await.expect("execution failed");
4169 assert_eq!(result.out.trim(), "exit: 0");
4170
4171 let result = kernel.execute(r#"echo "pid: $$""#).await.expect("execution failed");
4173 assert!(result.out.starts_with("pid: "), "unexpected output: {}", result.out);
4174 let pid_part = result.out.trim().strip_prefix("pid: ").unwrap();
4175 let _pid: u32 = pid_part.parse().expect("PID in string should be a number");
4176 }
4177
4178 #[tokio::test]
4183 async fn test_command_subst_assignment() {
4184 let kernel = Kernel::transient().expect("failed to create kernel");
4185
4186 let result = kernel.execute(r#"X=$(echo hello); echo "$X""#).await.expect("execution failed");
4188 assert_eq!(result.out.trim(), "hello");
4189 }
4190
4191 #[tokio::test]
4192 async fn test_command_subst_with_args() {
4193 let kernel = Kernel::transient().expect("failed to create kernel");
4194
4195 let result = kernel.execute(r#"X=$(echo "a b c"); echo "$X""#).await.expect("execution failed");
4197 assert_eq!(result.out.trim(), "a b c");
4198 }
4199
4200 #[tokio::test]
4201 async fn test_command_subst_nested_vars() {
4202 let kernel = Kernel::transient().expect("failed to create kernel");
4203
4204 let result = kernel.execute(r#"Y=world; X=$(echo "hello $Y"); echo "$X""#).await.expect("execution failed");
4206 assert_eq!(result.out.trim(), "hello world");
4207 }
4208
4209 #[tokio::test]
4210 async fn test_background_job_basic() {
4211 use std::time::Duration;
4212
4213 let kernel = Kernel::new(KernelConfig::isolated()).expect("failed to create kernel");
4214
4215 let result = kernel.execute("echo hello &").await.expect("execution failed");
4217 assert!(result.ok(), "background command should succeed: {}", result.err);
4218 assert!(result.out.contains("[1]"), "should return job ID: {}", result.out);
4219
4220 tokio::time::sleep(Duration::from_millis(100)).await;
4222
4223 let status = kernel.execute("cat /v/jobs/1/status").await.expect("status check failed");
4225 assert!(status.ok(), "status should succeed: {}", status.err);
4226 assert!(
4227 status.out.contains("done:") || status.out.contains("running"),
4228 "should have valid status: {}",
4229 status.out
4230 );
4231
4232 let stdout = kernel.execute("cat /v/jobs/1/stdout").await.expect("stdout check failed");
4234 assert!(stdout.ok());
4235 assert!(stdout.out.contains("hello"));
4236 }
4237
4238 #[tokio::test]
4239 async fn test_heredoc_piped_to_command() {
4240 let kernel = Kernel::transient().expect("kernel");
4242 let result = kernel.execute("cat <<EOF | cat\nhello world\nEOF").await.expect("exec");
4243 assert!(result.ok(), "heredoc | cat failed: {}", result.err);
4244 assert_eq!(result.out.trim(), "hello world");
4245 }
4246
4247 #[tokio::test]
4248 async fn test_for_loop_glob_iterates() {
4249 let kernel = Kernel::transient().expect("kernel");
4251 let dir = format!("/tmp/kaish_test_glob_{}", std::process::id());
4252 kernel.execute(&format!("mkdir -p {dir}")).await.unwrap();
4253 kernel.execute(&format!("echo a > {dir}/a.txt")).await.unwrap();
4254 kernel.execute(&format!("echo b > {dir}/b.txt")).await.unwrap();
4255 let result = kernel.execute(&format!(r#"
4256 N=0
4257 for F in $(glob "{dir}/*.txt"); do
4258 N=$((N + 1))
4259 done
4260 echo $N
4261 "#)).await.unwrap();
4262 assert!(result.ok(), "for glob failed: {}", result.err);
4263 assert_eq!(result.out.trim(), "2", "Should iterate 2 files, got: {}", result.out);
4264 kernel.execute(&format!("rm {dir}/a.txt")).await.unwrap();
4265 kernel.execute(&format!("rm {dir}/b.txt")).await.unwrap();
4266 }
4267
4268 #[tokio::test]
4269 async fn test_command_subst_echo_not_iterable() {
4270 let kernel = Kernel::transient().expect("kernel");
4272 let result = kernel.execute(r#"
4273 N=0
4274 for X in $(echo "a b c"); do N=$((N + 1)); done
4275 echo $N
4276 "#).await.unwrap();
4277 assert!(result.ok());
4278 assert_eq!(result.out.trim(), "1", "echo should be one item: {}", result.out);
4279 }
4280
4281 #[test]
4284 fn test_accumulate_no_double_newlines() {
4285 let mut acc = ExecResult::success("line1\n");
4287 let new = ExecResult::success("line2\n");
4288 accumulate_result(&mut acc, &new);
4289 assert_eq!(acc.out, "line1\nline2\n");
4290 assert!(!acc.out.contains("\n\n"), "should not have double newlines: {:?}", acc.out);
4291 }
4292
4293 #[test]
4294 fn test_accumulate_adds_separator_when_needed() {
4295 let mut acc = ExecResult::success("line1");
4297 let new = ExecResult::success("line2");
4298 accumulate_result(&mut acc, &new);
4299 assert_eq!(acc.out, "line1\nline2");
4300 }
4301
4302 #[test]
4303 fn test_accumulate_empty_into_nonempty() {
4304 let mut acc = ExecResult::success("");
4305 let new = ExecResult::success("hello\n");
4306 accumulate_result(&mut acc, &new);
4307 assert_eq!(acc.out, "hello\n");
4308 }
4309
4310 #[test]
4311 fn test_accumulate_nonempty_into_empty() {
4312 let mut acc = ExecResult::success("hello\n");
4313 let new = ExecResult::success("");
4314 accumulate_result(&mut acc, &new);
4315 assert_eq!(acc.out, "hello\n");
4316 }
4317
4318 #[test]
4319 fn test_accumulate_stderr_no_double_newlines() {
4320 let mut acc = ExecResult::failure(1, "err1\n");
4321 let new = ExecResult::failure(1, "err2\n");
4322 accumulate_result(&mut acc, &new);
4323 assert!(!acc.err.contains("\n\n"), "stderr should not have double newlines: {:?}", acc.err);
4324 }
4325
4326 #[tokio::test]
4327 async fn test_multiple_echo_no_blank_lines() {
4328 let kernel = Kernel::transient().expect("kernel");
4329 let result = kernel
4330 .execute("echo one\necho two\necho three")
4331 .await
4332 .expect("execution failed");
4333 assert!(result.ok());
4334 assert_eq!(result.out, "one\ntwo\nthree\n");
4335 }
4336
4337 #[tokio::test]
4338 async fn test_for_loop_no_blank_lines() {
4339 let kernel = Kernel::transient().expect("kernel");
4340 let result = kernel
4341 .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
4342 .await
4343 .expect("execution failed");
4344 assert!(result.ok());
4345 assert_eq!(result.out, "item: a\nitem: b\nitem: c\n");
4346 }
4347
4348 #[tokio::test]
4349 async fn test_for_command_subst_no_blank_lines() {
4350 let kernel = Kernel::transient().expect("kernel");
4351 let result = kernel
4352 .execute(r#"for N in $(seq 1 3); do echo "n=${N}"; done"#)
4353 .await
4354 .expect("execution failed");
4355 assert!(result.ok());
4356 assert_eq!(result.out, "n=1\nn=2\nn=3\n");
4357 }
4358
4359}