#![allow(clippy::unwrap_used, clippy::expect_used)]
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);
}
#[cfg(feature = "localfs")]
#[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
);
}
#[cfg(feature = "localfs")]
#[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,
);
}
#[tokio::test]
async fn test_top_level_return_keeps_prior_output_and_stops_execution() {
use kaish_kernel::KernelConfig;
let kernel = Kernel::new(KernelConfig::transient().with_skip_validation(true)).unwrap();
let result = kernel
.execute("echo setup-ok; return 0; echo after")
.await
.expect("skip-validation kernel should run a top-level return");
assert_eq!(result.code, 0);
assert!(
result.text_out().contains("setup-ok"),
"return must not discard prior statements' output: {:?}",
result.text_out()
);
assert!(
!result.text_out().contains("after"),
"return must stop execution of subsequent statements: {:?}",
result.text_out()
);
}
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");
}
#[tokio::test]
async fn test_double_dash_allows_bareword_assignment() {
let kernel = Kernel::transient().unwrap();
let result = kernel.execute("echo -- a=b").await.unwrap();
assert_eq!(result.text_out().trim(), "a=b");
}
#[tokio::test]
async fn test_double_dash_allows_multiple_bareword_assignments() {
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");
}
#[tokio::test]
async fn test_local_assignment_propagates_subst_status() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute("f() { local x=$(false) }; f; echo $?")
.await
.unwrap();
assert_eq!(result.text_out().trim(), "1");
}
#[tokio::test]
async fn test_local_assignment_or_fallback_fires() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(r#"f() { local x="$(false)" || echo FIRED }; f"#)
.await
.unwrap();
assert_eq!(result.text_out().trim(), "FIRED");
}
#[tokio::test]
async fn test_list_literal_last_subst_wins_success() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute("xs=[$(false) $(true)]; echo $?")
.await
.unwrap();
assert_eq!(result.text_out().trim(), "0");
}
#[tokio::test]
async fn test_list_literal_last_subst_wins_failure() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute("xs=[$(true) $(false)]; echo $?")
.await
.unwrap();
assert_eq!(result.text_out().trim(), "1");
}
#[tokio::test]
async fn test_list_literal_failed_subst_fires_or_fallback() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute("xs=[$(true) $(false)] || echo FIRED")
.await
.unwrap();
assert_eq!(result.text_out().trim(), "FIRED");
}
#[tokio::test]
async fn test_record_literal_failed_subst_propagates() {
let kernel = Kernel::transient().unwrap();
let result = kernel.execute("r={k: $(false)}; echo $?").await.unwrap();
assert_eq!(result.text_out().trim(), "1");
}
#[tokio::test]
async fn test_errexit_preserves_builtin_error_message() {
let with_e = Kernel::transient().unwrap();
let with_e_result = with_e.execute("set -e; wc --bogus-flag-xyz").await.unwrap();
assert!(!with_e_result.ok(), "wc with an unknown flag should fail");
assert_eq!(with_e_result.code, 2, "clap usage errors exit 2");
assert!(
!with_e_result.err.is_empty(),
"set -e must not discard the failing command's error message"
);
let without_e = Kernel::transient().unwrap();
let without_e_result = without_e.execute("wc --bogus-flag-xyz").await.unwrap();
assert_eq!(
with_e_result.code, without_e_result.code,
"set -e must not change the exit code"
);
assert_eq!(
with_e_result.err, without_e_result.err,
"set -e must not change the error message"
);
}
#[cfg(feature = "localfs")]
#[tokio::test]
async fn test_errexit_preserves_message_for_missing_file() {
let kernel = Kernel::transient().unwrap();
let result = kernel.execute("set -e; cat /nope").await.unwrap();
assert!(!result.ok());
assert_eq!(result.code, 1);
assert!(
!result.err.is_empty(),
"set -e must not discard cat's error message"
);
assert!(
result.err.contains("/nope"),
"error should name the missing path: {}",
result.err
);
}
#[tokio::test]
async fn test_errexit_preserves_message_in_for_loop_body() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
"set -e
for x in a; do
echo pre
wc --bogus-flag-xyz
echo SHOULD_NOT_RUN
done
echo ALSO_NOT_RUN",
)
.await
.unwrap();
assert_eq!(result.code, 2);
assert!(
!result.err.is_empty(),
"set -e in a for-loop body must not discard the message"
);
assert!(
result.text_out().contains("pre"),
"output before the failure in the same iteration is kept: {}",
result.text_out()
);
assert!(!result.text_out().contains("SHOULD_NOT_RUN"));
assert!(!result.text_out().contains("ALSO_NOT_RUN"));
}
#[tokio::test]
async fn test_errexit_preserves_message_in_while_loop_body() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
"set -e
i=0
while [[ $i -lt 1 ]]; do
echo pre
wc --bogus-flag-xyz
echo SHOULD_NOT_RUN
i=1
done
echo ALSO_NOT_RUN",
)
.await
.unwrap();
assert_eq!(result.code, 2);
assert!(
!result.err.is_empty(),
"set -e in a while-loop body must not discard the message"
);
assert!(
result.text_out().contains("pre"),
"output before the failure in the same iteration is kept: {}",
result.text_out()
);
assert!(!result.text_out().contains("SHOULD_NOT_RUN"));
assert!(!result.text_out().contains("ALSO_NOT_RUN"));
}
#[tokio::test]
async fn test_errexit_still_aborts_statement_list() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute("set -e; wc --bogus-flag-xyz; echo SHOULD_NOT_RUN")
.await
.unwrap();
assert_eq!(result.code, 2);
assert!(
!result.text_out().contains("SHOULD_NOT_RUN"),
"a statement after the -e failure must not run: {}",
result.text_out()
);
}
#[tokio::test]
async fn test_errexit_success_continues() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute("set -e; true; echo CONTINUED")
.await
.unwrap();
assert!(result.ok());
assert_eq!(result.code, 0);
assert_eq!(result.text_out().trim(), "CONTINUED");
}
#[tokio::test]
async fn test_exit_n_carries_no_message_of_its_own() {
let kernel = Kernel::transient().unwrap();
let result = kernel.execute("echo hi; exit 3").await.unwrap();
assert_eq!(result.code, 3);
assert_eq!(result.text_out().trim(), "hi");
assert!(
result.err.is_empty(),
"exit N has no message of its own: {}",
result.err
);
}
#[tokio::test]
async fn test_return_n_carries_no_message_of_its_own() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute("f() { echo hi; return 3; }; f")
.await
.unwrap();
assert_eq!(result.code, 3);
assert_eq!(result.text_out().trim(), "hi");
assert!(
result.err.is_empty(),
"return N has no message of its own: {}",
result.err
);
}