app-server: test duplicate active process handles

This commit is contained in:
Adam Perry
2026-06-13 23:37:47 +00:00
parent 0151b459c2
commit 4656232376

View File

@@ -31,45 +31,7 @@ async fn process_spawn_returns_before_exit_and_emits_exit_notification() -> Resu
// Use a probe/release handshake instead of asserting on wall-clock timing:
// the child proves it started by writing the probe file, then waits for the
// test to create the release file before it can emit output and exit.
let command = if cfg!(windows) {
vec![
"powershell.exe".to_string(),
"-NoProfile".to_string(),
"-NonInteractive".to_string(),
"-Command".to_string(),
concat!(
"[IO.File]::WriteAllText($env:CODEX_PROCESS_EXEC_PROBE_FILE, 'process'); ",
"while (!(Test-Path -LiteralPath $env:CODEX_PROCESS_EXEC_RELEASE_FILE)) { ",
"Start-Sleep -Milliseconds 20 ",
"}; ",
"[Console]::Out.Write('process-out'); ",
"[Console]::Error.Write('process-err')",
)
.to_string(),
]
} else {
vec![
"sh".to_string(),
"-c".to_string(),
concat!(
"printf process > \"$CODEX_PROCESS_EXEC_PROBE_FILE\"; ",
"while [ ! -e \"$CODEX_PROCESS_EXEC_RELEASE_FILE\" ]; do sleep 0.05; done; ",
"printf process-out; ",
"printf process-err >&2",
)
.to_string(),
]
};
let env = HashMap::from([
(
"CODEX_PROCESS_EXEC_PROBE_FILE".to_string(),
Some(probe_file.display().to_string()),
),
(
"CODEX_PROCESS_EXEC_RELEASE_FILE".to_string(),
Some(release_file.display().to_string()),
),
]);
let (command, env) = probe_release_process(&probe_file, &release_file);
let spawn_request_id = mcp
.send_process_spawn_request(ProcessSpawnParams {
env: Some(env),
@@ -103,6 +65,61 @@ async fn process_spawn_returns_before_exit_and_emits_exit_notification() -> Resu
Ok(())
}
#[tokio::test]
async fn process_spawn_rejects_duplicate_active_handle_without_replacing_original() -> Result<()> {
let codex_home = TempDir::new()?;
let (_server, mut mcp) = initialized_mcp(codex_home.path()).await?;
let process_handle = "duplicate-active-1".to_string();
let probe_file = codex_home.path().join("duplicate-process-created");
let release_file = codex_home.path().join("duplicate-process-release");
let (command, env) = probe_release_process(&probe_file, &release_file);
let spawn_request_id = mcp
.send_process_spawn_request(ProcessSpawnParams {
env: Some(env),
output_bytes_cap: Some(None),
timeout_ms: Some(None),
..process_spawn_params(process_handle.clone(), codex_home.path(), command)?
})
.await?;
let response = mcp
.read_stream_until_response_message(RequestId::Integer(spawn_request_id))
.await?;
assert_eq!(response.result, serde_json::json!({}));
wait_for_file(&probe_file).await?;
assert_eq!(std::fs::read_to_string(&probe_file)?, "process");
let duplicate_request_id = mcp
.send_process_spawn_request(process_spawn_params(
process_handle.clone(),
codex_home.path(),
vec!["codex-process-exec-duplicate-must-not-start".to_string()],
)?)
.await?;
let error = mcp
.read_stream_until_error_message(RequestId::Integer(duplicate_request_id))
.await?;
assert_eq!(
error.error.message,
format!("duplicate active process handle: {process_handle:?}")
);
std::fs::write(&release_file, "release")?;
let exited = read_process_exited(&mut mcp).await?;
assert_eq!(
exited,
ProcessExitedNotification {
process_handle,
exit_code: 0,
stdout: "process-out".to_string(),
stdout_cap_reached: false,
stderr: "process-err".to_string(),
stderr_cap_reached: false,
}
);
Ok(())
}
#[tokio::test]
async fn process_spawn_returns_error_when_local_environment_is_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -230,6 +247,52 @@ async fn process_kill_terminates_running_process() -> Result<()> {
Ok(())
}
fn probe_release_process(
probe_file: &Path,
release_file: &Path,
) -> (Vec<String>, HashMap<String, Option<String>>) {
let command = if cfg!(windows) {
vec![
"powershell.exe".to_string(),
"-NoProfile".to_string(),
"-NonInteractive".to_string(),
"-Command".to_string(),
concat!(
"[IO.File]::WriteAllText($env:CODEX_PROCESS_EXEC_PROBE_FILE, 'process'); ",
"while (!(Test-Path -LiteralPath $env:CODEX_PROCESS_EXEC_RELEASE_FILE)) { ",
"Start-Sleep -Milliseconds 20 ",
"}; ",
"[Console]::Out.Write('process-out'); ",
"[Console]::Error.Write('process-err')",
)
.to_string(),
]
} else {
vec![
"sh".to_string(),
"-c".to_string(),
concat!(
"printf process > \"$CODEX_PROCESS_EXEC_PROBE_FILE\"; ",
"while [ ! -e \"$CODEX_PROCESS_EXEC_RELEASE_FILE\" ]; do sleep 0.05; done; ",
"printf process-out; ",
"printf process-err >&2",
)
.to_string(),
]
};
let env = HashMap::from([
(
"CODEX_PROCESS_EXEC_PROBE_FILE".to_string(),
Some(probe_file.display().to_string()),
),
(
"CODEX_PROCESS_EXEC_RELEASE_FILE".to_string(),
Some(release_file.display().to_string()),
),
]);
(command, env)
}
async fn initialized_mcp(codex_home: &Path) -> Result<(MockServer, TestAppServer)> {
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
create_config_toml(codex_home, &server.uri(), "never")?;