## What changed - The PTY Python REPL test now starts Python with a startup marker already embedded in argv. - The test waits for that marker in PTY output before making assertions. ## Why this fixes the flake - The old version tried to probe the live REPL almost immediately after spawn. - That races PTY initialization, Python startup, and prompt buffering, all of which vary across platforms and CI load. - By having the child process emit a known marker as part of its own startup path, the test gets a deterministic synchronization point that comes from the process under test rather than from guessed timing. ## Scope - Test-only change. |
||
|---|---|---|
| .. | ||
| src | ||
| BUILD.bazel | ||
| Cargo.toml | ||
| README.md | ||
codex-utils-pty
Lightweight helpers for spawning interactive processes either under a PTY (pseudo terminal) or regular pipes. The public API is minimal and mirrors both backends so callers can switch based on their needs (e.g., enabling or disabling TTY).
API surface
spawn_pty_process(program, args, cwd, env, arg0, size)→SpawnedProcessspawn_pipe_process(program, args, cwd, env, arg0)→SpawnedProcessspawn_pipe_process_no_stdin(program, args, cwd, env, arg0)→SpawnedProcesscombine_output_receivers(stdout_rx, stderr_rx)→broadcast::Receiver<Vec<u8>>conpty_supported()→bool(Windows only; always true elsewhere)TerminalSize { rows, cols }selects PTY dimensions in character cells.ProcessHandleexposes:writer_sender()→mpsc::Sender<Vec<u8>>(stdin)resize(TerminalSize)close_stdin()has_exited(),exit_code(),terminate()
SpawnedProcessbundlessession,stdout_rx,stderr_rx, andexit_rx(oneshot exit code).
Usage examples
use std::collections::HashMap;
use std::path::Path;
use codex_utils_pty::combine_output_receivers;
use codex_utils_pty::spawn_pty_process;
use codex_utils_pty::TerminalSize;
# tokio_test::block_on(async {
let env_map: HashMap<String, String> = std::env::vars().collect();
let spawned = spawn_pty_process(
"bash",
&["-lc".into(), "echo hello".into()],
Path::new("."),
&env_map,
&None,
TerminalSize::default(),
).await?;
let writer = spawned.session.writer_sender();
writer.send(b"exit\n".to_vec()).await?;
// Collect output until the process exits.
let mut output_rx = combine_output_receivers(spawned.stdout_rx, spawned.stderr_rx);
let mut collected = Vec::new();
while let Ok(chunk) = output_rx.try_recv() {
collected.extend_from_slice(&chunk);
}
let exit_code = spawned.exit_rx.await.unwrap_or(-1);
# let _ = (collected, exit_code);
# anyhow::Ok(())
# });
Swap in spawn_pipe_process for a non-TTY subprocess; the rest of the API stays the same.
Use spawn_pipe_process_no_stdin to force stdin closed (commands that read stdin will see EOF immediately).
Tests
Unit tests live in src/lib.rs and cover both backends (PTY Python REPL and pipe-based stdin roundtrip). Run with:
cargo test -p codex-utils-pty -- --nocapture