use choreo_daemon::tools::shell_util::{RecordFraming, run_shell_streaming, spawn_with_streaming};
use choreo_daemon::{ShArgs, execute_sh_tool};
use std::path::Path;
fn cmd(program: &str, arg: &str, dir: &Path) -> std::process::Command {
let mut c = std::process::Command::new(program);
c.args(["-c", arg])
.current_dir(dir)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
c
}
#[test]
#[ignore]
fn spawn_with_streaming_produces_stdout() {
let dir = Path::new("/tmp");
let mut c = cmd("bash", "echo hello world", dir);
let (tx, rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let (output, was_killed) =
spawn_with_streaming(&mut c, 5000, RecordFraming::none(), tx).unwrap();
drop(rx);
assert!(!was_killed, "should not have been killed by timeout");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("hello world"), "stdout: {stdout}");
assert!(output.status.success(), "exit should be 0");
}
#[test]
#[ignore]
fn spawn_with_streaming_stderr_is_streamed_into_the_body() {
let dir = Path::new("/tmp");
let mut c = cmd("bash", "echo errmsg >&2", dir);
let (tx, rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let (output, was_killed) =
spawn_with_streaming(&mut c, 5000, RecordFraming::none(), tx).unwrap();
let streamed: String = rx
.try_iter()
.map(|c| String::from_utf8_lossy(&c).into_owned())
.collect();
assert!(!was_killed);
let body = String::from_utf8_lossy(&output.stdout);
assert!(body.contains("errmsg"), "body: {body}");
assert!(streamed.contains("errmsg"), "streamed: {streamed}");
}
#[test]
#[ignore]
fn spawn_with_streaming_interleaves_stdout_and_stderr() {
let dir = Path::new("/tmp");
let mut c = cmd("bash", "echo out1; echo err1 >&2; echo out2", dir);
let (tx, rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let (output, _was_killed) =
spawn_with_streaming(&mut c, 5000, RecordFraming::none(), tx).unwrap();
let streamed: String = rx
.try_iter()
.map(|c| String::from_utf8_lossy(&c).into_owned())
.collect();
let body = String::from_utf8_lossy(&output.stdout);
let pos1 = body.find("out1").expect("out1 in body");
let pos2 = body.find("out2").expect("out2 in body");
assert!(pos1 < pos2, "stdout order preserved: {body}");
assert!(body.contains("err1"), "stderr present in body: {body}");
for needle in ["out1", "err1", "out2"] {
assert!(
streamed.contains(needle),
"stream missing {needle}: {streamed}"
);
}
}
#[test]
#[ignore]
fn run_shell_streaming_final_body_matches_streamed_body() {
let dir = Path::new("/tmp");
let mut c = cmd("bash", "echo line1; echo err1 >&2; echo line2", dir);
let (tx, rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let collector = std::thread::spawn(move || {
let mut s = String::new();
for chunk in rx {
s.push_str(&String::from_utf8_lossy(&chunk));
}
s
});
let result = run_shell_streaming(&mut c, "echo", 5000, tx).unwrap();
let streamed = collector.join().unwrap();
assert_eq!(
result,
format!("$ echo\n{streamed}\n\nExit code: 0"),
"final body must equal the streamed body"
);
for needle in ["line1", "err1", "line2"] {
assert!(
streamed.contains(needle),
"stream missing {needle}: {streamed}"
);
}
}
#[test]
#[ignore]
fn spawn_with_streaming_timeout_kills() {
let dir = Path::new("/tmp");
let mut c = cmd("bash", "sleep 10", dir);
let (tx, rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let (output, was_killed) =
spawn_with_streaming(&mut c, 500, RecordFraming::none(), tx).unwrap();
drop(rx);
assert!(was_killed, "should have been killed by timeout");
assert!(!output.status.success(), "should have non-zero exit");
}
#[test]
#[ignore]
fn run_shell_streaming_combines_output() {
let dir = Path::new("/tmp");
let mut c = cmd("bash", "echo hello", dir);
let (tx, rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let result = run_shell_streaming(&mut c, "echo hello", 5000, tx).unwrap();
drop(rx);
assert!(result.contains("hello"), "result: {result}");
assert!(result.contains("Exit code: 0"), "result: {result}");
}
#[test]
#[ignore]
fn run_shell_streaming_streams_lines_in_realtime() {
let dir = Path::new("/tmp");
let mut c = cmd("bash", "echo line1 && echo line2", dir);
let (tx, rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let handle = std::thread::spawn(move || {
let mut chunks = Vec::new();
for chunk in rx {
chunks.push(String::from_utf8_lossy(&chunk).to_string());
}
chunks
});
let result = run_shell_streaming(&mut c, "echo", 5000, tx).unwrap();
let streamed = handle.join().unwrap();
assert!(result.contains("Exit code: 0"), "result: {result}");
let combined: String = streamed.concat();
assert!(combined.contains("line1"), "streamed: {combined}");
assert!(combined.contains("line2"), "streamed: {combined}");
}
#[test]
#[ignore]
fn execute_sh_tool_non_streaming_still_works() {
let result = execute_sh_tool(
&ShArgs {
command: "echo hello".into(),
shell: choreo_daemon::Shell::Bash,
workdir: None,
timeout: None,
},
Some(Path::new("/tmp")),
);
let content = result.unwrap_or_default();
assert!(content.contains("hello"), "{content}");
assert!(content.contains("Exit code: 0"), "{content}");
}
#[test]
#[ignore]
fn spawn_with_streaming_caps_total_forwarded_bytes() {
let mut cmd = std::process::Command::new("sh");
cmd.args([
"-c",
"i=0; while [ $i -lt 5000 ]; do printf 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\\n'; i=$((i+1)); done",
])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let (output_tx, output_rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let (output, _was_killed) =
spawn_with_streaming(&mut cmd, 30_000, RecordFraming::none(), output_tx).expect("spawn");
let mut forwarded = 0usize;
let mut marker_count = 0usize;
for chunk in output_rx {
if chunk.as_slice() == b"\n...[truncated]" {
marker_count += 1;
}
forwarded += chunk.len();
}
assert_eq!(
marker_count, 1,
"truncation marker must be sent exactly once"
);
assert!(
forwarded <= choreo_sanitize::MAX_TOOL_OUTPUT_BYTES + b"\n...[truncated]".len(),
"streamed total must not exceed cap + one marker: {forwarded}"
);
assert!(
output.stdout.len() <= choreo_sanitize::MAX_TOOL_OUTPUT_BYTES + b"\n...[truncated]".len(),
"accumulated stdout must be capped at budget + one marker: {}",
output.stdout.len()
);
assert!(
forwarded > 0,
"the command's output must actually be streamed"
);
}
#[test]
#[ignore]
fn run_shell_streaming_truncated_record_matches_streamed_body() {
let dir = Path::new("/tmp");
let mut c = cmd(
"sh",
"i=0; while [ $i -lt 5000 ]; do printf 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\\n'; i=$((i+1)); done",
dir,
);
let (tx, rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let collector = std::thread::spawn(move || {
let mut s = Vec::new();
for chunk in rx {
s.extend_from_slice(&chunk);
}
s
});
let result = run_shell_streaming(&mut c, "sh", 30_000, tx).unwrap();
let streamed = collector.join().unwrap();
let body = result
.strip_prefix("$ sh\n")
.expect("header prefix")
.strip_suffix("\n\nExit code: 0")
.expect("footer suffix");
assert_eq!(
body.as_bytes(),
streamed,
"recorded body must equal the streamed view"
);
assert!(
streamed.ends_with(b"\n...[truncated]"),
"streamed view must show the truncation marker"
);
assert!(
result.len() <= choreo_sanitize::MAX_TOOL_OUTPUT_BYTES,
"recorded result must stay within the budget: {}",
result.len()
);
assert_eq!(result.matches("...[truncated]").count(), 1);
}
#[test]
#[ignore]
fn run_shell_streaming_cf_heavy_record_matches_streamed_body() {
let dir = Path::new("/tmp");
let mut c = cmd(
"sh",
"i=0; while [ $i -lt 45000 ]; do printf '\\342\\200\\213'; i=$((i+1)); done",
dir,
);
let (tx, rx) = crossbeam_channel::unbounded::<Vec<u8>>();
let collector = std::thread::spawn(move || {
let mut s = Vec::new();
for chunk in rx {
s.extend_from_slice(&chunk);
}
s
});
let result = run_shell_streaming(&mut c, "sh", 30_000, tx).unwrap();
let streamed = collector.join().unwrap();
let body = result
.strip_prefix("$ sh\n")
.expect("header prefix")
.strip_suffix("\n\nExit code: 0")
.expect("footer suffix");
assert_eq!(
body.as_bytes(),
streamed,
"recorded body must equal the streamed view even for Cf-heavy output"
);
assert!(
streamed.ends_with(b"\n...[truncated]"),
"streamed view must show the truncation marker"
);
assert!(
result.len() <= choreo_sanitize::MAX_TOOL_OUTPUT_BYTES,
"recorded result must stay within the budget: {}",
result.len()
);
assert_eq!(
result.matches("...[truncated]").count(),
1,
"the marker must appear exactly once (no re-cut)"
);
}