#![allow(clippy::unwrap_used, clippy::expect_used)]
use kaish_kernel::{Kernel, KernelConfig};
fn kernel() -> Kernel {
Kernel::new(KernelConfig::isolated()).expect("kernel")
}
async fn run(source: &str) -> (i64, String, String) {
let k = kernel();
let r = k.execute(source).await.expect("kernel execute");
(r.code, r.text_out().into_owned(), r.err.clone())
}
async fn plan_json(source: &str) -> serde_json::Value {
let (code, out, err) = run(source).await;
assert_eq!(code, 0, "plan should succeed: {err:?}");
serde_json::from_str(&out).unwrap_or_else(|e| panic!("plan --json must emit JSON: {e}: {out:?}"))
}
#[tokio::test]
async fn plan_emits_the_statements_projection() {
let doc = plan_json(r#"plan 'echo one; echo two' --json"#).await;
let statements = doc["statements"].as_array().expect("statements array");
assert_eq!(statements.len(), 2, "two top-level statements: {doc}");
assert_eq!(statements[0]["index"], 0);
assert_eq!(statements[0]["plan"]["rendered"], "echo one");
assert_eq!(statements[1]["plan"]["rendered"], "echo two");
}
#[tokio::test]
async fn commands_descend_into_a_loop_body() {
let doc = plan_json(r#"plan 'for f in a b; do shred $f; done | wc -l' --json"#).await;
let names: Vec<&str> = doc["statements"][0]["plan"]["commands"]
.as_array()
.expect("commands")
.iter()
.map(|c| c["name"].as_str().unwrap_or_default())
.collect();
assert!(
names.contains(&"shred"),
"a command inside the loop body must surface on its own: {names:?}"
);
assert!(names.contains(&"wc"), "and so must the later pipeline stage: {names:?}");
}
#[tokio::test]
async fn plan_does_not_execute_what_it_plans() {
let (_, control, err) = run(
r#"touch /v/control.txt; if [[ -e /v/control.txt ]]; then echo CREATED; else echo NO; fi"#,
)
.await;
assert_eq!(
control.trim(),
"CREATED",
"control: touch must work at this path, or the assertion below is vacuous: {err:?}"
);
let (code, out, _) = run(
r#"plan 'touch /v/planned.txt' > /dev/null; if [[ -e /v/planned.txt ]]; then echo RAN; else echo INERT; fi"#,
)
.await;
assert_eq!(code, 0);
assert_eq!(out.trim(), "INERT", "plan must not execute its argument");
}
#[tokio::test]
async fn a_parse_error_exits_2_and_explains() {
let (code, _, err) = run(r#"plan 'for f in'"#).await;
assert_eq!(code, 2, "parse failure is the usage code");
assert!(!err.is_empty(), "a parse failure must say what was wrong");
}
#[tokio::test]
async fn plan_reads_stdin_when_given_no_argument() {
let doc = plan_json(r#"echo 'echo hi' | plan --json"#).await;
assert_eq!(doc["statements"][0]["plan"]["rendered"], "echo hi");
}
#[tokio::test]
async fn plan_reports_the_variables_a_statement_reads_and_writes() {
let doc = plan_json(r#"plan 'd=/tmp; echo "$d"' --json"#).await;
let bound = doc["statements"][0]["plan"]["bound_variables"]
.as_array()
.expect("bound_variables");
assert!(bound.iter().any(|v| v == "d"), "assignment binds d: {doc}");
let free = doc["statements"][1]["plan"]["free_variables"]
.as_array()
.expect("free_variables");
assert!(free.iter().any(|v| v == "d"), "the echo reads d: {doc}");
}
#[tokio::test]
async fn indexes_match_list_position_even_with_a_leading_comment() {
let doc = plan_json("plan '# lead\necho a\necho b' --json").await;
let statements = doc["statements"].as_array().expect("statements");
assert_eq!(statements.len(), 2, "the comment is not a statement: {doc}");
for (position, statement) in statements.iter().enumerate() {
assert_eq!(
statement["index"].as_u64(),
Some(position as u64),
"index must equal list position: {doc}"
);
}
}
#[tokio::test]
async fn more_than_one_word_is_refused_rather_than_truncated() {
let (code, out, err) = run("plan rm build").await;
assert_eq!(code, 2, "extra words are a usage error, not a shorter plan");
assert!(
!out.contains("\"rm\"") && !out.contains("rendered"),
"no plan may be emitted for a statement that was not fully given: {out:?}"
);
assert!(
err.contains("quote the whole statement"),
"the error must name the fix: {err:?}"
);
let doc = plan_json("plan 'rm build' --json").await;
let args = doc["statements"][0]["plan"]["commands"][0]["args"]
.as_array()
.expect("args");
assert_eq!(args.len(), 1, "the argument survives when quoted: {doc}");
}
#[tokio::test]
async fn an_empty_plan_stays_empty_under_the_kernel_json_rule() {
let (code, out, _) = run("plan '' --json").await;
assert_eq!(code, 0, "an empty statement is not an error");
assert_eq!(out, "", "an empty success stays empty under --json");
}