mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
686 lines
23 KiB
Rust
686 lines
23 KiB
Rust
#![cfg(target_os = "windows")]
|
|
|
|
use super::spawn_windows_sandbox_session_legacy;
|
|
use crate::acl::add_allow_ace;
|
|
use crate::cap::load_or_create_cap_sids;
|
|
use crate::cap::workspace_cap_sid_for_cwd;
|
|
use crate::ipc_framed::Message;
|
|
use crate::ipc_framed::decode_bytes;
|
|
use crate::ipc_framed::read_frame;
|
|
use crate::run_windows_sandbox_capture;
|
|
use crate::spawn_prep::LocalSid;
|
|
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::Mutex;
|
|
use std::sync::MutexGuard;
|
|
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 = codex_home.join(".sandbox").join("sandbox.log");
|
|
fs::read_to_string(&log_path)
|
|
.unwrap_or_else(|err| format!("failed to read {}: {err}", log_path.display()))
|
|
}
|
|
|
|
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 spawned = spawn_windows_sandbox_session_legacy(
|
|
"workspace-write",
|
|
cwd.as_path(),
|
|
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_honors_deny_read_overrides() {
|
|
let _guard = legacy_process_test_guard();
|
|
let runtime = current_thread_runtime();
|
|
runtime.block_on(async move {
|
|
let codex_home = sandbox_home("legacy-non-tty-deny-read");
|
|
let fixture_id = TEST_HOME_COUNTER.fetch_add(1, Ordering::Relaxed);
|
|
let fixture_dir = codex_home
|
|
.path()
|
|
.join(format!("legacy-non-tty-deny-read-fixture-{fixture_id}"));
|
|
let _ = fs::remove_dir_all(&fixture_dir);
|
|
let secret_path = fixture_dir.join("secret.env");
|
|
let public_path = fixture_dir.join("public.txt");
|
|
fs::create_dir_all(&fixture_dir).expect("create deny-read fixture");
|
|
fs::write(&secret_path, "secret denied").expect("write secret");
|
|
fs::write(&public_path, "public allowed").expect("write public");
|
|
|
|
let caps = load_or_create_cap_sids(codex_home.path()).expect("load caps");
|
|
let generic_sid = LocalSid::from_string(&caps.workspace).expect("generic workspace SID");
|
|
let workspace_sid = LocalSid::from_string(
|
|
&workspace_cap_sid_for_cwd(codex_home.path(), fixture_dir.as_path())
|
|
.expect("workspace SID string"),
|
|
)
|
|
.expect("workspace SID");
|
|
unsafe {
|
|
for path in [&fixture_dir, &secret_path, &public_path] {
|
|
add_allow_ace(path, generic_sid.as_ptr()).expect("allow generic workspace SID");
|
|
add_allow_ace(path, workspace_sid.as_ptr()).expect("allow workspace SID");
|
|
}
|
|
}
|
|
|
|
let public_read_without_deny = spawn_windows_sandbox_session_legacy(
|
|
"workspace-write",
|
|
fixture_dir.as_path(),
|
|
codex_home.path(),
|
|
vec![
|
|
"C:\\Windows\\System32\\cmd.exe".to_string(),
|
|
"/c".to_string(),
|
|
"type \"public.txt\" 2>&1".to_string(),
|
|
],
|
|
fixture_dir.as_path(),
|
|
HashMap::new(),
|
|
Some(5_000),
|
|
&[],
|
|
&[],
|
|
/*tty*/ false,
|
|
/*stdin_open*/ false,
|
|
/*use_private_desktop*/ true,
|
|
)
|
|
.await
|
|
.expect("spawn legacy public read control session");
|
|
let (stdout, exit_code) = collect_stdout_and_exit(
|
|
public_read_without_deny,
|
|
codex_home.path(),
|
|
Duration::from_secs(10),
|
|
)
|
|
.await;
|
|
let stdout = String::from_utf8_lossy(&stdout);
|
|
assert_eq!(exit_code, 0, "control stdout={stdout:?}");
|
|
assert!(
|
|
stdout.contains("public allowed"),
|
|
"control stdout={stdout:?}"
|
|
);
|
|
|
|
let public_read = spawn_windows_sandbox_session_legacy(
|
|
"workspace-write",
|
|
fixture_dir.as_path(),
|
|
codex_home.path(),
|
|
vec![
|
|
"C:\\Windows\\System32\\cmd.exe".to_string(),
|
|
"/c".to_string(),
|
|
"type \"public.txt\" 2>&1".to_string(),
|
|
],
|
|
fixture_dir.as_path(),
|
|
HashMap::new(),
|
|
Some(5_000),
|
|
std::slice::from_ref(&secret_path),
|
|
&[],
|
|
/*tty*/ false,
|
|
/*stdin_open*/ false,
|
|
/*use_private_desktop*/ true,
|
|
)
|
|
.await
|
|
.expect("spawn legacy public read session");
|
|
let (stdout, exit_code) =
|
|
collect_stdout_and_exit(public_read, codex_home.path(), Duration::from_secs(10)).await;
|
|
let stdout = String::from_utf8_lossy(&stdout);
|
|
assert_eq!(exit_code, 0, "deny stdout={stdout:?}");
|
|
assert!(stdout.contains("public allowed"), "stdout={stdout:?}");
|
|
|
|
let secret_read = spawn_windows_sandbox_session_legacy(
|
|
"workspace-write",
|
|
fixture_dir.as_path(),
|
|
codex_home.path(),
|
|
vec![
|
|
"C:\\Windows\\System32\\cmd.exe".to_string(),
|
|
"/c".to_string(),
|
|
"type \"secret.env\" 2>NUL".to_string(),
|
|
],
|
|
fixture_dir.as_path(),
|
|
HashMap::new(),
|
|
Some(5_000),
|
|
std::slice::from_ref(&secret_path),
|
|
&[],
|
|
/*tty*/ false,
|
|
/*stdin_open*/ false,
|
|
/*use_private_desktop*/ true,
|
|
)
|
|
.await
|
|
.expect("spawn legacy secret read session");
|
|
let (stdout, _) =
|
|
collect_stdout_and_exit(secret_read, codex_home.path(), Duration::from_secs(10)).await;
|
|
let stdout = String::from_utf8_lossy(&stdout);
|
|
assert!(!stdout.contains("secret denied"), "stdout={stdout:?}");
|
|
|
|
let _ = fs::remove_dir_all(&fixture_dir);
|
|
});
|
|
}
|
|
|
|
#[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 spawned = spawn_windows_sandbox_session_legacy(
|
|
"workspace-write",
|
|
cwd.as_path(),
|
|
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 result = run_windows_sandbox_capture(
|
|
"workspace-write",
|
|
cwd.as_path(),
|
|
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),
|
|
/*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_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 spawned = spawn_windows_sandbox_session_legacy(
|
|
"workspace-write",
|
|
cwd.as_path(),
|
|
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 spawned = spawn_windows_sandbox_session_legacy(
|
|
"workspace-write",
|
|
cwd.as_path(),
|
|
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 spawned = spawn_windows_sandbox_session_legacy(
|
|
"workspace-write",
|
|
cwd.as_path(),
|
|
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:?}");
|
|
});
|
|
}
|