use crate::tools::exec_command::strip_cd_prefix;
use crate::tools::exec_runtime::{
build_exec_command, handle_output_persist, persist_interleaved_overflow, run_exec_impl,
};
use crate::{SIZE_LIMIT, STDIN_MAX_BYTES, filters::CompiledRule};
#[test]
fn test_exec_stdin_size_cap_validation() {
let oversized_stdin = "x".repeat(STDIN_MAX_BYTES + 1);
assert!(
oversized_stdin.len() > STDIN_MAX_BYTES,
"test setup: oversized stdin should exceed 1 MB"
);
let max_stdin = "y".repeat(STDIN_MAX_BYTES);
assert_eq!(
max_stdin.len(),
STDIN_MAX_BYTES,
"test setup: max stdin should be exactly 1 MB"
);
}
#[tokio::test]
async fn test_exec_stdin_cat_roundtrip() {
let stdin_content = "hello world";
let mut child = tokio::process::Command::new("sh")
.arg("-c")
.arg("cat")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn cat");
if let Some(mut stdin_handle) = child.stdin.take() {
use tokio::io::AsyncWriteExt as _;
stdin_handle
.write_all(stdin_content.as_bytes())
.await
.expect("write stdin");
drop(stdin_handle);
}
let output = child.wait_with_output().await.expect("wait for cat");
let stdout_str = String::from_utf8_lossy(&output.stdout);
assert!(
stdout_str.contains(stdin_content),
"stdout should contain stdin content: {}",
stdout_str
);
}
#[tokio::test]
async fn test_exec_stdin_none_no_regression() {
let child = tokio::process::Command::new("sh")
.arg("-c")
.arg("echo hi")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("spawn echo");
let output = child.wait_with_output().await.expect("wait for echo");
let stdout_str = String::from_utf8_lossy(&output.stdout);
assert!(
stdout_str.contains("hi"),
"stdout should contain echo output: {}",
stdout_str
);
}
#[test]
fn test_exec_command_path_injected() {
let resolved_path = Some("/usr/local/bin:/usr/bin:/bin");
let cmd = build_exec_command("echo test", None, false, resolved_path);
let cmd_str = format!("{:?}", cmd);
assert!(
!cmd_str.contains("-l"),
"build_exec_command must not use -l on any platform"
);
assert!(
!cmd_str.is_empty(),
"build_exec_command should return a valid Command"
);
}
#[test]
fn test_exec_command_path_fallback() {
let cmd = build_exec_command("echo test", None, false, None);
let cmd_str = format!("{:?}", cmd);
assert!(
!cmd_str.contains("-l"),
"build_exec_command must not use -l on any platform"
);
assert!(
!cmd_str.is_empty(),
"build_exec_command should handle None resolved_path gracefully"
);
}
#[test]
fn test_exec_no_truncation_under_limits() {
let stdout = "hello world".to_string();
let stderr = "no errors".to_string();
let slot = 0u32;
let (out_stdout, out_stderr, stdout_path, stderr_path, byte_truncated) =
handle_output_persist(stdout, stderr, slot);
assert_eq!(out_stdout, "hello world");
assert_eq!(out_stderr, "no errors");
assert!(stdout_path.is_none());
assert!(stderr_path.is_none());
assert!(!byte_truncated);
}
#[test]
fn test_exec_byte_overflow_stdout_exceeds_30k() {
let stdout = "x".repeat(35_000);
let stderr = "small".to_string();
let slot = 0u32;
let (out_stdout, out_stderr, stdout_path, stderr_path, byte_truncated) =
handle_output_persist(stdout.clone(), stderr.clone(), slot);
assert!(byte_truncated, "byte_truncated should be true");
assert!(stdout_path.is_some(), "stdout_path should be set");
assert!(stderr_path.is_some(), "stderr_path should be set");
assert!(
out_stdout.len() <= 30_000,
"stdout should be truncated to <= 30k"
);
assert_eq!(out_stderr, "small", "stderr should be unchanged");
let base = std::env::temp_dir()
.join("aptu-coder-overflow")
.join(format!("slot-{slot}"));
let stdout_file = base.join("stdout");
assert!(
stdout_file.exists(),
"stdout slot file should exist after byte overflow"
);
}
#[test]
fn test_exec_byte_overflow_stderr_exceeds_10k() {
let stdout = "small".to_string();
let stderr = "y".repeat(15_000);
let slot = 1u32;
let (out_stdout, out_stderr, stdout_path, stderr_path, byte_truncated) =
handle_output_persist(stdout.clone(), stderr.clone(), slot);
assert!(byte_truncated, "byte_truncated should be true");
assert!(stdout_path.is_some(), "stdout_path should be set");
assert!(stderr_path.is_some(), "stderr_path should be set");
assert_eq!(out_stdout, "small", "stdout should be unchanged");
assert!(
out_stderr.len() <= 10_000,
"stderr should be truncated to <= 10k"
);
let base = std::env::temp_dir()
.join("aptu-coder-overflow")
.join(format!("slot-{slot}"));
let stderr_file = base.join("stderr");
assert!(
stderr_file.exists(),
"stderr slot file should exist after byte overflow"
);
}
#[test]
fn test_exec_byte_overflow_combined_exceeds_50k() {
let large_output = "z".repeat(60_000);
assert!(large_output.len() > SIZE_LIMIT);
let mut combined_truncated = false;
let truncated = if large_output.len() > SIZE_LIMIT {
combined_truncated = true;
let tail_start = large_output.len().saturating_sub(SIZE_LIMIT);
let safe_start = large_output.floor_char_boundary(tail_start);
large_output[safe_start..].to_string()
} else {
large_output.clone()
};
assert!(combined_truncated, "combined_truncated should be true");
assert!(
truncated.len() <= SIZE_LIMIT,
"output should be truncated to <= 50k"
);
}
#[test]
fn test_exec_line_and_byte_interaction() {
let lines: Vec<String> = (0..1500)
.map(|i| {
format!(
"line {} with some padding to make it longer: {}",
i,
"x".repeat(15)
)
})
.collect();
let stdout = lines.join("\n");
assert!(stdout.lines().count() <= 2000, "should have <= 2000 lines");
assert!(stdout.len() > 30_000, "should exceed 30k bytes");
let stderr = "".to_string();
let slot = 2u32;
let (out_stdout, _out_stderr, stdout_path, _stderr_path, byte_truncated) =
handle_output_persist(stdout.clone(), stderr, slot);
assert!(byte_truncated, "byte_truncated should be true");
assert!(stdout_path.is_some(), "stdout_path should be set");
assert!(
out_stdout.len() <= 30_000,
"stdout should be truncated by byte cap"
);
}
#[test]
fn test_exec_utf8_boundary_safety() {
let mut stdout = String::new();
for _ in 0..4000 {
stdout.push_str("hello world ");
}
stdout.push_str("γγγ«γ‘γ―"); assert!(stdout.len() > 30_000, "stdout should exceed 30k bytes");
let stderr = "".to_string();
let slot = 5u32;
let (out_stdout, _out_stderr, _stdout_path, _stderr_path, byte_truncated) =
handle_output_persist(stdout, stderr, slot);
assert!(byte_truncated, "byte_truncated should be true");
assert!(
out_stdout.is_char_boundary(0),
"start should be char boundary"
);
assert!(
out_stdout.is_char_boundary(out_stdout.len()),
"end should be char boundary"
);
let _char_count = out_stdout.chars().count();
}
#[test]
fn test_strip_cd_prefix_basic() {
let (cmd, path) = strip_cd_prefix("cd /tmp && echo hello");
assert_eq!(cmd, "echo hello");
assert_eq!(path, Some("/tmp"));
}
#[test]
fn test_strip_cd_prefix_no_ampersand() {
let (cmd, path) = strip_cd_prefix("cd /tmp");
assert_eq!(cmd, "cd /tmp");
assert_eq!(path, None);
}
#[test]
fn test_strip_cd_prefix_with_extra_spaces() {
let (cmd, path) = strip_cd_prefix("cd /tmp && echo hello");
assert_eq!(path, Some("/tmp"));
assert_eq!(cmd, "echo hello");
}
#[test]
fn test_strip_cd_prefix_splits_on_first_ampersand_only() {
let (cmd, path) = strip_cd_prefix("cd /a && cmd1 && cd /b && cmd2");
assert_eq!(path, Some("/a"));
assert_eq!(cmd, "cmd1 && cd /b && cmd2");
}
#[tokio::test]
async fn test_handle_output_persist_mid_char_boundary() {
let mut stdout = String::new();
stdout.push('\u{4E2D}'); stdout.push_str(&"a".repeat(29998)); assert_eq!(stdout.len(), 30_001);
let stderr = String::new();
let slot = 99u32;
let (out_stdout, _out_stderr, _stdout_path, _stderr_path, byte_truncated) =
handle_output_persist(stdout, stderr, slot);
assert!(byte_truncated, "byte_truncated should be true");
assert!(
out_stdout.is_char_boundary(0),
"start should be char boundary"
);
assert!(
out_stdout.is_char_boundary(out_stdout.len()),
"end should be char boundary"
);
let _char_count = out_stdout.chars().count();
}
#[tokio::test]
async fn test_persist_interleaved_mid_char_boundary() {
let mut interleaved = String::new();
interleaved.push('\u{4E2D}'); interleaved.push_str(&"a".repeat(98)); assert_eq!(interleaved.len(), 101);
let max_bytes = 100usize;
let slot = 42u32;
let (preview, path) = persist_interleaved_overflow(interleaved, max_bytes, slot).await;
assert!(path.is_some(), "should have overflowed to slot file");
assert!(preview.is_char_boundary(0), "start should be char boundary");
assert!(
preview.is_char_boundary(preview.len()),
"end should be char boundary"
);
let _char_count = preview.chars().count();
}
#[tokio::test]
async fn test_run_exec_impl_raw_byte_counters() {
let filter_table = std::sync::Arc::new(Vec::<CompiledRule>::new());
let (output, raw_so, raw_se) = run_exec_impl(
"echo hello && echo world >&2".to_string(),
None,
None,
0,
None,
&filter_table,
Some(5),
std::time::Duration::from_millis(500),
)
.await;
assert!(raw_so >= 6, "raw_stdout_bytes should be >= 6 (hello\\n)");
assert!(raw_se >= 6, "raw_stderr_bytes should be >= 6 (world\\n)");
assert_eq!(output.exit_code, Some(0));
assert!(!output.timed_out);
}
#[tokio::test]
async fn test_run_exec_impl_raw_counters_exceed_budget() {
let filter_table = std::sync::Arc::new(Vec::<CompiledRule>::new());
let large_line = "x".repeat(1000);
let cmd = format!("for i in $(seq 1 50); do echo {}; done", large_line);
let (output, raw_so, _raw_se) = run_exec_impl(
cmd,
None,
None,
0,
None,
&filter_table,
Some(10),
std::time::Duration::from_millis(500),
)
.await;
assert!(raw_so > 30_000, "raw_stdout_bytes should exceed 30k budget");
assert!(output.output_truncated, "output should be truncated");
assert_eq!(output.exit_code, Some(0));
assert!(!output.timed_out);
}
#[tokio::test]
async fn test_run_exec_impl_raw_counters_zero_on_timeout() {
let filter_table = std::sync::Arc::new(Vec::<CompiledRule>::new());
let (output, raw_so, raw_se) = run_exec_impl(
"sleep 2".to_string(),
None,
None,
0,
None,
&filter_table,
Some(1), std::time::Duration::from_millis(500),
)
.await;
assert!(output.timed_out, "command should have timed out");
assert_eq!(raw_so, 0, "raw_stdout_bytes should be 0 on timeout");
assert_eq!(raw_se, 0, "raw_stderr_bytes should be 0 on timeout");
}