use std::path::PathBuf;
use kaish_kernel::{Kernel, KernelConfig};
async fn make_kernel() -> std::sync::Arc<Kernel> {
Kernel::transient().expect("should create kernel").into_arc()
}
fn make_repo_kernel() -> std::sync::Arc<Kernel> {
let config = KernelConfig::repl()
.with_cwd(PathBuf::from(env!("CARGO_MANIFEST_DIR")));
Kernel::new(config).expect("should create kernel").into_arc()
}
#[tokio::test]
async fn validation_blocks_break_outside_loop() {
let kernel = make_kernel().await;
let result = kernel.execute("break").await;
assert!(result.is_err(), "break outside loop should fail validation");
let err = result.unwrap_err().to_string();
assert!(
err.contains("loop") || err.contains("validation"),
"error should mention loop or validation: {}",
err
);
}
#[tokio::test]
async fn validation_blocks_dollar_question_field_access() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"echo "${?.data}""#).await;
assert!(
result.is_err(),
"${{?.data}} should fail validation (use kaish-last instead)"
);
let err = result.unwrap_err().to_string();
assert!(
err.contains("kaish-last"),
"error should suggest kaish-last: {}",
err
);
}
#[tokio::test]
async fn validation_blocks_dollar_question_code_field() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"echo "${?.code}""#).await;
assert!(result.is_err(), "${{?.code}} should fail validation");
}
#[tokio::test]
async fn validation_blocks_continue_outside_loop() {
let kernel = make_kernel().await;
let result = kernel.execute("continue").await;
assert!(result.is_err(), "continue outside loop should fail validation");
}
#[tokio::test]
async fn validation_blocks_return_outside_function() {
let kernel = make_kernel().await;
let result = kernel.execute("return").await;
assert!(result.is_err(), "return outside function should fail validation");
}
#[tokio::test]
async fn validation_blocks_invalid_regex() {
let kernel = make_kernel().await;
let result = kernel.execute("grep '[' /dev/null").await;
assert!(result.is_err(), "invalid regex should fail validation");
let err = result.unwrap_err().to_string();
assert!(
err.contains("regex") || err.contains("validation"),
"error should mention regex or validation: {}",
err
);
}
#[tokio::test]
async fn validation_blocks_seq_zero_increment() {
let kernel = make_kernel().await;
let result = kernel.execute("seq 1 0 10").await;
assert!(result.is_err(), "seq with zero increment should fail validation");
let err = result.unwrap_err().to_string();
assert!(
err.contains("zero") || err.contains("increment") || err.contains("validation"),
"error should mention zero/increment or validation: {}",
err
);
}
#[tokio::test]
async fn validation_blocks_bare_var_in_for_loop() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"
ITEMS="a b c"
for i in $ITEMS; do
echo $i
done
"#).await;
assert!(result.is_err(), "bare variable in for loop should fail validation");
let err = result.unwrap_err().to_string();
assert!(
err.contains("word splitting") || err.contains("iterate once") || err.contains("E012"),
"error should mention word splitting or iterate once: {}",
err
);
}
#[tokio::test]
async fn validation_allows_split_in_for_loop() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"
ITEMS="a b c"
for i in $(split "$ITEMS"); do
echo $i
done
"#).await;
assert!(result.is_ok(), "split in for loop should pass validation");
let exec = result.unwrap();
assert!(exec.ok(), "split in for loop should execute successfully");
assert!(exec.text_out().contains("a") && exec.text_out().contains("b") && exec.text_out().contains("c"));
}
#[tokio::test]
async fn validation_allows_seq_in_for_loop() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"
for i in $(seq 1 3); do
echo $i
done
"#).await;
assert!(result.is_ok(), "seq in for loop should pass validation");
let exec = result.unwrap();
assert!(exec.ok(), "seq in for loop should execute successfully");
}
#[tokio::test]
async fn validation_allows_unknown_command_with_warning() {
let kernel = make_kernel().await;
let result = kernel.execute("nonexistent_command_xyz").await;
match result {
Ok(exec_result) => {
assert!(!exec_result.ok(), "unknown command should fail at runtime");
}
Err(e) => {
let err = e.to_string();
assert!(
!err.contains("validation failed"),
"unknown command should be warning not error: {}",
err
);
}
}
}
#[tokio::test]
async fn validation_allows_undefined_variable_with_warning() {
let kernel = make_kernel().await;
let result = kernel.execute("echo $UNDEFINED_VARIABLE_XYZ").await;
match result {
Ok(_exec_result) => {
}
Err(e) => {
let err = e.to_string();
assert!(
!err.contains("validation failed"),
"undefined variable should be warning: {}",
err
);
}
}
}
#[tokio::test]
async fn skip_validation_allows_break_outside_loop() {
use kaish_kernel::KernelConfig;
let config = KernelConfig::transient().with_skip_validation(true);
let kernel = Kernel::new(config).expect("should create kernel");
let result = kernel.execute("break").await;
match result {
Ok(_) => {
}
Err(e) => {
let err = e.to_string();
assert!(
!err.contains("validation failed"),
"should not fail validation when skipped: {}",
err
);
}
}
}
#[tokio::test]
async fn validation_passes_for_valid_script() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"
x=1
echo $x
"#).await;
assert!(result.is_ok(), "valid script should pass validation");
let exec = result.unwrap();
assert!(exec.ok(), "valid script should execute successfully");
}
#[tokio::test]
async fn validation_passes_for_loop_with_break() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"
for i in 1 2 3; do
if [[ $i == 2 ]]; then
break
fi
echo $i
done
"#).await;
assert!(result.is_ok(), "break inside loop should pass validation");
let exec = result.unwrap();
assert!(exec.ok(), "loop with break should execute successfully");
}
#[tokio::test]
async fn validation_passes_for_valid_grep() {
let kernel = make_kernel().await;
let result = kernel.execute("echo 'hello world' | grep 'hello'").await;
assert!(result.is_ok(), "valid grep should pass validation");
let exec = result.unwrap();
assert!(exec.ok(), "valid grep should execute successfully");
}
#[tokio::test]
async fn validation_passes_for_valid_seq() {
let kernel = make_kernel().await;
let result = kernel.execute("seq 1 2 10").await;
assert!(result.is_ok(), "valid seq should pass validation");
let exec = result.unwrap();
assert!(exec.ok(), "valid seq should execute successfully");
assert!(exec.text_out().contains("1") && exec.text_out().contains("9"));
}
#[tokio::test]
async fn validation_accepts_glob_pattern_ls_star() {
let kernel = make_kernel().await;
let result = kernel.execute("ls *.txt").await;
match result {
Ok(exec) => {
assert!(exec.code == 0 || exec.code == 1);
}
Err(e) => {
let err = e.to_string();
assert!(err.contains("no matches"), "expected no-matches error: {}", err);
}
}
}
#[tokio::test]
async fn validation_allows_glob_pattern_ls_quoted() {
let kernel = make_kernel().await;
let result = kernel.execute("ls \"*.txt\"").await;
assert!(result.is_ok(), "ls should accept glob patterns: {:?}", result.unwrap_err());
}
#[tokio::test]
async fn validation_bare_glob_rm_bak_parses() {
let kernel = make_kernel().await;
let result = kernel.execute("rm *.bak").await;
match result {
Ok(_) => {} Err(e) => assert!(e.to_string().contains("no matches"), "expected no-matches: {}", e),
}
}
#[tokio::test]
async fn validation_bare_glob_question_parses() {
let kernel = make_kernel().await;
let result = kernel.execute("cat file?.log").await;
match result {
Ok(_) => {}
Err(e) => assert!(e.to_string().contains("no matches"), "expected no-matches: {}", e),
}
}
#[tokio::test]
async fn validation_bare_glob_with_path_parses() {
let kernel = make_kernel().await;
let result = kernel.execute("cp src/*.rs dest/").await;
match result {
Ok(_) => {}
Err(e) => assert!(e.to_string().contains("no matches"), "expected no-matches: {}", e),
}
}
#[tokio::test]
async fn validation_bare_glob_bracket_parses() {
let kernel = make_kernel().await;
let result = kernel.execute("echo [abc].txt").await;
match result {
Ok(_) => {}
Err(e) => assert!(e.to_string().contains("no matches"), "expected no-matches: {}", e),
}
}
#[tokio::test]
async fn validation_allows_glob_builtin() {
let kernel = make_kernel().await;
let result = kernel.execute("glob \"*.txt\"").await;
assert!(result.is_ok(), "glob builtin should pass validation");
}
#[tokio::test]
async fn validation_allows_grep_pattern() {
let kernel = make_kernel().await;
let result = kernel.execute("echo 'func_test' | grep 'func.*test'").await;
assert!(result.is_ok(), "grep pattern should pass validation");
let exec = result.unwrap();
assert!(exec.ok(), "grep should execute successfully");
}
#[tokio::test]
async fn validation_allows_find_pattern() {
let kernel = make_repo_kernel();
let result = kernel.execute("find . -name \"*.rs\" -maxdepth 2").await;
assert!(result.is_ok(), "find with pattern should pass validation");
}
#[tokio::test]
async fn validation_allows_quoted_glob_pattern() {
let kernel = make_kernel().await;
let result = kernel.execute("echo \"*.txt\"").await;
assert!(result.is_ok(), "quoted pattern should pass validation");
let exec = result.unwrap();
assert!(exec.ok(), "quoted pattern should execute");
assert!(exec.text_out().contains("*.txt"), "pattern should be literal");
}
#[tokio::test]
async fn validation_allows_correct_glob_usage() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"
for f in $(glob "*.nonexistent"); do
echo $f
done
"#).await;
assert!(result.is_ok(), "correct glob usage should pass validation");
}
#[tokio::test]
async fn validation_allows_glob_double_star_in_cat() {
let kernel = make_kernel().await;
let result = kernel.execute("cat \"**/*.rs\"").await;
assert!(result.is_ok(), "cat should accept glob patterns: {:?}", result.unwrap_err());
}
#[tokio::test]
async fn validation_quoted_glob_in_mv() {
let kernel = make_kernel().await;
let result = kernel.execute("mv \"*.old\" backup/").await;
assert!(result.is_ok(), "quoted glob should parse and validate: {result:?}");
let r = result.expect("checked above");
assert!(!r.ok(), "mv of a nonexistent literal should fail");
assert!(
r.err.contains("*.old"),
"error should name the literal *.old: err={:?}",
r.err
);
}
#[tokio::test]
async fn validation_allows_glob_in_head() {
let kernel = make_kernel().await;
let result = kernel.execute("head \"config*.yaml\"").await;
assert!(result.is_ok(), "head should accept glob patterns: {:?}", result.unwrap_err());
}
#[tokio::test]
async fn validation_allows_glob_in_tail() {
let kernel = make_kernel().await;
let result = kernel.execute("tail \"log?.txt\"").await;
assert!(result.is_ok(), "tail should accept glob patterns: {:?}", result.unwrap_err());
}
#[tokio::test]
async fn validation_allows_glob_character_class_in_cat() {
let kernel = make_kernel().await;
let result = kernel.execute("cat \"file[a-z].txt\"").await;
assert!(result.is_ok(), "cat should accept glob patterns: {:?}", result.unwrap_err());
}
#[tokio::test]
async fn validation_allows_sed_pattern() {
let kernel = make_kernel().await;
let result = kernel.execute("echo 'test' | sed 's/*.txt/replaced/'").await;
assert!(result.is_ok(), "sed pattern should pass validation");
}
#[tokio::test]
async fn validation_allows_awk_pattern() {
let kernel = make_kernel().await;
let result = kernel.execute("echo 'test' | awk '/.*\\.txt/ {print}'").await;
assert!(result.is_ok(), "awk pattern should pass validation");
}
#[tokio::test]
async fn validation_allows_jq_pattern() {
let kernel = make_kernel().await;
let result = kernel.execute("echo '{}' | jq '.files[].name'").await;
assert!(result.is_ok(), "jq filter should pass validation");
}
#[tokio::test]
async fn validation_allows_glob_in_pipeline_first() {
let kernel = make_kernel().await;
let result = kernel.execute("cat \"*.log\" | grep error").await;
assert!(result.is_ok(), "cat with glob in pipeline should pass validation: {:?}", result.unwrap_err());
}
#[tokio::test]
async fn validation_allows_glob_in_pipeline_grep() {
let kernel = make_kernel().await;
let result = kernel.execute("echo 'test.txt' | grep '.*\\.txt'").await;
assert!(result.is_ok(), "grep pattern in pipeline should pass");
}
#[tokio::test]
async fn validation_allows_glob_with_path_prefix_in_ls() {
let kernel = make_kernel().await;
let result = kernel.execute("ls \"/tmp/*.log\"").await;
assert!(result.is_ok(), "ls should accept glob patterns: {:?}", result.unwrap_err());
}
#[tokio::test]
async fn validation_allows_literal_asterisk_filename() {
let kernel = make_kernel().await;
let result = kernel.execute("cat \"notes\"").await;
assert!(result.is_ok(), "literal filename should pass validation");
}
#[tokio::test]
async fn validation_allows_printf_pattern() {
let kernel = make_kernel().await;
let result = kernel.execute("printf '%s\\n' \"*.txt\"").await;
assert!(result.is_ok(), "printf with pattern should pass validation");
}
#[tokio::test]
async fn validation_blocks_scatter_without_gather() {
let kernel = make_kernel().await;
let result = kernel.execute("seq 1 3 | scatter | echo hi").await;
assert!(result.is_err(), "scatter without gather should fail validation");
let err = result.unwrap_err().to_string();
assert!(
err.contains("gather") || err.contains("E014"),
"error should mention gather or E014: {}",
err
);
}
#[tokio::test]
async fn validation_allows_scatter_with_gather() {
let kernel = make_kernel().await;
let result = kernel.execute("seq 1 3 | scatter | echo \"hi\" | gather").await;
assert!(result.is_ok(), "scatter with gather should pass validation: {:?}", result.err());
}
#[tokio::test]
async fn scatter_seq_structured_data() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"seq 1 3 | scatter | echo "$ITEM" | gather"#).await;
assert!(result.is_ok(), "seq | scatter | gather should pass: {:?}", result.err());
let exec = result.unwrap();
assert!(exec.ok(), "pipeline should succeed: {}", exec.err);
assert!(exec.text_out().contains("1"), "should contain 1: {}", exec.text_out());
assert!(exec.text_out().contains("2"), "should contain 2: {}", exec.text_out());
assert!(exec.text_out().contains("3"), "should contain 3: {}", exec.text_out());
}
#[tokio::test]
async fn scatter_split_structured_data() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"split "a,b,c" "," | scatter --as X | echo "got $X" | gather"#).await;
assert!(result.is_ok(), "split | scatter | gather should pass: {:?}", result.err());
let exec = result.unwrap();
assert!(exec.ok(), "pipeline should succeed: {}", exec.err);
assert!(exec.text_out().contains("got a"), "should contain 'got a': {}", exec.text_out());
assert!(exec.text_out().contains("got b"), "should contain 'got b': {}", exec.text_out());
assert!(exec.text_out().contains("got c"), "should contain 'got c': {}", exec.text_out());
}
#[tokio::test]
async fn scatter_split_stdin_pipe() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"echo "x,y,z" | split "," | scatter --as V | echo "got $V" | gather"#).await;
assert!(result.is_ok(), "echo | split | scatter should pass: {:?}", result.err());
let exec = result.unwrap();
assert!(exec.ok(), "pipeline should succeed: {}", exec.err);
assert!(exec.text_out().contains("got x"), "should contain 'got x': {}", exec.text_out());
assert!(exec.text_out().contains("got y"), "should contain 'got y': {}", exec.text_out());
assert!(exec.text_out().contains("got z"), "should contain 'got z': {}", exec.text_out());
}
#[tokio::test]
async fn scatter_single_item() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"echo "hello" | scatter | echo "$ITEM" | gather"#).await;
assert!(result.is_ok(), "single item scatter should pass: {:?}", result.err());
let exec = result.unwrap();
assert!(exec.ok(), "pipeline should succeed: {}", exec.err);
assert!(exec.text_out().contains("hello"), "should contain 'hello': {}", exec.text_out());
}
#[tokio::test]
async fn scatter_empty_input() {
let kernel = make_kernel().await;
let result = kernel.execute(r#"split "" "," | scatter | echo "$ITEM" | gather"#).await;
assert!(result.is_ok(), "empty scatter should pass: {:?}", result.err());
let exec = result.unwrap();
assert!(exec.ok(), "pipeline should succeed: {}", exec.err);
}
#[tokio::test]
async fn validation_issue_in_heredoc_body_carries_span() {
use std::collections::HashMap;
use kaish_kernel::parser::parse;
use kaish_kernel::tools::{register_builtins, ToolRegistry};
use kaish_kernel::validator::Validator;
let source = "cat <<EOF\n${UNDEFINED_VAR}\nEOF";
let program = parse(source).expect("source parses");
let mut registry = ToolRegistry::new();
register_builtins(&mut registry);
let user_tools = HashMap::new();
let validator = Validator::new(®istry, &user_tools);
let issues = validator.validate(&program);
let undef = issues
.iter()
.find(|i| i.message.contains("UNDEFINED_VAR"))
.expect("should warn about UNDEFINED_VAR");
let span = undef.span.expect("span must be populated for body-internal issue");
let (line, col) = span.to_line_col(source);
assert_eq!(line, 2, "body-internal issue should report line 2");
assert_eq!(col, 1, "issue should start at column 1 of body line");
let rendered = undef.format(source);
assert!(rendered.starts_with("2:1"), "rendered should start with line:col, got: {rendered}");
assert!(
rendered.contains("UNDEFINED_VAR"),
"rendered should mention the variable: {rendered}",
);
assert!(
rendered.contains("${UNDEFINED_VAR}"),
"rendered should include the source-line caret showing the offending line: {rendered}",
);
}
#[tokio::test]
async fn validation_issue_in_double_quoted_string_still_works() {
use std::collections::HashMap;
use kaish_kernel::parser::parse;
use kaish_kernel::tools::{register_builtins, ToolRegistry};
use kaish_kernel::validator::Validator;
let source = r#"echo "value is ${UNDEFINED_VAR_TWO}""#;
let program = parse(source).expect("source parses");
let mut registry = ToolRegistry::new();
register_builtins(&mut registry);
let user_tools = HashMap::new();
let validator = Validator::new(®istry, &user_tools);
let issues = validator.validate(&program);
let undef = issues
.iter()
.find(|i| i.message.contains("UNDEFINED_VAR_TWO"))
.expect("should still warn about double-quoted-string undefs");
assert!(
undef.span.is_none(),
"double-quoted strings remain spanless until follow-up refactor",
);
}
#[tokio::test]
async fn validation_issue_in_heredoc_body_full_rendering_snapshot() {
use std::collections::HashMap;
use kaish_kernel::parser::parse;
use kaish_kernel::tools::{register_builtins, ToolRegistry};
use kaish_kernel::validator::Validator;
let source = "cat <<EOF\n${STILL_UNDEFINED}\nEOF";
let program = parse(source).expect("source parses");
let mut registry = ToolRegistry::new();
register_builtins(&mut registry);
let user_tools = HashMap::new();
let validator = Validator::new(®istry, &user_tools);
let issues = validator.validate(&program);
let undef = issues
.iter()
.find(|i| i.message.contains("STILL_UNDEFINED"))
.expect("expected undefined-variable warning");
insta::assert_snapshot!(undef.format(source));
}