#![cfg(feature = "subprocess")]
use kaish_kernel::{Kernel, KernelConfig};
fn repl_kernel() -> Kernel {
Kernel::new(KernelConfig::repl()).expect("Failed to create kernel")
}
#[tokio::test]
async fn external_command_basic() {
let kernel = repl_kernel();
let result = kernel.execute("true").await.unwrap();
assert!(result.ok(), "true should succeed: {:?}", result);
}
#[tokio::test]
async fn external_command_with_args() {
let kernel = repl_kernel();
let result = kernel.execute("echo hello world").await.unwrap();
assert!(result.ok());
}
#[tokio::test]
async fn external_command_not_found() {
let kernel = repl_kernel();
let result = kernel
.execute("definitely_not_a_real_command_12345")
.await
.unwrap();
assert_eq!(result.code, 127, "Should return 127 for command not found");
assert!(
result.err.contains("command not found"),
"Error should mention 'command not found': {}",
result.err
);
}
#[tokio::test]
async fn external_command_date_format() {
let kernel = repl_kernel();
let result = kernel.execute("date +%s").await.unwrap();
assert!(result.ok(), "date +%s should succeed: {:?}", result);
let text = result.text_out();
let out = text.trim();
assert!(
out.chars().all(|c| c.is_ascii_digit()),
"date +%s should output digits: '{}'",
out
);
}
#[tokio::test]
async fn external_command_date_complex_format() {
let kernel = repl_kernel();
let result = kernel.execute("date +%Y-%m-%d").await.unwrap();
assert!(result.ok(), "date +%Y-%m-%d should succeed: {:?}", result);
let text = result.text_out();
let out = text.trim();
assert_eq!(out.len(), 10, "Date should be 10 chars: '{}'", out);
assert!(out.contains('-'), "Date should have dashes: '{}'", out);
}
#[tokio::test]
async fn external_command_short_flags() {
let kernel = repl_kernel();
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().display();
let result = kernel.execute(&format!("ls -la {path}")).await.unwrap();
assert!(result.ok(), "ls -la should succeed: {:?}", result);
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn external_command_long_flags() {
let kernel = repl_kernel();
let result = kernel.execute("spawn --command uname --argv '--kernel-name'").await.unwrap();
assert!(result.ok(), "spawn uname --kernel-name should succeed: {:?}", result);
assert!(
result.text_out().contains("Linux"),
"Should show Linux: {}",
result.text_out()
);
}
#[tokio::test]
async fn external_command_exit_code_success() {
let kernel = repl_kernel();
let result = kernel.execute("true").await.unwrap();
assert_eq!(result.code, 0, "true should exit with 0");
}
#[tokio::test]
async fn external_command_exit_code_failure() {
let kernel = repl_kernel();
let result = kernel.execute("false").await.unwrap();
assert_eq!(result.code, 1, "false should exit with 1");
}
#[tokio::test]
async fn external_command_exit_code_specific() {
let kernel = repl_kernel();
let result = kernel.execute("sh -c 'exit 42'").await.unwrap();
assert_eq!(result.code, 42, "Should preserve exit code 42");
}
#[tokio::test]
async fn external_command_stdin_piping() {
let kernel = repl_kernel();
let result = kernel.execute("echo 'hello world' | wc -c").await.unwrap();
assert!(result.ok(), "pipe should succeed: {:?}", result);
let count: i64 = result.text_out().trim().parse().unwrap_or(-1);
assert_eq!(count, 12, "wc -c should count 12 chars: {}", result.text_out());
}
#[tokio::test]
async fn external_command_respects_cwd() {
let kernel = repl_kernel();
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().to_string_lossy().to_string();
kernel.execute(&format!("cd {path}")).await.unwrap();
let result = kernel.execute("pwd").await.unwrap();
assert!(result.ok(), "pwd should succeed: {:?}", result);
assert!(
result.text_out().contains(&path),
"Should be in {}: {}",
path,
result.text_out()
);
}
#[tokio::test]
async fn pipeline_builtin_to_external() {
let kernel = repl_kernel();
let result = kernel
.execute("echo 'c\nb\na' | sort")
.await
.unwrap();
assert!(result.ok(), "pipeline should succeed: {:?}", result);
let text = result.text_out();
let lines: Vec<&str> = text.trim().lines().collect();
assert_eq!(lines, vec!["a", "b", "c"], "Should be sorted: {:?}", lines);
}
#[tokio::test]
async fn pipeline_builtin_to_builtin() {
let kernel = repl_kernel();
let result = kernel.execute("seq 1 10 | head -n 3").await.unwrap();
assert!(result.ok(), "pipeline should succeed: {:?}", result);
let text = result.text_out();
let lines: Vec<&str> = text.trim().lines().collect();
assert_eq!(lines, vec!["1", "2", "3"], "Should have first 3: {:?}", lines);
}
#[tokio::test]
async fn external_command_is_hermetic_by_default() {
let kernel = repl_kernel();
assert!(
std::env::var_os("PATH").is_some(),
"test precondition: PATH must be set for cargo test"
);
let result = kernel.execute("printenv PATH").await.unwrap();
assert!(
!result.ok(),
"printenv PATH must fail in hermetic kernel: {:?}",
result
);
}
#[tokio::test]
async fn external_command_sees_initial_vars() {
use kaish_kernel::ast::Value;
use std::collections::HashMap;
let mut vars = HashMap::new();
vars.insert("PATH".to_string(), Value::String("/usr/bin:/bin".into()));
vars.insert("MY_PROBE".to_string(), Value::String("seeded".into()));
let kernel = Kernel::new(KernelConfig::repl().with_initial_vars(vars))
.expect("Failed to create kernel");
let result = kernel.execute("printenv MY_PROBE").await.unwrap();
assert!(result.ok(), "printenv MY_PROBE should succeed: {:?}", result);
assert_eq!(result.text_out().trim(), "seeded");
}
fn interactive_kernel() -> Kernel {
Kernel::new(KernelConfig::repl().with_interactive(true)).expect("Failed to create kernel")
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn non_interactive_stdin_is_dev_null() {
let kernel = repl_kernel();
let result = kernel
.execute("readlink /proc/self/fd/0")
.await
.unwrap();
assert!(result.ok(), "readlink should succeed: {:?}", result);
assert_eq!(
result.text_out().trim(),
"/dev/null",
"Non-interactive external command stdin should be /dev/null: {}",
result.text_out()
);
}
#[cfg(target_os = "linux")]
#[tokio::test]
#[ignore = "requires TTY stdin — fails when cargo test runs with stdin=/dev/null"]
async fn interactive_stdin_is_not_dev_null() {
let kernel = interactive_kernel();
let result = kernel
.execute("readlink /proc/self/fd/0 | cat")
.await
.unwrap();
assert!(result.ok(), "readlink should succeed: {:?}", result);
assert_ne!(
result.text_out().trim(),
"/dev/null",
"Interactive external command stdin should NOT be /dev/null: {}",
result.text_out()
);
}
#[tokio::test]
async fn interactive_piped_stdin_still_works() {
let kernel = interactive_kernel();
let result = kernel
.execute("echo hello | grep hello")
.await
.unwrap();
assert_eq!(
result.code, 0,
"grep should find 'hello' in piped input (exit 0): {:?}",
result
);
}
#[tokio::test]
async fn minus_alone_lexes_correctly() {
let kernel = repl_kernel();
let result = kernel.execute("echo - foo -").await.unwrap();
assert!(result.ok(), "echo should succeed: {:?}", result);
assert!(result.text_out().contains("- foo -"), "Should include dashes: {}", result.text_out());
}