use kaish_kernel::Kernel;
#[tokio::test]
async fn test_braced_vs_unbraced_exit_code_equivalence() {
let kernel = Kernel::transient().unwrap();
let result1 = kernel.execute("false; echo $?").await.unwrap();
let result2 = kernel.execute("false; echo ${?}").await.unwrap();
assert_eq!(
result1.text_out().trim(),
result2.text_out().trim(),
"Braced and unbraced $? should be equivalent"
);
}
#[tokio::test]
async fn test_pid_in_arithmetic() {
let kernel = Kernel::transient().unwrap();
let result = kernel.execute("echo $(( $$ % 100000 ))").await.unwrap();
let val: i64 = result.text_out().trim().parse().expect("Should be a number");
assert!(val >= 0, "PID mod should be non-negative");
}
#[tokio::test]
async fn test_braced_current_pid() {
let kernel = Kernel::transient().unwrap();
let result = kernel.execute("echo ${$}").await.unwrap();
let pid: u64 = result
.text_out()
.trim()
.parse()
.expect("${$} should be a number");
assert!(pid > 0, "PID should be positive: {}", pid);
}
#[tokio::test]
async fn test_braced_vs_unbraced_pid_equivalence() {
let kernel = Kernel::transient().unwrap();
let result1 = kernel.execute("echo $$").await.unwrap();
let result2 = kernel.execute("echo ${$}").await.unwrap();
assert_eq!(
result1.text_out().trim(),
result2.text_out().trim(),
"Braced and unbraced $$ should be equivalent"
);
}
#[tokio::test]
async fn test_two_kernels_have_distinct_pids() {
let k1 = Kernel::transient().unwrap();
let k2 = Kernel::transient().unwrap();
let p1 = k1.execute("echo $$").await.unwrap().text_out().trim().to_string();
let p2 = k2.execute("echo $$").await.unwrap().text_out().trim().to_string();
assert_ne!(p1, p2, "two kernels should have different $$, got {p1} and {p2}");
}
#[tokio::test]
async fn test_fork_inherits_parent_pid() {
let parent = Kernel::transient().unwrap();
let parent_pid = parent
.execute("echo $$")
.await
.unwrap()
.text_out()
.trim()
.to_string();
let fork = parent.fork().await;
let fork_pid = fork
.execute("echo $$")
.await
.unwrap()
.text_out()
.trim()
.to_string();
assert_eq!(parent_pid, fork_pid, "fork should inherit parent $$");
}
#[tokio::test]
async fn test_kaish_clear_preserves_pid() {
let kernel = Kernel::transient().unwrap();
let before = kernel.execute("echo $$").await.unwrap().text_out().trim().to_string();
kernel.execute("kaish-clear").await.unwrap();
let after = kernel.execute("echo $$").await.unwrap().text_out().trim().to_string();
assert_eq!(before, after, "kaish-clear should preserve $$");
}
#[tokio::test]
async fn test_local_variable_scoping() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
x=outer
f() {
local x = inner
echo "in func: $x"
}
f
echo "after func: $x"
"#,
)
.await
.unwrap();
assert!(
result.text_out().contains("in func: inner"),
"Local var should be 'inner' inside function: {}",
result.text_out()
);
assert!(
result.text_out().contains("after func: outer"),
"Outer var should be 'outer' after function: {}",
result.text_out()
);
}
#[tokio::test]
async fn test_local_does_not_affect_outer_scope() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
count=0
increment() {
local count = 99
}
increment
echo $count
"#,
)
.await
.unwrap();
assert_eq!(
result.text_out().trim(),
"0",
"Outer 'count' should still be 0: {}",
result.text_out()
);
}
#[tokio::test]
async fn test_non_local_modifies_outer_scope() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
count=0
increment() {
count=99
}
increment
echo $count
"#,
)
.await
.unwrap();
assert_eq!(
result.text_out().trim(),
"99",
"Without local, 'count' should be modified: {}",
result.text_out()
);
}
#[tokio::test]
async fn test_local_with_command_substitution() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
val=original
f() {
local val = $(echo "from_cmd")
echo "local: $val"
}
f
echo "outer: $val"
"#,
)
.await
.unwrap();
assert!(
result.text_out().contains("local: from_cmd"),
"Local with cmd subst: {}",
result.text_out()
);
assert!(
result.text_out().contains("outer: original"),
"Outer unchanged: {}",
result.text_out()
);
}
#[tokio::test]
async fn test_positional_params_arithmetic_with_variable() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
calc() {
local base = 10
echo $(($base + $1))
}
calc 5
"#,
)
.await
.unwrap();
assert_eq!(
result.text_out().trim(),
"15",
"$(($base + $1)) with base=10 and $1=5 should be 15: {}",
result.text_out()
);
}
#[tokio::test]
async fn test_command_subst_with_explicit_split() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
for x in $(split "a b c"); do
echo "item: $x"
done
"#,
)
.await
.unwrap();
assert!(
result.text_out().contains("item: a"),
"Should have item a: {}",
result.text_out()
);
assert!(
result.text_out().contains("item: b"),
"Should have item b: {}",
result.text_out()
);
assert!(
result.text_out().contains("item: c"),
"Should have item c: {}",
result.text_out()
);
}
#[tokio::test]
async fn test_for_loop_with_seq_returns_array() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
for num in $(seq 1 3); do
echo "number $num"
done
"#,
)
.await
.unwrap();
assert!(
result.text_out().contains("number 1"),
"Should have number 1: {}",
result.text_out()
);
assert!(
result.text_out().contains("number 2"),
"Should have number 2: {}",
result.text_out()
);
assert!(
result.text_out().contains("number 3"),
"Should have number 3: {}",
result.text_out()
);
}
#[tokio::test]
async fn test_stdout_to_stderr_redirect_1_ampersand_2() {
let kernel = Kernel::transient().unwrap();
let result = kernel.execute("echo error 1>&2").await.unwrap();
assert!(
result.err.contains("error"),
"Expected 'error' in stderr: stdout={}, stderr={}",
result.text_out(),
result.err
);
}
#[tokio::test]
async fn test_stdout_to_stderr_redirect_ampersand_2() {
let kernel = Kernel::transient().unwrap();
let result = kernel.execute("echo warning >&2").await.unwrap();
assert!(
result.err.contains("warning"),
"Expected 'warning' in stderr: stdout={}, stderr={}",
result.text_out(),
result.err
);
}
#[tokio::test]
async fn test_file_test_sees_vfs_dirs() {
let kernel = Kernel::transient().unwrap();
kernel.execute("mkdir -p /v/testdir").await.unwrap();
let result = kernel.execute(r#"[[ -d /v/testdir ]] && echo "found" || echo "missing""#).await.unwrap();
assert_eq!(result.text_out().trim(), "found", "[[ -d ]] should see VFS dirs: {}", result.text_out());
}
#[tokio::test]
async fn test_file_test_sees_vfs_files() {
let kernel = Kernel::transient().unwrap();
kernel.execute("write /v/testfile 'hello'").await.unwrap();
let result = kernel.execute(r#"[[ -f /v/testfile ]] && echo "found" || echo "missing""#).await.unwrap();
assert_eq!(result.text_out().trim(), "found", "[[ -f ]] should see VFS files: {}", result.text_out());
}
#[tokio::test]
async fn test_file_test_exists_vfs() {
let kernel = Kernel::transient().unwrap();
kernel.execute("write /v/somefile 'data'").await.unwrap();
let result = kernel.execute(r#"[[ -e /v/somefile ]] && echo "found" || echo "missing""#).await.unwrap();
assert_eq!(result.text_out().trim(), "found", "[[ -e ]] should see VFS entries: {}", result.text_out());
}
#[tokio::test]
async fn test_short_flag_with_value_head() {
let kernel = Kernel::transient().unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().display();
kernel.execute(&format!(r#"printf "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n" > {path}"#)).await.unwrap();
let result = kernel.execute(&format!("head -n 3 {path}")).await.unwrap();
assert!(result.ok(), "head -n 3 should succeed: err={}", result.err);
let text = result.text_out();
let lines: Vec<&str> = text.trim().lines().collect();
assert_eq!(lines, vec!["1", "2", "3"], "head -n 3 should return first 3 lines, got: {:?}", lines);
}
#[tokio::test]
async fn test_short_flag_with_value_tail() {
let kernel = Kernel::transient().unwrap();
let tmp = tempfile::NamedTempFile::new().unwrap();
let path = tmp.path().display();
kernel.execute(&format!(r#"printf "1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n" > {path}"#)).await.unwrap();
let result = kernel.execute(&format!("tail -n 3 {path}")).await.unwrap();
assert!(result.ok(), "tail -n 3 should succeed: err={}", result.err);
let text = result.text_out();
let lines: Vec<&str> = text.trim().lines().collect();
assert_eq!(lines, vec!["8", "9", "10"], "tail -n 3 should return last 3 lines, got: {:?}", lines);
}
#[tokio::test]
async fn test_cmd_subst_cwd_isolation() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
go_root() { cd /; pwd; }
X=$(go_root)
pwd
"#,
)
.await
.unwrap();
assert!(result.ok(), "Should succeed: err={}", result.err);
let text = result.text_out();
let pwd_output = text.trim();
assert_ne!(
pwd_output, "/",
"CWD should not leak from command substitution, got: {}",
pwd_output
);
}
#[tokio::test]
async fn test_cmd_subst_in_string_cwd_isolation() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
go_root() { cd /; pwd; }
echo "dir: $(go_root)"
pwd
"#,
)
.await
.unwrap();
assert!(result.ok(), "Should succeed: err={}", result.err);
let text = result.text_out();
let lines: Vec<&str> = text.trim().lines().collect();
assert!(lines.len() >= 2, "Expected at least 2 lines: {:?}", lines);
assert!(
lines[0].contains("dir: /"),
"Captured output should contain '/': {}",
lines[0]
);
assert_ne!(
lines[1], "/",
"CWD should not leak from string command substitution: {}",
lines[1]
);
}
#[tokio::test]
async fn test_cmd_subst_captures_output_correctly() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
go_root() { cd /; pwd; }
X=$(go_root)
echo "captured: $X"
"#,
)
.await
.unwrap();
assert!(result.ok(), "Should succeed: err={}", result.err);
assert!(
result.text_out().contains("captured: /"),
"Should capture '/' from subshell: {}",
result.text_out()
);
}
#[tokio::test]
async fn test_find_name_filter_through_parser() {
let kernel = Kernel::transient().unwrap();
kernel
.execute("mkdir -p /v/proj/src; mkdir -p /v/proj/docs")
.await
.unwrap();
kernel
.execute(
"write /v/proj/src/main.rs 'fn main() {}'; \
write /v/proj/src/lib.rs 'pub mod lib;'; \
write /v/proj/docs/README.md '# Docs'; \
write /v/proj/Cargo.toml '[package]'",
)
.await
.unwrap();
let result = kernel
.execute("find /v/proj -name '*.rs'")
.await
.unwrap();
assert!(result.ok(), "find should succeed: {}", result.err);
let out = result.text_out();
assert!(out.contains("main.rs"), "should find main.rs: {out}");
assert!(out.contains("lib.rs"), "should find lib.rs: {out}");
assert!(
!out.contains("README.md"),
"-name '*.rs' should exclude README.md: {out}"
);
assert!(
!out.contains("Cargo.toml"),
"-name '*.rs' should exclude Cargo.toml: {out}"
);
}
#[tokio::test]
async fn test_find_type_filter_through_parser() {
let kernel = Kernel::transient().unwrap();
kernel
.execute("mkdir -p /v/proj/src; write /v/proj/src/main.rs 'fn main() {}'")
.await
.unwrap();
let files_result = kernel
.execute("find /v/proj -type f")
.await
.unwrap();
assert!(files_result.ok(), "find -type f should succeed: {}", files_result.err);
let files_out = files_result.text_out();
assert!(
files_out.contains("main.rs"),
"-type f should include files: {files_out}"
);
assert!(
!files_out.contains("/src\n"),
"-type f should exclude directories: {files_out}"
);
let dirs_result = kernel
.execute("find /v/proj -type d")
.await
.unwrap();
assert!(dirs_result.ok(), "find -type d should succeed: {}", dirs_result.err);
let dirs_out = dirs_result.text_out();
assert!(
dirs_out.contains("src"),
"-type d should include directories: {dirs_out}"
);
assert!(
!dirs_out.contains("main.rs"),
"-type d should exclude files: {dirs_out}"
);
}
#[tokio::test]
async fn test_argv_digit_leading_hex_passes_through() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(r#"echo 019dda1c "msg""#)
.await
.expect("parse + exec should succeed for digit-leading bareword");
assert!(result.ok(), "echo failed: err={}", result.err);
assert_eq!(result.text_out().trim(), "019dda1c msg");
}
#[tokio::test]
async fn test_argv_digit_leading_uuid_passes_through() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute("echo 019dda1c-5b3f-7000-abcd-0123456789ab")
.await
.expect("UUID-like bareword should lex");
assert!(result.ok(), "echo failed: err={}", result.err);
assert_eq!(
result.text_out().trim(),
"019dda1c-5b3f-7000-abcd-0123456789ab"
);
}
#[tokio::test]
async fn test_argv_dot_prefixed_simple() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(r#"echo .parent "msg""#)
.await
.expect("dot-prefixed bareword should lex");
assert!(result.ok(), "echo failed: err={}", result.err);
assert_eq!(result.text_out().trim(), ".parent msg");
}
#[tokio::test]
async fn test_argv_dot_prefixed_chained() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute("echo .parent.parent")
.await
.expect("chained .parent.parent should lex as one token");
assert!(result.ok(), "echo failed: err={}", result.err);
assert_eq!(result.text_out().trim(), ".parent.parent");
}
#[tokio::test]
async fn test_argv_dot_prefixed_hidden_file() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute("echo .gitignore")
.await
.expect("hidden filename should not be parsed as source command");
assert!(result.ok(), "echo failed: err={}", result.err);
assert_eq!(result.text_out().trim(), ".gitignore");
}
#[tokio::test]
async fn test_source_alias_with_space_still_works() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(". /nonexistent-kaish-source-test")
.await
.expect("`. file` should still be parsed as source command");
assert!(
!result.ok(),
"source of nonexistent file should fail (proves source ran)"
);
assert!(
result.err.contains("source") || result.err.contains("not found")
|| result.err.contains("No such file"),
"expected source error, got: stdout={}, stderr={}",
result.text_out(),
result.err,
);
}
async fn test_truthy(expr: &str) -> bool {
let kernel = Kernel::transient().unwrap();
let script = format!("if [[ {expr} ]]; then echo Y; else echo N; fi");
let out = kernel.execute(&script).await.unwrap();
match out.text_out().trim() {
"Y" => true,
"N" => false,
other => panic!("unexpected output {other:?} for [[ {expr} ]]"),
}
}
#[tokio::test]
async fn test_bang_binds_tighter_than_or() {
assert!(
test_truthy(r#"! "a" == "b" || "a" == "a""#).await,
"`! A || B` must parse as `(!A) || B`"
);
assert!(
test_truthy(r#"! "a" == "a" || "b" == "b""#).await,
"`! A || B` with A true must still be `(!A) || B`"
);
}
#[tokio::test]
async fn test_bang_binds_tighter_than_and() {
assert!(
!test_truthy(r#"! "a" == "a" && "b" == "b""#).await,
"`! A && B` must parse as `(!A) && B`"
);
assert!(
test_truthy(r#"! "a" == "b" && "b" == "b""#).await,
"`! A && B` with A false is `(!A) && B` == true"
);
}
#[tokio::test]
async fn test_bang_single_term_unaffected() {
assert!(test_truthy(r#"! "a" == "b""#).await, "!(a==b) is true");
assert!(!test_truthy(r#"! "a" == "a""#).await, "!(a==a) is false");
assert!(test_truthy(r#"! ! "a" == "a""#).await, "!!(a==a) is true");
}
#[tokio::test]
async fn test_double_dash_protects_internal_hyphen_word() {
let kernel = Kernel::transient().unwrap();
let result = kernel.execute("echo -- -not-a-flag").await.unwrap();
assert_eq!(result.text_out().trim(), "-not-a-flag");
}
#[tokio::test]
async fn test_double_dash_protects_multiple_dash_words() {
let kernel = Kernel::transient().unwrap();
let result = kernel.execute("echo -- -a-b -c-d").await.unwrap();
assert_eq!(result.text_out().trim(), "-a-b -c-d");
}