Codex Handbook
windows-sandbox-rs/src/unified_exec/tests.rs 668 lines
#![cfg(target_os = "windows")]use super::spawn_windows_sandbox_session_legacy;use crate::WindowsSandboxCancellationToken;use crate::ipc_framed::Message;use crate::ipc_framed::decode_bytes;use crate::ipc_framed::read_frame;use crate::run_windows_sandbox_capture;use codex_protocol::models::PermissionProfile;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_pty::ProcessDriver;use pretty_assertions::assert_eq;use std::collections::HashMap;use std::fs;use std::fs::OpenOptions;use std::io::Seek;use std::io::SeekFrom;use std::path::Path;use std::path::PathBuf;use std::sync::Arc;use std::sync::Mutex;use std::sync::MutexGuard;use std::sync::atomic::AtomicBool;use std::sync::atomic::AtomicU64;use std::sync::atomic::Ordering;use std::time::Duration;use std::time::Instant;use tempfile::TempDir;use tokio::runtime::Builder;use tokio::sync::broadcast;use tokio::sync::mpsc;use tokio::sync::oneshot;use tokio::time::timeout;static TEST_HOME_COUNTER: AtomicU64 = AtomicU64::new(0);static LEGACY_PROCESS_TEST_LOCK: Mutex<()> = Mutex::new(());fn legacy_process_test_guard() -> MutexGuard<'static, ()> {    LEGACY_PROCESS_TEST_LOCK        .lock()        .expect("legacy Windows sandbox process test lock poisoned")}fn current_thread_runtime() -> tokio::runtime::Runtime {    Builder::new_current_thread()        .enable_all()        .build()        .expect("build tokio runtime")}fn pwsh_path() -> Option<PathBuf> {    let program_files = std::env::var_os("ProgramFiles")?;    let path = PathBuf::from(program_files).join("PowerShell\\7\\pwsh.exe");    path.is_file().then_some(path)}fn sandbox_cwd() -> PathBuf {    if let Ok(workspace_root) = std::env::var("INSTA_WORKSPACE_ROOT") {        return PathBuf::from(workspace_root);    }    PathBuf::from(env!("CARGO_MANIFEST_DIR"))        .parent()        .expect("repo root")        .to_path_buf()}fn sandbox_home(name: &str) -> TempDir {    let id = TEST_HOME_COUNTER.fetch_add(1, Ordering::Relaxed);    let path = std::env::temp_dir().join(format!("codex-windows-sandbox-{name}-{id}"));    let _ = fs::remove_dir_all(&path);    fs::create_dir_all(&path).expect("create sandbox home");    tempfile::TempDir::new_in(&path).expect("create sandbox home tempdir")}fn sandbox_log(codex_home: &Path) -> String {    let log_path = crate::current_log_file_path(&codex_home.join(".sandbox"));    fs::read_to_string(&log_path)        .unwrap_or_else(|err| format!("failed to read {}: {err}", log_path.display()))}fn workspace_roots_for(root: &Path) -> Vec<AbsolutePathBuf> {    vec![AbsolutePathBuf::from_absolute_path(root).expect("absolute workspace root")]}fn wait_for_frame_count(frames_path: &Path, expected_frames: usize) -> Vec<Message> {    let deadline = Instant::now() + Duration::from_secs(2);    loop {        let mut reader = OpenOptions::new()            .read(true)            .open(frames_path)            .expect("open frame file for read");        reader            .seek(SeekFrom::Start(0))            .expect("seek to start of frame file");        let mut frames = Vec::new();        loop {            match read_frame(&mut reader) {                Ok(Some(frame)) => frames.push(frame.message),                Ok(None) => break,                Err(_) => break,            }        }        if frames.len() >= expected_frames {            return frames;        }        assert!(            Instant::now() < deadline,            "timed out waiting for {expected_frames} frames, saw {}",            frames.len()        );        std::thread::sleep(Duration::from_millis(10));    }}async fn collect_stdout_and_exit(    spawned: codex_utils_pty::SpawnedProcess,    codex_home: &Path,    timeout_duration: Duration,) -> (Vec<u8>, i32) {    let codex_utils_pty::SpawnedProcess {        session: _session,        mut stdout_rx,        stderr_rx: _stderr_rx,        exit_rx,    } = spawned;    let stdout_task = tokio::spawn(async move {        let mut stdout = Vec::new();        while let Some(chunk) = stdout_rx.recv().await {            stdout.extend(chunk);        }        stdout    });    let exit_code = timeout(timeout_duration, exit_rx)        .await        .unwrap_or_else(|_| panic!("timed out waiting for exit\n{}", sandbox_log(codex_home)))        .unwrap_or(-1);    let stdout = timeout(timeout_duration, stdout_task)        .await        .unwrap_or_else(|_| {            panic!(                "timed out waiting for stdout task\n{}",                sandbox_log(codex_home)            )        })        .expect("stdout task join");    (stdout, exit_code)}#[test]fn legacy_non_tty_cmd_emits_output() {    let _guard = legacy_process_test_guard();    let runtime = current_thread_runtime();    runtime.block_on(async move {        let cwd = sandbox_cwd();        let codex_home = sandbox_home("legacy-non-tty-cmd");        println!("cmd codex_home={}", codex_home.path().display());        let permission_profile = PermissionProfile::workspace_write();        let spawned = spawn_windows_sandbox_session_legacy(            &permission_profile,            workspace_roots_for(cwd.as_path()).as_slice(),            codex_home.path(),            vec![                "C:\\Windows\\System32\\cmd.exe".to_string(),                "/c".to_string(),                "echo LEGACY-NONTTY-CMD".to_string(),            ],            cwd.as_path(),            HashMap::new(),            Some(5_000),            &[],            &[],            /*tty*/ false,            /*stdin_open*/ false,            /*use_private_desktop*/ true,        )        .await        .expect("spawn legacy non-tty cmd session");        println!("cmd spawn returned");        let (stdout, exit_code) =            collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(10)).await;        println!("cmd collect returned exit_code={exit_code}");        let stdout = String::from_utf8_lossy(&stdout);        assert_eq!(exit_code, 0, "stdout={stdout:?}");        assert!(stdout.contains("LEGACY-NONTTY-CMD"), "stdout={stdout:?}");    });}#[test]fn legacy_non_tty_cmd_rejects_deny_read_overrides() {    let _guard = legacy_process_test_guard();    let runtime = current_thread_runtime();    runtime.block_on(async move {        let cwd = sandbox_cwd();        let codex_home = sandbox_home("legacy-non-tty-deny-read");        let secret_path =            AbsolutePathBuf::from_absolute_path(cwd.join("legacy-non-tty-deny-read-secret.env"))                .expect("absolute deny-read fixture path");        let permission_profile = PermissionProfile::workspace_write();        let err = spawn_windows_sandbox_session_legacy(            &permission_profile,            workspace_roots_for(cwd.as_path()).as_slice(),            codex_home.path(),            vec![                "C:\\Windows\\System32\\cmd.exe".to_string(),                "/c".to_string(),                "echo deny-read".to_string(),            ],            cwd.as_path(),            HashMap::new(),            Some(5_000),            std::slice::from_ref(&secret_path),            &[],            /*tty*/ false,            /*stdin_open*/ false,            /*use_private_desktop*/ true,        )        .await        .expect_err("legacy deny-read should require the elevated backend");        assert!(            err.to_string()                .contains("deny-read overrides require the elevated Windows sandbox backend"),            "unexpected error: {err:#}"        );    });}#[test]fn legacy_non_tty_powershell_emits_output() {    let Some(pwsh) = pwsh_path() else {        return;    };    let _guard = legacy_process_test_guard();    let runtime = current_thread_runtime();    runtime.block_on(async move {        let cwd = sandbox_cwd();        let codex_home = sandbox_home("legacy-non-tty-pwsh");        println!("pwsh codex_home={}", codex_home.path().display());        let permission_profile = PermissionProfile::workspace_write();        let spawned = spawn_windows_sandbox_session_legacy(            &permission_profile,            workspace_roots_for(cwd.as_path()).as_slice(),            codex_home.path(),            vec![                pwsh.display().to_string(),                "-NoProfile".to_string(),                "-Command".to_string(),                "Write-Output LEGACY-NONTTY-DIRECT".to_string(),            ],            cwd.as_path(),            HashMap::new(),            Some(5_000),            &[],            &[],            /*tty*/ false,            /*stdin_open*/ false,            /*use_private_desktop*/ true,        )        .await        .expect("spawn legacy non-tty powershell session");        println!("pwsh spawn returned");        let (stdout, exit_code) =            collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(10)).await;        println!("pwsh collect returned exit_code={exit_code}");        let stdout = String::from_utf8_lossy(&stdout);        assert_eq!(exit_code, 0, "stdout={stdout:?}");        assert!(stdout.contains("LEGACY-NONTTY-DIRECT"), "stdout={stdout:?}");    });}#[test]fn finish_driver_spawn_keeps_stdin_open_when_requested() {    let runtime = current_thread_runtime();    runtime.block_on(async move {        let (writer_tx, mut writer_rx) = mpsc::channel::<Vec<u8>>(1);        let (_stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(1);        let (exit_tx, exit_rx) = oneshot::channel::<i32>();        drop(exit_tx);        let spawned = super::finish_driver_spawn(            ProcessDriver {                writer_tx,                stdout_rx,                stderr_rx: None,                exit_rx,                terminator: None,                writer_handle: None,                resizer: None,            },            /*stdin_open*/ true,        );        spawned            .session            .writer_sender()            .send(b"open".to_vec())            .await            .expect("stdin should stay open");        assert_eq!(writer_rx.recv().await, Some(b"open".to_vec()));    });}#[test]fn finish_driver_spawn_closes_stdin_when_not_requested() {    let runtime = current_thread_runtime();    runtime.block_on(async move {        let (writer_tx, _writer_rx) = mpsc::channel::<Vec<u8>>(1);        let (_stdout_tx, stdout_rx) = broadcast::channel::<Vec<u8>>(1);        let (exit_tx, exit_rx) = oneshot::channel::<i32>();        drop(exit_tx);        let spawned = super::finish_driver_spawn(            ProcessDriver {                writer_tx,                stdout_rx,                stderr_rx: None,                exit_rx,                terminator: None,                writer_handle: None,                resizer: None,            },            /*stdin_open*/ false,        );        assert!(            spawned                .session                .writer_sender()                .send(b"closed".to_vec())                .await                .is_err(),            "stdin should be closed when streaming input is disabled"        );    });}#[test]fn runner_stdin_writer_sends_close_stdin_after_input_eof() {    let runtime = current_thread_runtime();    runtime.block_on(async move {        let tempdir = TempDir::new().expect("create tempdir");        let frames_path = tempdir.path().join("runner-stdin-frames.bin");        let file = OpenOptions::new()            .create(true)            .truncate(true)            .read(true)            .write(true)            .open(&frames_path)            .expect("create frame file");        let outbound_tx = super::start_runner_pipe_writer(file);        let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(1);        let writer_handle = super::start_runner_stdin_writer(            writer_rx,            outbound_tx,            /*normalize_newlines*/ false,            /*stdin_open*/ true,        );        writer_tx            .send(b"hello".to_vec())            .await            .expect("send stdin bytes");        drop(writer_tx);        writer_handle.await.expect("join stdin writer");        let frames = wait_for_frame_count(&frames_path, 2);        match &frames[0] {            Message::Stdin { payload } => {                let bytes = decode_bytes(&payload.data_b64).expect("decode stdin payload");                assert_eq!(bytes, b"hello".to_vec());            }            other => panic!("expected stdin frame, got {other:?}"),        }        match &frames[1] {            Message::CloseStdin { .. } => {}            other => panic!("expected close-stdin frame, got {other:?}"),        }    });}#[test]fn runner_resizer_sends_resize_frame() {    let runtime = current_thread_runtime();    runtime.block_on(async move {        let tempdir = TempDir::new().expect("create tempdir");        let frames_path = tempdir.path().join("runner-resize-frames.bin");        let file = OpenOptions::new()            .create(true)            .truncate(true)            .read(true)            .write(true)            .open(&frames_path)            .expect("create frame file");        let outbound_tx = super::start_runner_pipe_writer(file);        let mut resizer = super::make_runner_resizer(outbound_tx);        resizer(codex_utils_pty::TerminalSize {            rows: 45,            cols: 132,        })        .expect("send resize frame");        let frames = wait_for_frame_count(&frames_path, 1);        match &frames[0] {            Message::Resize { payload } => {                assert_eq!(payload.rows, 45);                assert_eq!(payload.cols, 132);            }            other => panic!("expected resize frame, got {other:?}"),        }    });}#[test]fn legacy_capture_powershell_emits_output() {    let Some(pwsh) = pwsh_path() else {        return;    };    let _guard = legacy_process_test_guard();    let cwd = sandbox_cwd();    let codex_home = sandbox_home("legacy-capture-pwsh");    println!("capture pwsh codex_home={}", codex_home.path().display());    let permission_profile = PermissionProfile::workspace_write();    let result = run_windows_sandbox_capture(        &permission_profile,        workspace_roots_for(cwd.as_path()).as_slice(),        codex_home.path(),        vec![            pwsh.display().to_string(),            "-NoProfile".to_string(),            "-Command".to_string(),            "Write-Output LEGACY-CAPTURE-DIRECT".to_string(),        ],        cwd.as_path(),        HashMap::new(),        Some(10_000),        /*cancellation*/ None,        /*use_private_desktop*/ true,    )    .expect("run legacy capture powershell");    println!("capture pwsh exit_code={}", result.exit_code);    println!("capture pwsh timed_out={}", result.timed_out);    let stdout = String::from_utf8_lossy(&result.stdout);    let stderr = String::from_utf8_lossy(&result.stderr);    println!("capture pwsh stderr={stderr:?}");    assert_eq!(result.exit_code, 0, "stdout={stdout:?} stderr={stderr:?}");    assert!(        stdout.contains("LEGACY-CAPTURE-DIRECT"),        "stdout={stdout:?}"    );}#[test]fn legacy_capture_cancellation_is_not_reported_as_timeout() {    let Some(pwsh) = pwsh_path() else {        eprintln!("skipping cancellation regression test: PowerShell 7 is not installed");        return;    };    let _guard = legacy_process_test_guard();    let cwd = sandbox_cwd();    let codex_home = sandbox_home("legacy-capture-cancel");    let permission_profile = PermissionProfile::workspace_write();    let cancelled = Arc::new(AtomicBool::new(false));    let cancelled_for_token = Arc::clone(&cancelled);    let cancellation =        WindowsSandboxCancellationToken::new(move || cancelled_for_token.load(Ordering::SeqCst));    let cancelled_for_thread = Arc::clone(&cancelled);    let cancel_thread = std::thread::spawn(move || {        std::thread::sleep(Duration::from_millis(200));        cancelled_for_thread.store(true, Ordering::SeqCst);    });    let started_at = Instant::now();    let result = run_windows_sandbox_capture(        &permission_profile,        workspace_roots_for(cwd.as_path()).as_slice(),        codex_home.path(),        vec![            pwsh.display().to_string(),            "-NoProfile".to_string(),            "-Command".to_string(),            "Start-Sleep -Seconds 30".to_string(),        ],        cwd.as_path(),        HashMap::new(),        Some(30_000),        /*cancellation*/ Some(cancellation),        /*use_private_desktop*/ true,    )    .expect("run legacy capture powershell with cancellation");    cancel_thread.join().expect("cancel thread should finish");    assert!(        started_at.elapsed() < Duration::from_secs(10),        "cancellation should end capture before the timeout"    );    assert!(        !result.timed_out,        "cancellation should not be reported as a timeout"    );    assert_ne!(result.exit_code, 0);}#[test]fn legacy_tty_powershell_emits_output_and_accepts_input() {    let Some(pwsh) = pwsh_path() else {        return;    };    let _guard = legacy_process_test_guard();    let runtime = current_thread_runtime();    runtime.block_on(async move {        let cwd = sandbox_cwd();        let codex_home = sandbox_home("legacy-tty-pwsh");        println!("tty pwsh codex_home={}", codex_home.path().display());        let permission_profile = PermissionProfile::workspace_write();        let spawned = spawn_windows_sandbox_session_legacy(            &permission_profile,            workspace_roots_for(cwd.as_path()).as_slice(),            codex_home.path(),            vec![                pwsh.display().to_string(),                "-NoLogo".to_string(),                "-NoProfile".to_string(),                "-NoExit".to_string(),                "-Command".to_string(),                "$PID; Write-Output ready".to_string(),            ],            cwd.as_path(),            HashMap::new(),            Some(10_000),            &[],            &[],            /*tty*/ true,            /*stdin_open*/ true,            /*use_private_desktop*/ true,        )        .await        .expect("spawn legacy tty powershell session");        println!("tty pwsh spawn returned");        let writer = spawned.session.writer_sender();        writer            .send(b"Write-Output second\n".to_vec())            .await            .expect("send second command");        writer            .send(b"exit\n".to_vec())            .await            .expect("send exit command");        spawned.session.close_stdin();        let (stdout, exit_code) =            collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await;        let stdout = String::from_utf8_lossy(&stdout);        assert_eq!(exit_code, 0, "stdout={stdout:?}");        assert!(stdout.contains("ready"), "stdout={stdout:?}");        assert!(stdout.contains("second"), "stdout={stdout:?}");    });}#[test]#[ignore = "TODO: legacy ConPTY cmd.exe exits with STATUS_DLL_INIT_FAILED in CI"]fn legacy_tty_cmd_emits_output_and_accepts_input() {    let runtime = current_thread_runtime();    runtime.block_on(async move {        let cwd = sandbox_cwd();        let codex_home = sandbox_home("legacy-tty-cmd");        println!("tty cmd codex_home={}", codex_home.path().display());        let permission_profile = PermissionProfile::workspace_write();        let spawned = spawn_windows_sandbox_session_legacy(            &permission_profile,            workspace_roots_for(cwd.as_path()).as_slice(),            codex_home.path(),            vec![                "C:\\Windows\\System32\\cmd.exe".to_string(),                "/K".to_string(),                "echo ready".to_string(),            ],            cwd.as_path(),            HashMap::new(),            Some(10_000),            &[],            &[],            /*tty*/ true,            /*stdin_open*/ true,            /*use_private_desktop*/ true,        )        .await        .expect("spawn legacy tty cmd session");        println!("tty cmd spawn returned");        let writer = spawned.session.writer_sender();        writer            .send(b"echo second\n".to_vec())            .await            .expect("send second command");        writer            .send(b"exit\n".to_vec())            .await            .expect("send exit command");        spawned.session.close_stdin();        let (stdout, exit_code) =            collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await;        let stdout = String::from_utf8_lossy(&stdout);        assert_eq!(exit_code, 0, "stdout={stdout:?}");        assert!(stdout.contains("ready"), "stdout={stdout:?}");        assert!(stdout.contains("second"), "stdout={stdout:?}");    });}#[test]#[ignore = "TODO: legacy ConPTY cmd.exe exits with STATUS_DLL_INIT_FAILED in CI"]fn legacy_tty_cmd_default_desktop_emits_output_and_accepts_input() {    let runtime = current_thread_runtime();    runtime.block_on(async move {        let cwd = sandbox_cwd();        let codex_home = sandbox_home("legacy-tty-cmd-default-desktop");        println!(            "tty cmd default desktop codex_home={}",            codex_home.path().display()        );        let permission_profile = PermissionProfile::workspace_write();        let spawned = spawn_windows_sandbox_session_legacy(            &permission_profile,            workspace_roots_for(cwd.as_path()).as_slice(),            codex_home.path(),            vec![                "C:\\Windows\\System32\\cmd.exe".to_string(),                "/K".to_string(),                "echo ready".to_string(),            ],            cwd.as_path(),            HashMap::new(),            Some(10_000),            &[],            &[],            /*tty*/ true,            /*stdin_open*/ true,            /*use_private_desktop*/ false,        )        .await        .expect("spawn legacy tty cmd session");        println!("tty cmd default desktop spawn returned");        let writer = spawned.session.writer_sender();        writer            .send(b"echo second\n".to_vec())            .await            .expect("send second command");        writer            .send(b"exit\n".to_vec())            .await            .expect("send exit command");        spawned.session.close_stdin();        let (stdout, exit_code) =            collect_stdout_and_exit(spawned, codex_home.path(), Duration::from_secs(15)).await;        let stdout = String::from_utf8_lossy(&stdout);        assert_eq!(exit_code, 0, "stdout={stdout:?}");        assert!(stdout.contains("ready"), "stdout={stdout:?}");        assert!(stdout.contains("second"), "stdout={stdout:?}");    });}