#![allow(clippy::unwrap_used, clippy::expect_used)]
#![cfg(feature = "localfs")]
use kaish_kernel::{Kernel, KernelConfig};
use rstest::rstest;
use tempfile::tempdir;
fn kernel() -> Kernel {
Kernel::new(KernelConfig::repl().with_trash(false)).expect("failed to create kernel")
}
#[rstest]
#[case::substitution_bare("echo $(echo sub) | cat", "sub\n")]
#[case::substitution_quoted("echo \"$(echo sub)\" | cat", "sub\n")]
#[case::substitution_glued("echo \"a$(echo b)\" | cat", "ab\n")]
#[case::literal_text_either_side("echo \"x:[$(echo C)]\" | cat", "x:[C]\n")]
#[case::literal_text_either_side_dashes("echo \"pre-$(echo M)-post\" | cat", "pre-M-post\n")]
#[case::substitution_feeding_seq("seq 1 $(echo 3) | cat", "1\n2\n3\n")]
#[case::substitution_through_two_pipes("echo $(echo sub) | cat | cat", "sub\n")]
#[case::function_body("f() { echo out; }; f | cat", "out\n")]
#[case::function_body_multi_statement("f() { echo a; echo b; }; f | cat", "a\nb\n")]
#[tokio::test]
async fn a_non_last_stage_stdout_survives_a_nested_dispatch(
#[case] source: &str,
#[case] expected: &str,
) {
let kernel = kernel();
let result = kernel.execute(source).await.expect("execution failed");
assert_eq!(
result.text_out(),
expected,
"`{source}` lost its stdout in the pipe (exit {}, stderr {:?})",
result.code,
result.err
);
}
#[tokio::test]
async fn losing_stage_output_was_silent_so_pin_the_success_code() {
let kernel = kernel();
let result = kernel
.execute("echo $(echo sub) | cat")
.await
.expect("execution failed");
assert_eq!(result.code, 0, "stderr: {:?}", result.err);
assert!(
!result.text_out().is_empty(),
"exit 0 with empty stdout is the exact shape of the bug"
);
}
#[tokio::test]
async fn substitution_still_consumes_the_stage_stdin() {
let kernel = kernel();
let result = kernel
.execute("echo hi | echo $(cat)")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "hi\n");
}
#[rstest]
#[case::pipeline_inside_a_substitution("echo \"$(echo a | cat)\" | cat", "a\n")]
#[case::assignment_is_not_a_stage("x=\"$(echo a | cat)\"; echo $x | cat", "a\n")]
#[case::assignment_of_a_bare_substitution("x=\"$(echo a)\"; echo $x | cat", "a\n")]
#[tokio::test]
async fn a_pipeline_inside_a_substitution_keeps_working(
#[case] source: &str,
#[case] expected: &str,
) {
let kernel = kernel();
let result = kernel.execute(source).await.expect("execution failed");
assert_eq!(result.text_out(), expected, "`{source}` regressed");
}
#[tokio::test]
async fn a_last_stage_substitution_is_unaffected() {
let kernel = kernel();
let result = kernel
.execute("echo x | echo $(echo sub)")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "sub\n");
}
#[tokio::test]
async fn a_plain_stage_still_pipes() {
let kernel = kernel();
let result = kernel
.execute("echo plain | cat")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "plain\n");
}
#[rstest]
#[case::source_keyword("source helper.kai | cat", "hello\n")]
#[case::source_dot(". helper.kai | cat", "hello\n")]
#[tokio::test]
async fn a_sourced_or_scripted_stage_keeps_its_stdout(
#[case] source_text: &str,
#[case] expected: &str,
) {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("helper.kai"), "echo hello\n").unwrap();
let kernel = Kernel::new(
KernelConfig::repl()
.with_cwd(dir.path().to_path_buf())
.with_trash(false),
)
.expect("failed to create kernel");
let result = kernel.execute(source_text).await.expect("execution failed");
assert_eq!(
result.text_out(),
expected,
"`{source_text}` lost its stdout in the pipe (exit {}, stderr {:?})",
result.code,
result.err
);
}
#[tokio::test]
async fn stdin_is_consumed_once_not_duplicated_by_a_nested_dispatch() {
let kernel = kernel();
let result = kernel
.execute("x=$(head -n 1); y=$(head -n 1); echo \"x=$x y=$y\"")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "x= y=\n");
}
#[tokio::test]
async fn a_nested_dispatch_leaves_the_read_end_for_the_next_statement() {
let kernel = kernel();
let result = kernel
.execute("echo \"pre $(echo z)\"; echo tail | cat")
.await
.expect("execution failed");
assert_eq!(result.text_out(), "pre z\ntail\n");
}
#[tokio::test]
async fn stderr_is_shared_not_owned_so_a_stage_body_still_reports() {
let kernel = kernel();
let result = kernel
.execute("f() { echo e1 >&2; }; f | cat")
.await
.expect("execution failed");
assert!(
result.err.contains("e1"),
"a non-last stage's stderr should reach the caller, got {:?}",
result.err
);
}