use super::*;
#[cfg(unix)]
async fn wait_for_published_pid(path: &std::path::Path, process: &str) -> i32 {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
if let Some(pid) = std::fs::read_to_string(path)
.ok()
.and_then(|raw| raw.trim().parse::<i32>().ok())
{
return pid;
}
assert!(
std::time::Instant::now() < deadline,
"{process} did not publish a complete PID"
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
}
#[test]
fn wrapper_probe_uses_last_host_token_and_validates_opaque_wrappers() {
assert_eq!(
version_probe_argv(
CodingAgent::Codex,
&command_argv("npm exec --package @openai/codex -- codex exec")
),
[
"npm",
"exec",
"--package",
"@openai/codex",
"--",
"codex",
"--version"
]
);
assert_eq!(
version_probe_argv(
CodingAgent::Codex,
&command_argv("custom-codex-wrapper --profile dev")
),
["custom-codex-wrapper", "--profile", "dev", "--version"]
);
assert_eq!(
version_probe_argv(CodingAgent::Codex, &[]),
["codex", "--version"]
);
}
#[test]
fn platform_resolution_supports_explicit_paths_and_windows_pathext() {
let temp = tempfile::tempdir().unwrap();
let shim = temp.path().join("codex.CMD");
std::fs::write(&shim, "").unwrap();
assert_eq!(
resolve_executable_for_platform(
"codex",
Some(temp.path().as_os_str()),
Some(std::ffi::OsStr::new(".EXE;.CMD")),
true,
),
Some(shim.clone())
);
assert_eq!(
resolve_executable_for_platform(
shim.to_str().unwrap(),
None,
Some(std::ffi::OsStr::new(".EXE;.CMD")),
true,
),
Some(shim)
);
assert_eq!(resolve_executable_for_platform("", None, None, false), None);
}
#[cfg(unix)]
#[tokio::test]
async fn supervised_wait_terminates_descendants_left_by_a_wrapper() {
let temp = tempfile::tempdir().unwrap();
let descendant_pid_path = temp.path().join("descendant.pid");
let argv = vec![
"sh".into(),
"-c".into(),
"sleep 30 & echo $! > \"$1\"; exit 0".into(),
"sh".into(),
descendant_pid_path.display().to_string(),
];
let mut command = tokio_command(&argv);
let mut child = SupervisedChild::spawn(&mut command).await.unwrap();
let status = tokio::time::timeout(std::time::Duration::from_secs(5), child.wait())
.await
.expect("wrapper did not exit")
.unwrap();
assert!(status.success());
let pid = wait_for_published_pid(&descendant_pid_path, "wrapper descendant").await;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
let result = unsafe { libc::kill(pid, 0) };
if result == -1 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
break;
}
assert!(
std::time::Instant::now() < deadline,
"wrapper descendant {pid} survived normal wrapper exit"
);
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
}
#[cfg(unix)]
#[tokio::test]
async fn supervision_error_terminates_and_reaps_the_child_tree_first() {
let temp = tempfile::tempdir().unwrap();
let child_pid_path = temp.path().join("child.pid");
let argv = vec![
"sh".into(),
"-c".into(),
"echo $$ > \"$1\"; exec sleep 30".into(),
"sh".into(),
child_pid_path.display().to_string(),
];
let mut command = tokio_command(&argv);
let mut child = SupervisedChild::spawn(&mut command).await.unwrap();
let pid = wait_for_published_pid(&child_pid_path, "supervised child").await;
let error = child
.inject_wait_error_for_test(std::io::Error::other("injected wait failure"))
.await
.unwrap_err();
assert!(error.to_string().contains("injected wait failure"));
assert_eq!(unsafe { libc::kill(pid, 0) }, -1);
assert_eq!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::ESRCH)
);
}
#[cfg(windows)]
#[tokio::test]
async fn windows_supervision_assigns_before_a_wrapper_can_spawn_a_descendant() {
let temp = tempfile::tempdir().unwrap();
let descendant_pid_path = temp.path().join("descendant.pid");
let release_path = temp.path().join("release-wrapper");
let wrapper = temp.path().join("spawn-descendant.ps1");
std::fs::write(
&wrapper,
r#"$ErrorActionPreference = 'Stop'
$start = [System.Diagnostics.ProcessStartInfo]::new()
$start.FileName = (Get-Process -Id $PID).Path
$start.Arguments = '-NoProfile -NonInteractive -Command "Start-Sleep -Seconds 30"'
$start.UseShellExecute = $false
$start.CreateNoWindow = $true
$start.RedirectStandardInput = $true
$start.RedirectStandardOutput = $true
$start.RedirectStandardError = $true
$child = [System.Diagnostics.Process]::Start($start)
Set-Content -LiteralPath $args[0] -Value $child.Id -Encoding ASCII -NoNewline
$deadline = [DateTime]::UtcNow.AddSeconds(15)
while (-not (Test-Path -LiteralPath $args[1])) {
if ([DateTime]::UtcNow -ge $deadline) {
throw 'Relay test did not release the wrapper'
}
Start-Sleep -Milliseconds 20
}
"#,
)
.unwrap();
let argv = vec![
"powershell.exe".into(),
"-NoProfile".into(),
"-NonInteractive".into(),
"-ExecutionPolicy".into(),
"Bypass".into(),
"-File".into(),
wrapper.display().to_string(),
descendant_pid_path.display().to_string(),
release_path.display().to_string(),
];
let mut command = tokio_command(&argv);
command
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
let mut child = SupervisedChild::spawn(&mut command).await.unwrap();
let publish_deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let descendant_pid = loop {
if let Some(process_id) = read_windows_process_id(&descendant_pid_path) {
break process_id;
}
if std::time::Instant::now() >= publish_deadline {
let _ = child.terminate().await;
panic!("PowerShell wrapper did not publish its child PID");
}
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
};
let descendant = match WindowsTestProcess::open(descendant_pid) {
Ok(descendant) => descendant,
Err(error) => {
let _ = child.terminate().await;
panic!("could not retain wrapper descendant {descendant_pid}: {error}");
}
};
std::fs::write(release_path, b"ready").unwrap();
let status = match tokio::time::timeout(std::time::Duration::from_secs(15), child.wait()).await
{
Ok(status) => status.unwrap(),
Err(_) => {
let _ = child.terminate().await;
panic!("PowerShell wrapper did not exit");
}
};
assert!(status.success());
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let escaped = loop {
if !descendant.is_active().unwrap() {
break false;
}
if std::time::Instant::now() >= deadline {
break true;
}
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
};
if escaped {
descendant.terminate();
}
assert!(
!escaped,
"wrapper descendant {descendant_pid} survived Job Object termination"
);
}
#[cfg(windows)]
fn read_windows_process_id(path: &std::path::Path) -> Option<u32> {
std::fs::read_to_string(path).ok()?.parse().ok()
}
#[cfg(windows)]
struct WindowsTestProcess {
handle: windows_sys::Win32::Foundation::HANDLE,
}
#[cfg(windows)]
impl WindowsTestProcess {
fn open(process_id: u32) -> std::io::Result<Self> {
use windows_sys::Win32::System::Threading::{
OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, PROCESS_TERMINATE,
};
let handle = unsafe {
OpenProcess(
PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SYNCHRONIZE | PROCESS_TERMINATE,
0,
process_id,
)
};
if handle.is_null() {
Err(std::io::Error::last_os_error())
} else {
Ok(Self { handle })
}
}
fn is_active(&self) -> std::io::Result<bool> {
use windows_sys::Win32::Foundation::STILL_ACTIVE;
use windows_sys::Win32::System::Threading::GetExitCodeProcess;
let mut exit_code = 0;
if unsafe { GetExitCodeProcess(self.handle, &mut exit_code) } == 0 {
Err(std::io::Error::last_os_error())
} else {
Ok(exit_code == STILL_ACTIVE as u32)
}
}
fn terminate(&self) {
use windows_sys::Win32::System::Threading::{TerminateProcess, WaitForSingleObject};
if matches!(self.is_active(), Ok(false)) {
return;
}
unsafe {
TerminateProcess(self.handle, 1);
WaitForSingleObject(self.handle, 5_000);
}
}
}
#[cfg(windows)]
impl Drop for WindowsTestProcess {
fn drop(&mut self) {
use windows_sys::Win32::Foundation::CloseHandle;
self.terminate();
unsafe { CloseHandle(self.handle) };
}
}
#[cfg(windows)]
#[test]
fn windows_command_shim_preserves_metacharacter_arguments() {
let temp = tempfile::tempdir().unwrap();
let shim = temp.path().join("agent shim.cmd");
let marker = temp.path().join("completed.txt");
std::fs::write(
&shim,
"@echo off\r\n\
@if not \"%~1\"==\"space & value\" exit /b 11\r\n\
@if not \"%~2\"==\"caret^value\" exit /b 12\r\n\
@if not \"%~3\"==\"%%TOKEN%%\" exit /b 13\r\n\
@echo ok>\"%NEMO_RELAY_ARGV_MARKER%\"\r\n",
)
.unwrap();
let argv = vec![
shim.display().to_string(),
"space & value".into(),
"caret^value".into(),
"%TOKEN%".into(),
];
let status = std_command(&argv)
.env("NEMO_RELAY_ARGV_MARKER", &marker)
.status()
.unwrap();
assert!(status.success());
assert_eq!(std::fs::read_to_string(marker).unwrap().trim(), "ok");
}