use crate::common::*; #[test] fn dry_run_prints_and_exits() { let output = Sandbox::new(&["--dry-run"]) .args(["--", "bash", "-c", "exit 42"]) .output() .expect("agent-sandbox binary failed to execute"); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("bwrap"), "expected bwrap command in dry-run output, got: {stdout}" ); assert!( output.status.success(), "dry-run should exit 0, not 42 from the inner command" ); } #[test] fn dry_run_output_is_copy_pasteable_shell() { let dry = Sandbox::new(&["--dry-run"]) .args(["--", "bash", "-c", "echo $HOME"]) .output() .expect("agent-sandbox binary failed to execute"); let dry_cmd = String::from_utf8_lossy(&dry.stdout).trim().to_string(); let args = shlex::split(&dry_cmd).expect("dry-run output is not valid shell"); assert_eq!(args[0], "bwrap"); assert_eq!(args[args.len() - 1], "echo $HOME"); assert_eq!(args[args.len() - 2], "-c"); } #[test] fn empty_home_rejected() { let output = Sandbox::new(&[]) .env("HOME", "") .args(["--", "true"]) .output() .expect("agent-sandbox binary failed to execute"); assert!( !output.status.success(), "expected failure with empty HOME, but got success" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.to_lowercase().contains("home"), "expected error mentioning HOME, got: {stderr}" ); } #[test] fn config_missing_file_errors() { let output = Sandbox::new_with_config(&["--config", "/nonexistent/config.toml"]) .args(["--", "true"]) .output() .expect("failed to execute"); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("/nonexistent/config.toml"), "expected config path in error, got: {stderr}" ); } #[test] fn config_invalid_toml_errors() { let cfg = ConfigFile::new("not valid {{{{ toml"); let output = Sandbox::new_with_config(&["--config", &cfg]) .args(["--", "true"]) .output() .expect("failed to execute"); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("cannot parse"), "expected parse error, got: {stderr}" ); } #[test] fn config_unknown_key_errors() { let cfg = ConfigFile::new("hardened = true\nbogus = \"nope\"\n"); let output = Sandbox::new_with_config(&["--config", &cfg]) .args(["--", "true"]) .output() .expect("failed to execute"); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("unknown config key"), "expected unknown key error, got: {stderr}" ); } #[test] fn bwrap_arg_setenv_passes_through() { let output = Sandbox::new(&["--bwrap-arg", "--setenv MYVAR hello"]) .args(["--", "bash", "-c", "echo $MYVAR"]) .output() .expect("agent-sandbox binary failed to execute"); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!( stdout, "hello", "expected --bwrap-arg to pass --setenv through to bwrap, got: {stdout}" ); } #[test] fn config_entrypoint_appends_passthrough_args() { let cfg = ConfigFile::new( r#" [profiles.test] entrypoint = ["bash", "-c"] "#, ); let output = Sandbox::new_with_config(&["--config", &cfg, "--profile", "test"]) .args(["--", "echo entrypoint-works"]) .output() .expect("failed to execute"); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!( stdout, "entrypoint-works", "expected passthrough args appended to entrypoint, got: {stdout}" ); } #[test] fn config_entrypoint_falls_back_to_command_defaults() { let cfg = ConfigFile::new( r#" [profiles.test] entrypoint = ["bash", "-c"] command = ["echo default-args"] "#, ); let output = Sandbox::new_with_config(&["--config", &cfg, "--profile", "test"]) .output() .expect("failed to execute"); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!( stdout, "default-args", "expected command defaults when no passthrough args, got: {stdout}" ); } #[test] fn config_entrypoint_alone_without_command_or_passthrough() { let cfg = ConfigFile::new( r#" [profiles.test] entrypoint = ["bash", "-c", "echo entrypoint-only"] "#, ); let output = Sandbox::new_with_config(&["--config", &cfg, "--profile", "test"]) .output() .expect("failed to execute"); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!( stdout, "entrypoint-only", "expected entrypoint to run on its own, got: {stdout}" ); } #[test] fn cli_entrypoint_appends_passthrough_args() { let output = Sandbox::new(&["--entrypoint", "bash"]) .args(["--", "-c", "echo cli-entrypoint-works"]) .output() .expect("failed to execute"); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!( stdout, "cli-entrypoint-works", "expected --entrypoint to receive trailing args, got: {stdout}" ); } #[test] fn cli_entrypoint_overrides_config_entrypoint() { let cfg = ConfigFile::new( r#" entrypoint = ["/bin/false"] "#, ); let output = Sandbox::new_with_config(&["--config", &cfg, "--entrypoint", "bash"]) .args(["--", "-c", "echo override-works"]) .output() .expect("failed to execute"); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!( stdout, "override-works", "expected CLI --entrypoint to override config entrypoint, got: {stdout}" ); } #[test] fn config_command_alone_without_passthrough() { let cfg = ConfigFile::new( r#" [profiles.test] command = ["bash", "-c", "echo command-only"] "#, ); let output = Sandbox::new_with_config(&["--config", &cfg, "--profile", "test"]) .output() .expect("failed to execute"); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!( stdout, "command-only", "expected config command to run on its own, got: {stdout}" ); } #[test] fn config_command_replaced_by_passthrough() { let cfg = ConfigFile::new( r#" [profiles.test] command = ["bash", "-c", "echo should-not-see-this"] "#, ); let output = Sandbox::new_with_config(&["--config", &cfg, "--profile", "test"]) .args(["--", "bash", "-c", "echo replaced"]) .output() .expect("failed to execute"); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert_eq!( stdout, "replaced", "expected passthrough to replace config command, got: {stdout}" ); }