#![allow(clippy::unwrap_used, clippy::expect_used)]
#![cfg(feature = "localfs")]
mod common;
use std::fs;
use std::path::Path;
use common::kernel_at;
use tempfile::tempdir;
fn newline_fixture() -> (tempfile::TempDir, String) {
let dir = tempdir().expect("tempdir");
fs::write(dir.path().join("normal.txt"), b"a").expect("write normal.txt");
fs::write(dir.path().join("we\nird.txt"), b"b").expect("write newline-named file");
let path = dir.path().display().to_string();
(dir, path)
}
#[tokio::test]
async fn ls_refuses_newline_named_file_as_text() {
let (_dir, path) = newline_fixture();
let kernel = kernel_at(Path::new(&path));
let result = kernel.execute("ls .").await.expect("execute");
assert_eq!(result.code, 2, "ls should refuse loudly: out={:?} err={:?}", result.text_out(), result.err);
assert!(result.err.contains("we\\nird.txt"), "error must name the escaped path: {:?}", result.err);
assert!(result.err.contains("ls --json"), "error must point at --json: {:?}", result.err);
assert!(result.err.starts_with("ls:"), "error must be ls-prefixed: {:?}", result.err);
}
#[tokio::test]
async fn find_refuses_newline_named_file_as_text() {
let (_dir, path) = newline_fixture();
let kernel = kernel_at(Path::new(&path));
let result = kernel.execute("find . -name '*.txt'").await.expect("execute");
assert_eq!(result.code, 2, "find should refuse loudly: out={:?} err={:?}", result.text_out(), result.err);
assert!(result.err.contains("we\\nird.txt"), "error must name the escaped path: {:?}", result.err);
assert!(result.err.contains("find --json"), "error must point at --json: {:?}", result.err);
assert!(result.err.starts_with("find:"), "error must be find-prefixed: {:?}", result.err);
}
#[tokio::test]
async fn glob_refuses_newline_named_file_as_text() {
let (_dir, path) = newline_fixture();
let kernel = kernel_at(Path::new(&path));
let result = kernel.execute("glob '*.txt'").await.expect("execute");
assert_eq!(result.code, 2, "glob should refuse loudly: out={:?} err={:?}", result.text_out(), result.err);
assert!(result.err.contains("we\\nird.txt"), "error must name the escaped path: {:?}", result.err);
assert!(result.err.contains("glob --json"), "error must point at --json: {:?}", result.err);
assert!(result.err.starts_with("glob:"), "error must be glob-prefixed: {:?}", result.err);
}
#[tokio::test]
async fn ls_json_succeeds_on_newline_named_file() {
let (_dir, path) = newline_fixture();
let kernel = kernel_at(Path::new(&path));
let result = kernel.execute("ls . --json").await.expect("execute");
assert!(result.ok(), "ls --json must still succeed: err={:?}", result.err);
let parsed: serde_json::Value =
serde_json::from_str(&result.text_out()).expect("ls --json must be valid JSON");
let names: Vec<&str> = parsed.as_array().expect("array").iter()
.map(|v| v.as_str().expect("string entry"))
.collect();
assert!(names.contains(&"we\nird.txt"), "the real newline-bearing name must survive intact: {names:?}");
assert!(names.contains(&"normal.txt"), "the ordinary name must still be present: {names:?}");
}
#[tokio::test]
async fn find_json_succeeds_on_newline_named_file() {
let (_dir, path) = newline_fixture();
let kernel = kernel_at(Path::new(&path));
let result = kernel.execute("find . -name '*.txt' --json").await.expect("execute");
assert!(result.ok(), "find --json must still succeed: err={:?}", result.err);
let parsed: serde_json::Value =
serde_json::from_str(&result.text_out()).expect("find --json must be valid JSON");
let names: Vec<&str> = parsed.as_array().expect("array").iter()
.map(|v| v.as_str().expect("string entry"))
.collect();
assert!(names.iter().any(|n| n.contains("we\nird.txt")), "newline-bearing name must survive: {names:?}");
}
#[tokio::test]
async fn glob_json_succeeds_on_newline_named_file() {
let (_dir, path) = newline_fixture();
let kernel = kernel_at(Path::new(&path));
let result = kernel.execute("glob '*.txt' --json").await.expect("execute");
assert!(result.ok(), "glob --json must still succeed: err={:?}", result.err);
let parsed: serde_json::Value =
serde_json::from_str(&result.text_out()).expect("glob --json must be valid JSON");
let names: Vec<&str> = parsed.as_array().expect("array").iter()
.map(|v| v.as_str().expect("string entry"))
.collect();
assert!(names.iter().any(|n| n.contains("we\nird.txt")), "newline-bearing name must survive: {names:?}");
}
#[tokio::test]
async fn for_over_find_iterates_once_per_clean_path() {
let dir = tempdir().expect("tempdir");
fs::write(dir.path().join("a.txt"), b"a").expect("write a.txt");
fs::write(dir.path().join("b.txt"), b"b").expect("write b.txt");
let kernel = kernel_at(dir.path());
let result = kernel
.execute("n=0\nfor f in $(find . -name '*.txt'); do n=$((n+1)); done\necho $n")
.await
.expect("execute");
assert!(result.ok(), "find for-loop should succeed: {}", result.err);
assert_eq!(result.text_out().trim(), "2", "should iterate exactly 2 paths, got: {}", result.text_out());
}
#[tokio::test]
async fn for_over_glob_iterates_once_per_clean_path() {
let dir = tempdir().expect("tempdir");
fs::write(dir.path().join("a.txt"), b"a").expect("write a.txt");
fs::write(dir.path().join("b.txt"), b"b").expect("write b.txt");
let kernel = kernel_at(dir.path());
let result = kernel
.execute("n=0\nfor f in $(glob '*.txt'); do n=$((n+1)); done\necho $n")
.await
.expect("execute");
assert!(result.ok(), "glob for-loop should succeed: {}", result.err);
assert_eq!(result.text_out().trim(), "2", "should iterate exactly 2 paths, got: {}", result.text_out());
}
#[tokio::test]
async fn for_over_ls_on_newline_dir_refuses_instead_of_miscounting() {
let (_dir, path) = newline_fixture();
let kernel = kernel_at(Path::new(&path));
let result = kernel
.execute("n=0\nfor f in $(ls .); do n=$((n+1)); done\necho $n")
.await
.expect("execute");
assert_eq!(result.text_out().trim(), "0", "must never reach the miscounted 3: {}", result.text_out());
assert!(result.err.contains("we\\nird.txt"), "the refusal must still reach stderr: {:?}", result.err);
}
#[tokio::test]
async fn fromjson_command_subst_index_stays_typed() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let result = kernel
.execute("xs=$(fromjson \"[10,20]\")\necho ${xs[0]}")
.await
.expect("execute");
assert!(result.ok(), "fromjson round-trip should succeed: {}", result.err);
assert_eq!(result.text_out().trim(), "10", "fromjson must stay typed through $(...): {}", result.text_out());
}
#[tokio::test]
async fn keys_command_subst_iterates_typed() {
let kernel = kaish_kernel::Kernel::transient().expect("kernel");
let result = kernel
.execute(
"r=$(fromjson '{\"a\":1,\"b\":2}')\nn=0\nfor k in $(keys $r); do n=$((n+1)); done\necho $n",
)
.await
.expect("execute");
assert!(result.ok(), "keys iteration should succeed: {}", result.err);
assert_eq!(result.text_out().trim(), "2", "keys must stay typed: {}", result.text_out());
}