//! End-to-end coverage for `exec`'s command-line handling against the real sidecar.//!//! `exec` takes a command *line*, not a pre-split argv. The client mirrors the sidecar's//! child_process shell decision: shell-free commands spawn directly (preserving the command's real//! exit code), while shell syntax or a POSIX builtin head runs under `sh -c <line>`. These tests//! prove both paths produce truthful results inside the VM.//!//! Self-gating: skips when the sidecar binary or coreutils wasm artifacts are absent.mod common;use agentos_client::ExecOptions;#[tokio::test]async fn exec_command_line_paths() { if !common::sidecar_available() { eprintln!("skipping exec_command_line_paths: sidecar binary not built"); return; } let Some(os) = common::new_vm_with_commands().await else { eprintln!("skipping exec_command_line_paths: coreutils wasm artifacts absent"); return; }; // Direct path: a simple command with arguments runs and returns its output. let echo = os .exec("echo hello world", ExecOptions::default()) .await .expect("exec echo"); assert_eq!(echo.exit_code, 0, "echo exit (stderr: {:?})", echo.stderr); assert_eq!(echo.stdout.trim_end(), "hello world", "echo stdout"); // Direct path preserves a real non-zero exit code (the `sh -c` wrapper can swallow it). let missing = os .exec("cat /no/such/file", ExecOptions::default()) .await .expect("exec cat missing returns a result"); assert_ne!( missing.exit_code, 0, "cat of a missing file must report a non-zero exit code" ); // Shell path: a `&&` chain runs as a single `sh -c` execution. let chain = os .exec("echo a && echo b", ExecOptions::default()) .await .expect("exec && chain"); assert_eq!( chain.exit_code, 0, "chain exit (stderr: {:?})", chain.stderr ); assert_eq!(chain.stdout, "a\nb\n", "chain stdout"); // Shell path: a redirect writes a file the VM can read back. let redirect = os .exec( "echo redirected > /tmp/exec_redirect.txt", ExecOptions::default(), ) .await .expect("exec redirect"); assert_eq!( redirect.exit_code, 0, "redirect exit (stderr: {:?})", redirect.stderr ); let body = os .read_file("/tmp/exec_redirect.txt") .await .expect("read redirected file"); assert_eq!( String::from_utf8_lossy(&body).trim_end(), "redirected", "redirected file contents" ); // Shell path: a quoted argument with a space stays one token through `sh -c`. let quoted = os .exec("echo 'a b'", ExecOptions::default()) .await .expect("exec quoted"); assert_eq!(quoted.stdout.trim_end(), "a b", "quoted stdout"); // An empty command line is an explicit error, not a silent no-op. assert!( os.exec(" ", ExecOptions::default()).await.is_err(), "empty command line must error" ); os.shutdown().await.expect("shutdown");}
//! End-to-end coverage for `exec`'s command-line handling against the real sidecar.
//!
//! `exec` takes a command *line*, not a pre-split argv. The client mirrors the sidecar's
//! child_process shell decision: shell-free commands spawn directly (preserving the command's real
//! exit code), while shell syntax or a POSIX builtin head runs under `sh -c <line>`. These tests
//! prove both paths produce truthful results inside the VM.
//!
//! Self-gating: skips when the sidecar binary or coreutils wasm artifacts are absent.
mod common;
use agentos_client::ExecOptions;
#[tokio::test]
async fn exec_command_line_paths() {
if !common::sidecar_available() {
eprintln!("skipping exec_command_line_paths: sidecar binary not built");
return;
}
let Some(os) = common::new_vm_with_commands().await else {
eprintln!("skipping exec_command_line_paths: coreutils wasm artifacts absent");
return;
};
// Direct path: a simple command with arguments runs and returns its output.
let echo = os
.exec("echo hello world", ExecOptions::default())
.await
.expect("exec echo");
assert_eq!(echo.exit_code, 0, "echo exit (stderr: {:?})", echo.stderr);
assert_eq!(echo.stdout.trim_end(), "hello world", "echo stdout");
// Direct path preserves a real non-zero exit code (the `sh -c` wrapper can swallow it).
let missing = os
.exec("cat /no/such/file", ExecOptions::default())
.await
.expect("exec cat missing returns a result");
assert_ne!(
missing.exit_code, 0,
"cat of a missing file must report a non-zero exit code"
);
// Shell path: a `&&` chain runs as a single `sh -c` execution.
let chain = os
.exec("echo a && echo b", ExecOptions::default())
.await
.expect("exec && chain");
assert_eq!(
chain.exit_code, 0,
"chain exit (stderr: {:?})",
chain.stderr
);
assert_eq!(chain.stdout, "a\nb\n", "chain stdout");
// Shell path: a redirect writes a file the VM can read back.
let redirect = os
.exec(
"echo redirected > /tmp/exec_redirect.txt",
ExecOptions::default(),
)
.await
.expect("exec redirect");
assert_eq!(
redirect.exit_code, 0,
"redirect exit (stderr: {:?})",
redirect.stderr
);
let body = os
.read_file("/tmp/exec_redirect.txt")
.await
.expect("read redirected file");
assert_eq!(
String::from_utf8_lossy(&body).trim_end(),
"redirected",
"redirected file contents"
);
// Shell path: a quoted argument with a space stays one token through `sh -c`.
let quoted = os
.exec("echo 'a b'", ExecOptions::default())
.await
.expect("exec quoted");
assert_eq!(quoted.stdout.trim_end(), "a b", "quoted stdout");
// An empty command line is an explicit error, not a silent no-op.
assert!(
os.exec(" ", ExecOptions::default()).await.is_err(),
"empty command line must error"
);
os.shutdown().await.expect("shutdown");
}