#![allow(clippy::unwrap_used, clippy::expect_used)]
#![cfg(feature = "localfs")]
use std::path::Path;
use kaish_kernel::ast::Value;
use kaish_kernel::interpreter::ExecResult;
use kaish_kernel::{Kernel, KernelConfig};
fn tempdir() -> tempfile::TempDir {
tempfile::Builder::new()
.prefix("execute-argv-")
.tempdir_in(env!("CARGO_TARGET_TMPDIR"))
.expect("tempdir under CARGO_TARGET_TMPDIR")
}
fn kernel_at(dir: &Path) -> Kernel {
let config = KernelConfig::repl()
.with_cwd(dir.to_path_buf())
.with_latch(false)
.with_trash(false);
Kernel::new(config).expect("kernel")
}
fn s(text: &str) -> Value {
Value::String(text.to_string())
}
fn observe(r: &ExecResult) -> (String, i64) {
(r.text_out().trim().to_string(), r.code)
}
#[tokio::test]
async fn argv_door_matches_string_door() {
let dir = tempdir();
let kernel = kernel_at(dir.path());
let cases: &[(&str, Vec<Value>, &str)] = &[
("echo", vec![s("hello"), s("world")], "echo hello world"),
("echo", vec![s("-n"), s("hi")], "echo -n hi"),
("echo", vec![s("--"), s("-n")], "echo -- -n"),
("echo", vec![s("a b c")], "echo 'a b c'"),
("echo", vec![s("A=1")], "echo A=1"),
("true", vec![], "true"),
("false", vec![], "false"),
];
for (name, argv, string) in cases {
let via_argv = kernel.execute_argv(name, argv).await.expect("execute_argv");
let via_string = kernel.execute(string).await.expect("execute");
assert_eq!(
observe(&via_argv),
observe(&via_string),
"doors diverged for `{string}` (argv {argv:?})"
);
}
}
#[tokio::test]
async fn argv_tokens_are_literal_not_globbed() {
let dir = tempdir();
std::fs::write(dir.path().join("a.txt"), "a").unwrap();
std::fs::write(dir.path().join("b.txt"), "b").unwrap();
let kernel = kernel_at(dir.path());
let (globbed, code) = observe(&kernel.execute("echo *.txt").await.unwrap());
assert_eq!(code, 0);
assert_eq!(globbed, "a.txt b.txt", "string door should glob");
let (literal, code) = observe(&kernel.execute_argv("echo", &[s("*.txt")]).await.unwrap());
assert_eq!(code, 0);
assert_eq!(literal, "*.txt", "argv token must stay literal, not glob");
}
#[tokio::test]
async fn argv_tokens_do_not_interpolate_variables() {
let kernel = kernel_at(tempdir().path());
kernel.execute("HOME_LIKE=secret").await.unwrap();
let (out, code) = observe(&kernel.execute_argv("echo", &[s("$HOME_LIKE")]).await.unwrap());
assert_eq!(code, 0);
assert_eq!(out, "$HOME_LIKE", "argv token must not interpolate");
}
#[tokio::test]
async fn tilde_expands_consistently_with_the_string_door() {
let kernel = kernel_at(tempdir().path());
kernel.execute("HOME=/home/agent").await.unwrap();
let via_argv = observe(&kernel.execute_argv("echo", &[s("~/work")]).await.unwrap());
let via_string = observe(&kernel.execute("echo ~/work").await.unwrap());
assert_eq!(via_argv, via_string);
assert_eq!(via_argv.0, "/home/agent/work", "tilde should expand via the shared binder");
}
#[tokio::test]
async fn typed_values_ride_through_as_positionals() {
let kernel = kernel_at(tempdir().path());
let (out, code) = observe(&kernel.execute_argv("echo", &[Value::Int(-9)]).await.unwrap());
assert_eq!(code, 0);
assert_eq!(out, "-9", "Int rides through as a positional value");
let json = Value::Json(serde_json::json!([1, 2, 3]));
let (out, code) = observe(&kernel.execute_argv("echo", &[json]).await.unwrap());
assert_eq!(code, 0);
assert!(out.contains('1') && out.contains('3'), "JSON positional reached echo: {out:?}");
}
#[tokio::test]
async fn json_output_transform_applies_via_argv() {
let dir = tempdir();
std::fs::write(dir.path().join("only.txt"), "x").unwrap();
let kernel = kernel_at(dir.path());
let via_argv = kernel.execute_argv("ls", &[s("--json")]).await.unwrap();
let via_string = kernel.execute("ls --json").await.unwrap();
assert_eq!(observe(&via_argv), observe(&via_string));
assert!(
via_argv.text_out().contains("only.txt"),
"ls --json via argv should list the file: {}",
via_argv.text_out()
);
}
#[tokio::test]
async fn request_timeout_interrupts_a_hung_command() {
use std::time::Duration;
let config = KernelConfig::repl()
.with_cwd(tempdir().path().to_path_buf())
.with_latch(false)
.with_trash(false)
.with_request_timeout(Duration::from_millis(200));
let kernel = Kernel::new(config).expect("kernel");
let r = tokio::time::timeout(Duration::from_secs(10), kernel.execute_argv("sleep", &[s("5")]))
.await
.expect("execute_argv must return well before 10s (watchdog should fire at 200ms)")
.expect("execute_argv");
assert_eq!(r.code, 124, "request_timeout should yield exit 124, err: {}", r.err);
}
#[tokio::test]
async fn unknown_command_is_127() {
let kernel = kernel_at(tempdir().path());
let r = kernel
.execute_argv("definitely-not-a-real-command-xyz", &[])
.await
.unwrap();
assert_eq!(r.code, 127, "unknown command should be 127, err: {}", r.err);
}
#[tokio::test]
async fn latch_round_trips_through_argv_door() {
let dir = tempdir();
std::fs::write(dir.path().join("precious.txt"), "data").unwrap();
let kernel = kernel_at(dir.path());
let enabled = kernel.execute_argv("set", &[s("-o"), s("latch")]).await.unwrap();
assert_eq!(enabled.code, 0, "set -o latch failed: {}", enabled.err);
let gated = kernel.execute_argv("rm", &[s("precious.txt")]).await.unwrap();
assert_eq!(gated.code, 2, "expected latch exit 2, err: {}", gated.err);
assert!(
dir.path().join("precious.txt").exists(),
"file must survive the latch gate"
);
let req = gated
.latch_request()
.expect("a gated rm result decodes to a LatchRequest");
assert_eq!(req.command, "rm");
assert_eq!(req.paths, vec!["precious.txt".to_string()]);
assert_eq!(req.ttl, 60);
let nonce = req.nonce;
let confirmed = kernel
.execute_argv("rm", &[s(&format!("--confirm={nonce}")), s("precious.txt")])
.await
.unwrap();
assert_eq!(confirmed.code, 0, "confirm failed: {}", confirmed.err);
assert!(
!dir.path().join("precious.txt").exists(),
"file should be deleted after argv confirmation"
);
}