Files
codex/codex-rs/exec-server/src/fs_sandbox_windows_tests.rs
jif a06545b311 Add opt-in history prefix preservation for full agent forks (#52535)
## What changed

Add `preserve_fork_prefix` to the multi-agent v2 configuration, defaulting to `false`. When enabled, full forks retain the parent's model-visible history, including commentary and tool records, and append the child's instructions. Legacy compaction without a reusable context baseline still rebuilds the child context.

Mark copied history as inherited, complete unfinished tool calls in the fork snapshot, and exclude inherited manual approvals from authorization review while retaining them in model context.

## Testing

Add coverage for preserved request prefixes, child instructions, legacy and paginated histories, Code Mode, missing inherited tool outputs, compaction fallback, and approval isolation across checkpoint replay. Update the noisy helper failure test to read its response before starting cleanup.

GitOrigin-RevId: ce9235dfa111c88d6ce66cb93d564fb63b326489
2026-10-09 11:08:24 +00:00

262 lines
9.3 KiB
Rust

#[cfg(windows)]
use std::collections::HashMap;
#[cfg(windows)]
use std::path::Path;
use std::time::Duration;
#[cfg(windows)]
use codex_protocol::config_types::WindowsSandboxLevel;
#[cfg(windows)]
use codex_protocol::models::PermissionProfile;
#[cfg(windows)]
use codex_protocol::sandbox::SandboxOverride;
#[cfg(windows)]
use codex_sandboxing::SandboxExecRequest;
#[cfg(windows)]
use codex_sandboxing::SandboxType;
#[cfg(windows)]
use codex_utils_path_uri::PathUri;
use codex_utils_pty::Command;
use pretty_assertions::assert_eq;
#[cfg(windows)]
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use super::drain_helper_stderr;
use super::read_helper_response;
use super::reap_helper_after_response;
#[cfg(windows)]
use super::run_command;
#[cfg(windows)]
use crate::fs_helper::FsHelperPayload;
#[cfg(windows)]
use crate::protocol::FsReadFileResponse;
#[cfg(windows)]
use crate::protocol::FsWriteFileResponse;
#[tokio::test(start_paused = true)]
async fn filesystem_operation_is_not_limited_by_helper_response_deadline() {
let (reader, mut writer) = tokio::io::duplex(/*max_buf_size*/ 256);
let response = tokio::spawn(async move { read_helper_response(reader).await });
tokio::task::yield_now().await;
tokio::time::advance(Duration::from_secs(/*secs*/ 31)).await;
tokio::task::yield_now().await;
assert!(
!response.is_finished(),
"a filesystem operation must not time out before the helper responds"
);
writer
.write_all(b"completed after the old deadline\n")
.await
.expect("helper response");
assert_eq!(
response
.await
.expect("response task")
.expect("unbounded filesystem operation"),
b"completed after the old deadline\n"
);
}
#[tokio::test]
async fn noisy_failing_helper_preserves_exit_status_and_bounded_stderr() {
#[cfg(unix)]
let mut command = {
let mut command = Command::new("sh");
command.arg("-c").arg(
"printf 'expected helper diagnostic' >&2; i=0; while [ \"$i\" -lt 1024 ]; do printf '%0128d' 0 >&2; i=$((i + 1)); done; printf 'response before failure\\n'; exit 7",
);
command
};
#[cfg(windows)]
let mut command = {
let system_root = std::env::var("SystemRoot").expect("Windows system root");
let powershell = Path::new(&system_root)
.join("System32")
.join("WindowsPowerShell")
.join("v1.0")
.join("powershell.exe");
let mut command = Command::new(powershell);
command
.arg("-NoProfile")
.arg("-Command")
.arg("[Console]::Error.Write('expected helper diagnostic' + ('x' * 131072)); [Console]::Out.WriteLine('response before failure'); exit 7");
command
};
command.envs(std::env::vars_os());
let mut child = command.spawn().expect("noisy helper process");
let stdout = child.stdout.take().expect("helper stdout");
let stderr = drain_helper_stderr(&mut child);
let error = tokio::time::timeout(Duration::from_secs(/*secs*/ 8), async {
// Cleanup starts after the response, not during slow helper startup.
read_helper_response(stdout).await?;
reap_helper_after_response(child, stderr).await
})
.await
.expect("helper stderr must be drained during bounded cleanup")
.expect_err("nonzero helper exit should fail after its stderr pipe fills");
assert!(error.message.contains('7'), "{}", error.message);
assert!(
error.message.contains("expected helper diagnostic"),
"{}",
error.message
);
assert!(error.message.len() < 4400, "{}", error.message.len());
}
#[tokio::test]
async fn helper_stderr_is_drained_before_the_response() {
#[cfg(unix)]
let mut command = {
let mut command = Command::new("sh");
command.arg("-c").arg(
"printf 'expected pre-response diagnostic' >&2; i=0; while [ \"$i\" -lt 1024 ]; do printf '%0128d' 0 >&2; i=$((i + 1)); done; printf 'completed after noisy stderr\\n'",
);
command
};
#[cfg(windows)]
let mut command = {
let system_root = std::env::var("SystemRoot").expect("Windows system root");
let powershell = Path::new(&system_root)
.join("System32")
.join("WindowsPowerShell")
.join("v1.0")
.join("powershell.exe");
let mut command = Command::new(powershell);
command.arg("-NoProfile").arg("-Command").arg(
"[Console]::Error.Write('expected pre-response diagnostic' + ('x' * 131072)); [Console]::Out.WriteLine('completed after noisy stderr')",
);
command
};
command.envs(std::env::vars_os());
let mut child = command.spawn().expect("noisy helper process");
let stdout = child.stdout.take().expect("helper stdout");
let stderr = drain_helper_stderr(&mut child);
let response = tokio::time::timeout(
Duration::from_secs(/*secs*/ 2),
read_helper_response(stdout),
)
.await
.expect("helper stderr must be drained before awaiting its response")
.expect("helper response");
assert_eq!(response.trim_ascii_end(), b"completed after noisy stderr");
reap_helper_after_response(child, stderr)
.await
.expect("noisy helper should be cleaned up after its response");
}
#[cfg(windows)]
#[tokio::test]
async fn completed_windows_image_read_does_not_wait_for_a_stuck_helper() {
let directory = tempfile::tempdir().expect("temporary directory");
let path = directory.path().join("image.png");
let operations = [
(
r#"[IO.File]::WriteAllText($env:CODEX_FS_HELPER_TEST_PATH, 'image')
[Console]::Out.WriteLine('{"status":"ok","payload":{"operation":"fs/writeFile","response":{}}}')"#,
FsHelperPayload::WriteFile(FsWriteFileResponse {}),
),
(
r#"$data = [Convert]::ToBase64String([IO.File]::ReadAllBytes($env:CODEX_FS_HELPER_TEST_PATH))
[Console]::Out.WriteLine('{"status":"ok","payload":{"operation":"fs/readFile","response":{"dataBase64":"' + $data + '"}}}')"#,
FsHelperPayload::ReadFile(FsReadFileResponse {
data_base64: "aW1hZ2U=".to_string(),
}),
),
];
for (operation, expected) in operations {
let script = format!(
"[Console]::In.ReadLine() | Out-Null\n{operation}\n[Console]::Out.Flush()\n[Threading.Thread]::Sleep(30000)"
);
let command = powershell_command(&script, &path).expect("PowerShell helper command");
let result = tokio::time::timeout(
Duration::from_secs(/*secs*/ 8),
run_command(command, b"{}".to_vec()),
)
.await
.expect("the completed operation must not wait for helper termination")
.expect("helper response");
assert_eq!(result, expected);
}
assert_eq!(std::fs::read(&path).expect("created file"), b"image");
}
#[cfg(windows)]
#[tokio::test]
async fn duplicated_windows_file_handle_survives_bounded_helper_cleanup() {
let directory = tempfile::tempdir().expect("temporary directory");
let path = directory.path().join("image.png");
std::fs::write(&path, b"image").expect("image file");
let command = powershell_command(
r#"[Console]::In.ReadLine() | Out-Null
$file = [IO.File]::OpenRead($env:CODEX_FS_HELPER_TEST_PATH)
$handle = $file.SafeFileHandle.DangerousGetHandle().ToInt64()
[Console]::Out.WriteLine('{"status":"ok","payload":{"operation":"fs/open","response":{"processId":' + $PID + ',"fileHandle":' + $handle + '}}}')
[Console]::Out.Flush()
[Threading.Thread]::Sleep(30000)"#,
&path,
)
.expect("PowerShell helper command");
let mut file = tokio::time::timeout(
Duration::from_secs(/*secs*/ 8),
crate::sandboxed_file_open::open(
command,
PathUri::from_host_native_path(&path).expect("image path URI"),
crate::protocol::FsOpenMode::Read,
),
)
.await
.expect("the opened file must not wait for helper termination")
.expect("duplicated file handle");
let mut data = Vec::new();
file.read_to_end(&mut data).await.expect("image contents");
assert_eq!(data, b"image");
}
#[cfg(windows)]
fn powershell_command(script: &str, path: &Path) -> anyhow::Result<SandboxExecRequest> {
let system_root = std::env::var("SystemRoot")?;
let powershell = Path::new(&system_root)
.join("System32")
.join("WindowsPowerShell")
.join("v1.0")
.join("powershell.exe");
let cwd = PathUri::from_host_native_path(std::env::current_dir()?)?;
Ok(SandboxExecRequest {
sandbox_override: SandboxOverride::NoOverride,
command: vec![
powershell.to_string_lossy().into_owned(),
"-NoProfile".to_string(),
"-Command".to_string(),
script.to_string(),
],
cwd: cwd.clone(),
sandbox_policy_cwd: cwd,
env: HashMap::from([
("SystemRoot".to_string(), system_root),
(
"CODEX_FS_HELPER_TEST_PATH".to_string(),
path.to_string_lossy().into_owned(),
),
]),
network: None,
network_environment_id: None,
sandbox: SandboxType::None,
windows_sandbox_level: WindowsSandboxLevel::Disabled,
permission_profile: PermissionProfile::Disabled,
arg0: None,
})
}