#![cfg(all(feature = "async", feature = "json"))]
use std::sync::{Arc, Mutex};
use claude_wrapper::{Claude, SpawnInfo};
fn recording() -> (Arc<Mutex<Vec<SpawnInfo>>>, claude_wrapper::SpawnObserver) {
let seen = Arc::new(Mutex::new(Vec::new()));
let sink = seen.clone();
(seen, Arc::new(move |info| sink.lock().unwrap().push(info)))
}
fn fake() -> &'static str {
concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fake-claude.sh")
}
#[tokio::test]
async fn observer_reports_a_live_pid_for_a_one_shot_run() {
let (seen, observer) = recording();
let claude = Claude::builder()
.binary(fake())
.on_spawn(observer)
.build()
.unwrap();
claude_wrapper::exec::run_claude(&claude, vec!["--version".into()])
.await
.expect("fake binary reports a version");
let seen = seen.lock().unwrap();
assert_eq!(seen.len(), 1, "exactly one spawn, got {seen:?}");
assert!(seen[0].pid > 0, "pid must be real, got {}", seen[0].pid);
}
#[tokio::test]
async fn pgid_is_some_only_when_the_child_leads_its_own_group() {
for (process_group, expect_pgid) in [(true, true), (false, false)] {
let (seen, observer) = recording();
let claude = Claude::builder()
.binary(fake())
.process_group(process_group)
.on_spawn(observer)
.build()
.unwrap();
claude_wrapper::exec::run_claude(&claude, vec!["--version".into()])
.await
.unwrap();
let seen = seen.lock().unwrap();
let info = seen.first().expect("observer fired");
assert_eq!(
info.pgid.is_some(),
expect_pgid,
"process_group={process_group} should give pgid.is_some()={expect_pgid}, got {info:?}"
);
if let Some(pgid) = info.pgid {
assert_eq!(pgid, info.pid, "a group leader's pgid is its own pid");
}
}
}
#[tokio::test]
async fn observer_fires_before_the_run_produces_output() {
let order = Arc::new(Mutex::new(Vec::<&'static str>::new()));
let sink = order.clone();
let claude = Claude::builder()
.binary(fake())
.on_spawn(Arc::new(move |_| sink.lock().unwrap().push("spawn")))
.build()
.unwrap();
let out = claude_wrapper::exec::run_claude(&claude, vec!["--version".into()])
.await
.unwrap();
order.lock().unwrap().push("output");
assert_eq!(
*order.lock().unwrap(),
vec!["spawn", "output"],
"the observer must fire before output is available"
);
assert!(!out.stdout.is_empty(), "the run really did produce output");
}
#[tokio::test]
async fn each_retry_attempt_is_its_own_spawn() {
use claude_wrapper::RetryPolicy;
let (seen, observer) = recording();
let claude = Claude::builder()
.binary(if std::path::Path::new("/usr/bin/false").exists() {
"/usr/bin/false"
} else {
"/bin/false"
})
.retry(
RetryPolicy::new()
.max_attempts(3)
.fixed()
.initial_backoff(std::time::Duration::from_millis(1))
.retry_on_exit_codes([1]),
)
.on_spawn(observer)
.build()
.unwrap();
let _ = claude_wrapper::exec::run_claude(&claude, vec!["--version".into()]).await;
let seen = seen.lock().unwrap();
assert_eq!(seen.len(), 3, "one spawn per attempt, got {seen:?}");
let first = seen[0].pid;
assert!(
seen.iter().any(|i| i.pid != first),
"attempts should be different processes, got {seen:?}"
);
}
#[tokio::test]
async fn no_observer_configured_is_fine() {
let claude = Claude::builder().binary(fake()).build().unwrap();
claude_wrapper::exec::run_claude(&claude, vec!["--version".into()])
.await
.expect("runs normally with no observer");
}