Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 47 additions & 28 deletions rust/src/managed_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1056,41 +1056,51 @@ mod tests {
drop(process);
}

// Reuse the native test executable so the containment test does not time
// PowerShell/.NET startup while the rest of the library suite runs.
#[test]
fn managed_descendant_fixture() {
let Some(marker) = std::env::var_os("CODEXBAR_MANAGED_DESCENDANT_MARKER") else {
return;
};
if std::env::var_os("CODEXBAR_MANAGED_DESCENDANT_LEAF").is_none() {
use std::os::windows::process::CommandExt;
let child = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"managed_process::tests::managed_descendant_fixture",
])
.env("CODEXBAR_MANAGED_DESCENDANT_LEAF", "1")
.creation_flags(0x0800_0000)
.spawn()
.expect("start the native descendant fixture");
std::fs::write(marker, child.id().to_string()).expect("write descendant pid");
}
std::thread::sleep(Duration::from_secs(120));
}

#[tokio::test]
async fn managed_process_contains_descendants_of_the_pty_child() {
let marker = std::env::temp_dir().join(format!(
"codexbar-managed-descendant-{}.txt",
std::process::id()
));
drop(std::fs::remove_file(&marker));
let script = format!(
"$psi = New-Object System.Diagnostics.ProcessStartInfo; \
$psi.FileName = 'powershell.exe'; \
$psi.Arguments = '-NoLogo -NoProfile -Command \"Start-Sleep -Seconds 120\"'; \
$psi.UseShellExecute = $false; \
$p = [System.Diagnostics.Process]::Start($psi); \
Set-Content -LiteralPath '{}' -Value $p.Id; \
Start-Sleep -Seconds 120",
marker.display()
);
let directory = tempfile::tempdir().expect("isolated descendant fixture directory");
let marker = directory.path().join("descendant.pid");
let config = ManagedProcessConfig {
program: PathBuf::from("powershell.exe"),
program: std::env::current_exe().expect("native test executable"),
args: vec![
OsString::from("-NoLogo"),
OsString::from("-NoProfile"),
OsString::from("-NonInteractive"),
OsString::from("-Command"),
OsString::from(script),
OsString::from("--exact"),
OsString::from("managed_process::tests::managed_descendant_fixture"),
],
env: Vec::new(),
cwd: None,
env: vec![(
OsString::from("CODEXBAR_MANAGED_DESCENDANT_MARKER"),
marker.as_os_str().to_owned(),
)],
cwd: Some(directory.path().to_owned()),
pty_rows: 30,
pty_cols: 120,
label: "test-descendant".to_string(),
};
let process = ManagedProcess::spawn(&config).expect("start a managed test process");
let mut process = ManagedProcess::spawn(&config).expect("start a managed test process");

let descendant = wait_for_descendant_pid(&marker);
let descendant = wait_for_descendant_pid(&marker, &mut process);
assert!(
process_is_alive(descendant),
"the descendant of the PTY child should be running before shutdown"
Expand All @@ -1099,20 +1109,29 @@ mod tests {
process.shutdown(Duration::from_secs(10)).await;

wait_for_exit_within(descendant, Duration::from_secs(10));
drop(std::fs::remove_file(&marker));
}

fn wait_for_descendant_pid(marker: &Path) -> u32 {
fn wait_for_descendant_pid(marker: &Path, process: &mut ManagedProcess) -> u32 {
let deadline = Instant::now() + Duration::from_secs(15);
loop {
if let Ok(contents) = std::fs::read_to_string(marker)
&& let Ok(pid) = contents.trim().parse::<u32>()
{
return pid;
}
assert!(
process
.try_wait()
.expect("poll descendant fixture")
.is_none(),
"PTY fixture exited before reporting its descendant pid"
);
assert!(
Instant::now() < deadline,
"the PTY child did not report a descendant pid"
"PTY fixture {} did not report a descendant pid at {}: {:?}",
process.pid(),
marker.display(),
std::fs::read_to_string(marker)
);
std::thread::sleep(Duration::from_millis(50));
}
Expand Down
70 changes: 60 additions & 10 deletions rust/src/providers/claude/accounts/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,23 +582,73 @@ mod tests {
)
.unwrap();
std::fs::write(dir.path().join(".claude.json"), r#"{"oauthAccount":{"accountUuid":"test","organizationUuid":"org","emailAddress":"test@example.com"}}"#).unwrap();
// This test covers exit handling and credential isolation, not shell
// startup speed. Reap each fixture before starting the login deadline;
// cancelled_and_timed_out_logins_reap_child covers a running process.
let mut successful_child = child(dir.path(), false, 0);
assert!(
wait_for_fixture(&mut successful_child, Duration::from_secs(60))
.unwrap()
.success()
);
let login = wait_for_login(
&mut child(dir.path(), false, 0),
&mut successful_child,
dir.path(),
&AtomicBool::new(false),
Duration::from_secs(10),
Duration::ZERO,
)
.unwrap();
assert_eq!(login.id().unwrap(), "test:org");
assert!(
wait_for_login(
&mut child(dir.path(), false, 1),
dir.path(),
&AtomicBool::new(false),
Duration::from_secs(10)
)
.is_err()
let mut failed_child = child(dir.path(), false, 1);
assert_eq!(
wait_for_fixture(&mut failed_child, Duration::from_secs(60))
.unwrap()
.code(),
Some(1)
);
let error = wait_for_login(
&mut failed_child,
dir.path(),
&AtomicBool::new(false),
Duration::ZERO,
)
.err()
.expect("a failed login process must be rejected");
assert!(error.to_string().contains("exit code 1"));
}

// This is a fixture-startup budget, not a change to the login timeout.
fn wait_for_fixture(
child: &mut LoginChild,
budget: Duration,
) -> io::Result<std::process::ExitStatus> {
let deadline = Instant::now() + budget;
loop {
if let Some(status) = child.try_wait()? {
return Ok(status);
}
if Instant::now() >= deadline {
child.kill()?;
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!("login test fixture did not exit within {budget:?}; terminated"),
));
}
std::thread::sleep(Duration::from_millis(25));
}
}

#[test]
fn stalled_login_fixture_is_terminated_at_its_deadline() {
let dir = tempfile::tempdir().unwrap();
let mut child = child(dir.path(), true, 0);
let started = Instant::now();
let error = wait_for_fixture(&mut child, Duration::ZERO).unwrap_err();
assert_eq!(error.kind(), io::ErrorKind::TimedOut);
assert!(error.to_string().contains("login test fixture"));
assert!(started.elapsed() < Duration::from_secs(5));
let status = wait_for_fixture(&mut child, Duration::from_secs(5)).unwrap();
assert!(!status.success());
}

#[test]
Expand Down