#![cfg(all(feature = "async", feature = "json"))]
use std::sync::{Arc, Mutex};
use claude_wrapper::Claude;
fn fake() -> &'static str {
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fake-claude.sh")
}
#[test]
fn support_is_reported_honestly_per_platform() {
assert_eq!(
claude_wrapper::exec::die_with_parent_supported(),
cfg!(target_os = "linux"),
"support must track the platform, not the build"
);
}
#[tokio::test]
async fn enabling_it_does_not_disturb_a_normal_run() {
let claude = Claude::builder()
.binary(fake())
.die_with_parent(true)
.build()
.unwrap();
let out = claude_wrapper::exec::run_claude(&claude, vec!["--version".into()])
.await
.expect("a run with die_with_parent enabled still succeeds");
assert!(!out.stdout.is_empty());
}
#[tokio::test]
async fn it_composes_with_the_spawn_observer() {
let seen = Arc::new(Mutex::new(Vec::new()));
let sink = seen.clone();
let claude = Claude::builder()
.binary(fake())
.die_with_parent(true)
.on_spawn(Arc::new(move |info| sink.lock().unwrap().push(info)))
.build()
.unwrap();
claude_wrapper::exec::run_claude(&claude, vec!["--version".into()])
.await
.unwrap();
assert_eq!(seen.lock().unwrap().len(), 1, "observer still fires");
}
#[test]
fn default_is_off() {
let claude = Claude::builder().binary(fake()).build().unwrap();
let rendered = format!("{claude:?}");
assert!(
!rendered.contains("die_with_parent: true"),
"default must be off, got {rendered}"
);
}
const HELPER_ENV: &str = "CLAUDE_WRAPPER_PDEATHSIG_HELPER";
#[test]
fn pdeathsig_helper_process() {
if std::env::var(HELPER_ENV).is_err() {
return;
}
let claude = Claude::builder()
.binary("/bin/sleep")
.die_with_parent(true)
.on_spawn(Arc::new(|info| {
println!("PID {}", info.pid);
use std::io::Write;
let _ = std::io::stdout().flush();
}))
.build()
.unwrap();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let _ = rt.block_on(claude_wrapper::exec::run_claude(
&claude,
vec!["300".into()],
));
}
#[cfg(target_os = "linux")]
#[test]
fn child_dies_when_the_parent_is_sigkilled() {
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
let exe = std::env::current_exe().expect("test binary path");
let mut helper = Command::new(&exe)
.args(["--exact", "pdeathsig_helper_process", "--nocapture"])
.env(HELPER_ENV, "1")
.stdout(Stdio::piped())
.spawn()
.expect("spawning the helper");
let stdout = helper.stdout.take().expect("piped");
let mut pid = None;
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
if let Some(rest) = line.strip_prefix("PID ") {
pid = rest.trim().parse::<u32>().ok();
break;
}
}
let pid = pid.expect("helper reported a child pid");
unsafe { libc::kill(helper.id() as i32, libc::SIGKILL) };
let _ = helper.wait();
let mut alive = true;
for _ in 0..50 {
std::thread::sleep(std::time::Duration::from_millis(100));
if unsafe { libc::kill(pid as i32, 0) } != 0 {
alive = false;
break;
}
}
assert!(
!alive,
"child {pid} survived its SIGKILLed parent; PR_SET_PDEATHSIG did not fire"
);
}