use kaish_kernel::Kernel;
use std::io::Write;
#[tokio::test]
async fn for_subst_printf_multiline_iterates_per_line() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for line in $(printf 'a\nb\nc\n'); do
N=$((N + 1))
echo "got=$line"
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("got=a"), "missing 'got=a' in:\n{text}");
assert!(text.contains("got=b"), "missing 'got=b' in:\n{text}");
assert!(text.contains("got=c"), "missing 'got=c' in:\n{text}");
assert!(text.contains("count=3"), "expected 3 iterations:\n{text}");
}
#[tokio::test]
async fn for_subst_cat_file_iterates_per_line() {
let kernel = Kernel::transient().unwrap();
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp, "alpha").unwrap();
writeln!(tmp, "beta").unwrap();
writeln!(tmp, "gamma").unwrap();
tmp.flush().unwrap();
let path = tmp.path().display();
let script = format!(
r#"
N=0
for word in $(cat {path}); do
N=$((N + 1))
echo "w=$word"
done
echo "count=$N"
"#
);
let result = kernel.execute(&script).await.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("w=alpha"), "missing 'w=alpha':\n{text}");
assert!(text.contains("w=beta"), "missing 'w=beta':\n{text}");
assert!(text.contains("w=gamma"), "missing 'w=gamma':\n{text}");
assert!(text.contains("count=3"), "expected 3 iterations:\n{text}");
}
#[tokio::test]
async fn for_subst_echo_with_spaces_iterates_once() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for x in $(echo "a b c"); do
N=$((N + 1))
echo "got=[$x]"
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("got=[a b c]"), "should preserve whole string:\n{text}");
assert!(text.contains("count=1"), "expected 1 iteration, got:\n{text}");
}
#[tokio::test]
async fn for_subst_single_line_no_newline_iterates_once() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for x in $(printf 'lonely'); do
N=$((N + 1))
echo "got=[$x]"
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("got=[lonely]"), "missing 'got=[lonely]':\n{text}");
assert!(text.contains("count=1"), "expected 1 iteration:\n{text}");
}
#[tokio::test]
async fn for_subst_trailing_newline_does_not_create_phantom_item() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for x in $(printf 'a\nb\n'); do
N=$((N + 1))
echo "got=[$x]"
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("got=[a]"), "missing 'got=[a]':\n{text}");
assert!(text.contains("got=[b]"), "missing 'got=[b]':\n{text}");
assert!(text.contains("count=2"), "expected 2 iterations, got:\n{text}");
assert!(!text.contains("got=[]"), "should not have a phantom empty item:\n{text}");
}
#[tokio::test]
async fn for_subst_interior_empty_line_preserved() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for x in $(printf 'a\n\nb\n'); do
N=$((N + 1))
echo "got=[$x]"
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("got=[a]"), "missing 'got=[a]':\n{text}");
assert!(text.contains("got=[]"), "missing empty-line iteration:\n{text}");
assert!(text.contains("got=[b]"), "missing 'got=[b]':\n{text}");
assert!(text.contains("count=3"), "expected 3 iterations:\n{text}");
}
#[tokio::test]
async fn for_subst_empty_stdout_zero_iterations() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for x in $(printf ''); do
N=$((N + 1))
echo "should-not-print"
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("count=0"), "expected 0 iterations:\n{text}");
assert!(!text.contains("should-not-print"), "body should not run:\n{text}");
}
#[tokio::test]
async fn for_subst_seq_still_uses_data_array() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for i in $(seq 1 3); do
N=$((N + 1))
echo "i=$i"
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("i=1"), "missing i=1:\n{text}");
assert!(text.contains("i=2"), "missing i=2:\n{text}");
assert!(text.contains("i=3"), "missing i=3:\n{text}");
assert!(text.contains("count=3"), "expected 3 iterations:\n{text}");
}
#[tokio::test]
async fn for_subst_jq_extract_still_uses_data_array() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for name in $(echo '["alice","bob","carol"]' | jq -r '.[]'); do
N=$((N + 1))
echo "hello-$name"
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("hello-alice"), "missing alice:\n{text}");
assert!(text.contains("hello-bob"), "missing bob:\n{text}");
assert!(text.contains("hello-carol"), "missing carol:\n{text}");
assert!(text.contains("count=3"), "expected 3 iterations:\n{text}");
}
#[tokio::test]
async fn for_subst_quoted_iterates_once_with_embedded_newlines() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for x in "$(printf 'a\nb\nc')"; do
N=$((N + 1))
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("count=1"), "quoted subst should iterate once:\n{text}");
}
#[tokio::test]
async fn assignment_does_not_split_multiline_subst() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
R=$(printf 'a\nb\nc')
echo "len=${#R}"
echo "value=[$R]"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("len=5"), "assignment should preserve full string:\n{text}");
assert!(text.contains("value=[a\nb\nc]"), "newlines should be preserved:\n{text}");
}
#[tokio::test]
async fn string_interpolation_does_not_split_multiline_subst() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
echo "before|$(printf 'a\nb')|after"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("before|a\nb|after"), "interpolation should preserve newlines:\n{text}");
}
#[tokio::test]
async fn argv_does_not_split_multiline_subst() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
echo "[" $(printf 'a\nb') "]"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("a\nb"), "argv should pass multi-line string:\n{text}");
}
#[tokio::test]
async fn for_subst_crlf_trims_carriage_returns() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for x in $(printf 'a\r\nb\r\nc\r\n'); do
N=$((N + 1))
echo "len=${#x}"
echo "got=[$x]"
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("count=3"), "expected 3 iterations:\n{text}");
assert!(text.contains("len=1"), "each line should be 1 char (no \\r):\n{text}");
assert!(!text.contains("len=2"), "stray \\r leaked into a line:\n{text}");
}
#[tokio::test]
async fn regression_for_bare_var_still_validator_error() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
ITEMS="a b c"
for i in $ITEMS; do echo $i; done
"#,
)
.await;
assert!(result.is_err(), "bare $VAR in for should still error");
let err = result.unwrap_err().to_string();
assert!(
err.contains("word splitting") || err.contains("iterate once") || err.contains("E012"),
"error should still mention E012/word splitting: {err}"
);
}
#[tokio::test]
async fn regression_while_condition_unchanged() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
while [[ $N -lt 3 ]]; do
N=$((N + 1))
echo "n=$N"
done
"#,
)
.await
.unwrap();
assert!(result.ok(), "while loop should run: err={}", result.err);
let text = result.text_out();
assert!(text.contains("n=1"), "missing n=1:\n{text}");
assert!(text.contains("n=2"), "missing n=2:\n{text}");
assert!(text.contains("n=3"), "missing n=3:\n{text}");
}
#[tokio::test]
async fn regression_explicit_split_still_works() {
let kernel = Kernel::transient().unwrap();
let result = kernel
.execute(
r#"
N=0
for x in $(split "alpha beta gamma"); do
N=$((N + 1))
echo "x=$x"
done
echo "count=$N"
"#,
)
.await
.unwrap();
assert!(result.ok(), "script should succeed: err={}", result.err);
let text = result.text_out();
assert!(text.contains("x=alpha"), "missing alpha:\n{text}");
assert!(text.contains("x=beta"), "missing beta:\n{text}");
assert!(text.contains("x=gamma"), "missing gamma:\n{text}");
assert!(text.contains("count=3"), "split should still produce 3:\n{text}");
}