Remove CODEX_RS_SSE_FIXTURE test hook (#22413)

## Why

`CODEX_RS_SSE_FIXTURE` let integration-style CLI, exec, and TUI tests
bypass the normal Responses transport by reading SSE from local files.
That kept test-only behavior wired through production client code. The
affected tests can stay hermetic by using the existing
`core_test_support::responses` mock server and passing `openai_base_url`
instead.

## What Changed

- Removed the `CODEX_RS_SSE_FIXTURE` flag,
`codex_api::stream_from_fixture`, the `env-flags` dependency, and the
checked-in SSE fixture files.
- Repointed the affected core, exec, and TUI tests at `MockServer` with
the existing SSE event constructors.
- Removed the Bazel test data plumbing for the deleted fixtures and
refreshed cargo/Bazel lock state.

## Verification

- `cargo build -p codex-cli`
- `cargo test -p codex-api`
- `cargo test -p codex-core --test all responses_api_stream_cli`
- `cargo test -p codex-core --test all
integration_creates_and_checks_session_file`
- `cargo test -p codex-exec --test all ephemeral`
- `cargo test -p codex-exec --test all resume`
- `cargo test -p codex-tui --test all
resume_startup_does_not_consume_model_availability_nux_count`
- `just bazel-lock-update`
- `just bazel-lock-check`
- `just fix -p codex-api -p codex-core -p codex-exec -p codex-tui`
- `git diff --check`
This commit is contained in:
pakrym-oai
2026-05-12 20:08:01 -07:00
committed by GitHub
Unverified
parent 913aad4d3c
commit 96833c5b15
23 changed files with 198 additions and 249 deletions
-1
View File
@@ -887,7 +887,6 @@
"enum-as-inner_0.6.1": "{\"dependencies\":[{\"name\":\"heck\",\"req\":\"^0.5\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}",
"enumflags2_0.7.12": "{\"dependencies\":[{\"name\":\"enumflags2_derive\",\"req\":\"=0.7.12\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.0\"}],\"features\":{\"std\":[]}}",
"enumflags2_derive_0.7.12": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"parsing\",\"printing\",\"derive\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}",
"env-flags_0.1.1": "{\"dependencies\":[],\"features\":{}}",
"env_filter_1.0.0": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"log\",\"req\":\"^0.4.8\"},{\"default_features\":false,\"features\":[\"std\",\"perf\"],\"name\":\"regex\",\"optional\":true,\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6\"}],\"features\":{\"default\":[\"regex\"],\"regex\":[\"dep:regex\"]}}",
"env_filter_1.0.1": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"log\",\"req\":\"^0.4.29\"},{\"default_features\":false,\"features\":[\"std\",\"perf\"],\"name\":\"regex\",\"optional\":true,\"req\":\"^1.12.3\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"regex\"],\"regex\":[\"dep:regex\"]}}",
"env_home_0.1.0": "{\"dependencies\":[],\"features\":{}}",
+2 -7
View File
@@ -2536,7 +2536,6 @@ dependencies = [
"ctor 0.6.3",
"dirs",
"dunce",
"env-flags",
"eventsource-stream",
"futures",
"http 1.4.0",
@@ -3791,6 +3790,7 @@ dependencies = [
"codex-utils-string",
"codex-windows-sandbox",
"color-eyre",
"core_test_support",
"cpal",
"crossterm",
"derive_more 2.1.1",
@@ -3841,6 +3841,7 @@ dependencies = [
"which 8.0.0",
"windows-sys 0.52.0",
"winsplit",
"wiremock",
]
[[package]]
@@ -5389,12 +5390,6 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "env-flags"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbfd0e7fc632dec5e6c9396a27bc9f9975b4e039720e1fd3e34021d3ce28c415"
[[package]]
name = "env_filter"
version = "1.0.0"
-1
View File
@@ -280,7 +280,6 @@ dotenvy = "0.15.7"
dunce = "1.0.4"
ed25519-dalek = { version = "2.2.0", features = ["pkcs8"] }
encoding_rs = "0.8.35"
env-flags = "0.1.1"
env_logger = "0.11.9"
eventsource-stream = "0.2.3"
flate2 = "1.1.8"
-1
View File
@@ -63,7 +63,6 @@ pub use crate::provider::Provider;
pub use crate::provider::RetryConfig;
pub use crate::provider::is_azure_responses_provider;
pub use crate::requests::Compression;
pub use crate::sse::stream_from_fixture;
pub use crate::telemetry::SseTelemetry;
pub use crate::telemetry::WebsocketTelemetry;
pub use codex_protocol::protocol::RealtimeAudioFrame;
-1
View File
@@ -3,4 +3,3 @@ pub(crate) mod responses;
pub(crate) use responses::ResponsesStreamEvent;
pub(crate) use responses::process_responses_event;
pub use responses::spawn_response_stream;
pub use responses::stream_from_fixture;
+3 -34
View File
@@ -5,24 +5,19 @@ use crate::rate_limits::parse_all_rate_limits;
use crate::telemetry::SseTelemetry;
use codex_client::ByteStream;
use codex_client::StreamResponse;
use codex_client::TransportError;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::ModelVerification;
use codex_protocol::protocol::TokenUsage;
use eventsource_stream::Eventsource;
use futures::StreamExt;
use futures::TryStreamExt;
use serde::Deserialize;
use serde_json::Value;
use std::io::BufRead;
use std::path::Path;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time::Instant;
use tokio::time::timeout;
use tokio_util::io::ReaderStream;
use tracing::debug;
use tracing::trace;
@@ -31,35 +26,6 @@ const OPENAI_MODEL_HEADER: &str = "openai-model";
const REQUEST_ID_HEADER: &str = "x-request-id";
const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION: &str = "trusted_access_for_cyber";
/// Streams SSE events from an on-disk fixture for tests.
pub fn stream_from_fixture(
path: impl AsRef<Path>,
idle_timeout: Duration,
) -> Result<ResponseStream, ApiError> {
let file =
std::fs::File::open(path.as_ref()).map_err(|err| ApiError::Stream(err.to_string()))?;
let mut content = String::new();
for line in std::io::BufReader::new(file).lines() {
let line = line.map_err(|err| ApiError::Stream(err.to_string()))?;
content.push_str(&line);
content.push_str("\n\n");
}
let reader = std::io::Cursor::new(content);
let stream = ReaderStream::new(reader).map_err(|err| TransportError::Network(err.to_string()));
let (tx_event, rx_event) = mpsc::channel::<Result<ResponseEvent, ApiError>>(1600);
tokio::spawn(process_sse(
Box::pin(stream),
tx_event,
idle_timeout,
/*telemetry*/ None,
));
Ok(ResponseStream {
rx_event,
upstream_request_id: None,
})
}
pub fn spawn_response_stream(
stream_response: StreamResponse,
idle_timeout: Duration,
@@ -593,8 +559,10 @@ mod tests {
use assert_matches::assert_matches;
use bytes::Bytes;
use codex_client::StreamResponse;
use codex_client::TransportError;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::ResponseItem;
use futures::TryStreamExt;
use futures::stream;
use http::HeaderMap;
use http::HeaderValue;
@@ -603,6 +571,7 @@ mod tests {
use serde_json::json;
use tokio::sync::mpsc;
use tokio_test::io::Builder as IoBuilder;
use tokio_util::io::ReaderStream;
async fn collect_events(chunks: &[&[u8]]) -> Vec<Result<ResponseEvent, ApiError>> {
let mut builder = IoBuilder::new();
-8
View File
@@ -1,13 +1,5 @@
load("//:defs.bzl", "codex_rust_crate")
filegroup(
name = "model_availability_nux_fixtures",
srcs = [
"tests/cli_responses_fixture.sse",
],
visibility = ["//visibility:public"],
)
codex_rust_crate(
name = "core",
crate_name = "codex_core",
-1
View File
@@ -77,7 +77,6 @@ codex-windows-sandbox = { package = "codex-windows-sandbox", path = "../windows-
csv = { workspace = true }
dirs = { workspace = true }
dunce = { workspace = true }
env-flags = { workspace = true }
eventsource-stream = { workspace = true }
futures = { workspace = true }
http = { workspace = true }
+1 -19
View File
@@ -112,7 +112,6 @@ use crate::client_common::Prompt;
use crate::client_common::ResponseEvent;
use crate::client_common::ResponseStream;
use crate::feedback_tags;
use crate::flags::CODEX_RS_SSE_FIXTURE;
use crate::util::emit_feedback_auth_recovery_tags;
use codex_api::map_api_error;
use codex_feedback::FeedbackRequestTags;
@@ -772,7 +771,6 @@ impl ModelClient {
pub fn responses_websocket_enabled(&self) -> bool {
if !self.state.provider.info().supports_websockets
|| self.state.disable_websockets.load(Ordering::Relaxed)
|| (*CODEX_RS_SSE_FIXTURE).is_some()
{
return false;
}
@@ -1194,8 +1192,7 @@ impl ModelClientSession {
/// Streams a turn via the OpenAI Responses API.
///
/// Handles SSE fixtures, reasoning summaries, verbosity, and the
/// `text` controls used for output schemas.
/// Handles reasoning summaries, verbosity, and the `text` controls used for output schemas.
#[allow(clippy::too_many_arguments)]
#[instrument(
name = "model_client.stream_responses_api",
@@ -1221,21 +1218,6 @@ impl ModelClientSession {
turn_metadata_header: Option<&str>,
inference_trace: &InferenceTraceContext,
) -> Result<ResponseStream> {
if let Some(path) = &*CODEX_RS_SSE_FIXTURE {
warn!(path, "Streaming from fixture");
let stream = codex_api::stream_from_fixture(
path,
self.client.state.provider.info().stream_idle_timeout(),
)
.map_err(map_api_error)?;
let (stream, _last_request_rx) = map_response_stream(
stream,
session_telemetry.clone(),
InferenceTraceAttempt::disabled(),
);
return Ok(stream);
}
let auth_manager = self.client.state.provider.auth_manager();
let mut auth_recovery = auth_manager
.as_ref()
+1 -2
View File
@@ -7250,8 +7250,7 @@ model_verbosity = "high"
/// 2. as part of a profile, where the `--profile` is specified via a CLI
/// (or in the config file itself)
/// 3. as an entry in `config.toml`, e.g. `model = "o3"`
/// 4. the default value for a required field defined in code, e.g.,
/// `crate::flags::OPENAI_DEFAULT_MODEL`
/// 4. the default value for a required field defined in code.
///
/// Note that profiles are the recommended way to specify a group of
/// configuration options together.
-6
View File
@@ -1,6 +0,0 @@
use env_flags::env_flags;
env_flags! {
/// Fixture path for offline tests (see client.rs).
pub CODEX_RS_SSE_FIXTURE: Option<&str> = None;
}
-1
View File
@@ -35,7 +35,6 @@ mod environment_selection;
pub mod exec;
pub mod exec_env;
mod exec_policy;
mod flags;
#[cfg(test)]
mod git_info_tests;
mod goals;
@@ -1,8 +0,0 @@
event: response.created
data: {"type":"response.created","response":{"id":"resp1"}}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"fixture hello"}]}}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp1","output":[]}}
-3
View File
@@ -4,9 +4,6 @@ codex_rust_crate(
name = "common",
crate_name = "core_test_support",
crate_srcs = glob(["*.rs"]),
lib_data_extra = [
"//codex-rs/core:model_availability_nux_fixtures",
],
deps_extra = [
"@crates//:similar",
],
+26 -22
View File
@@ -2,7 +2,6 @@ use assert_cmd::Command as AssertCommand;
use codex_git_utils::collect_git_info;
use codex_login::CODEX_API_KEY_ENV_VAR;
use codex_protocol::protocol::GitInfo;
use codex_utils_cargo_bin::find_resource;
use core_test_support::fs_wait;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
@@ -16,9 +15,12 @@ fn repo_root() -> std::path::PathBuf {
codex_utils_cargo_bin::repo_root().expect("failed to resolve repo root")
}
fn cli_responses_fixture() -> std::path::PathBuf {
#[expect(clippy::expect_used)]
find_resource!("tests/cli_responses_fixture.sse").expect("failed to resolve fixture path")
fn cli_sse_response() -> String {
responses::sse(vec![
responses::ev_response_created("resp-fixture"),
responses::ev_assistant_message("msg-fixture", "fixture hello"),
responses::ev_completed("resp-fixture"),
])
}
/// Tests streaming the Responses API through the CLI using a mock server.
@@ -262,37 +264,36 @@ async fn exec_cli_profile_applies_model_instructions_file() {
);
}
/// Tests streaming responses through the CLI using a local SSE fixture file.
/// This test:
/// 1. Uses a pre-recorded SSE response fixture instead of a live server
/// 2. Configures codex to read from this fixture via CODEX_RS_SSE_FIXTURE env var
/// 3. Sends a "hello?" prompt and verifies the response
/// 4. Ensures the fixture content is correctly streamed through the CLI
/// Tests streaming responses through the CLI using a local Responses API server.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn responses_api_stream_cli() {
skip_if_no_network!();
let fixture = cli_responses_fixture();
let server = MockServer::start().await;
let resp_mock = responses::mount_sse_once(&server, cli_sse_response()).await;
let repo_root = repo_root();
let home = TempDir::new().unwrap();
let bin = codex_utils_cargo_bin::cargo_bin("codex").unwrap();
let mut cmd = AssertCommand::new(bin);
cmd.timeout(Duration::from_secs(30));
cmd.arg("exec")
.arg("--skip-git-repo-check")
.arg("-c")
.arg("openai_base_url=\"http://unused.local\"")
.arg(format!("openai_base_url=\"{}/v1\"", server.uri()))
.arg("-C")
.arg(&repo_root)
.arg("hello?");
cmd.env("CODEX_HOME", home.path())
.env("OPENAI_API_KEY", "dummy")
.env("CODEX_RS_SSE_FIXTURE", fixture);
.env("OPENAI_API_KEY", "dummy");
let output = cmd.output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("fixture hello"));
let request = resp_mock.single_request();
assert_eq!(request.path(), "/v1/responses");
}
/// End-to-end: create a session (writes rollout), verify the file, then resume and confirm append.
@@ -308,23 +309,25 @@ async fn integration_creates_and_checks_session_file() -> anyhow::Result<()> {
let marker = format!("integration-test-{}", Uuid::new_v4());
let prompt = format!("echo {marker}");
// 3. Use the same offline SSE fixture as responses_api_stream_cli so the test is hermetic.
let fixture = cli_responses_fixture();
// 3. Serve two hermetic SSE responses, one for the initial run and one for resume.
let server = MockServer::start().await;
let resp_mock =
responses::mount_sse_sequence(&server, vec![cli_sse_response(), cli_sse_response()]).await;
let repo_root = repo_root();
// 4. Run the codex CLI and invoke `exec`, which is what records a session.
let bin = codex_utils_cargo_bin::cargo_bin("codex").unwrap();
let mut cmd = AssertCommand::new(bin);
cmd.timeout(Duration::from_secs(30));
cmd.arg("exec")
.arg("--skip-git-repo-check")
.arg("-c")
.arg("openai_base_url=\"http://unused.local\"")
.arg(format!("openai_base_url=\"{}/v1\"", server.uri()))
.arg("-C")
.arg(&repo_root)
.arg(&prompt);
cmd.env("CODEX_HOME", home.path())
.env(CODEX_API_KEY_ENV_VAR, "dummy")
.env("CODEX_RS_SSE_FIXTURE", &fixture);
.env(CODEX_API_KEY_ENV_VAR, "dummy");
let output = cmd.output().unwrap();
assert!(
@@ -436,21 +439,22 @@ async fn integration_creates_and_checks_session_file() -> anyhow::Result<()> {
let prompt2 = format!("echo {marker2}");
let bin2 = codex_utils_cargo_bin::cargo_bin("codex").unwrap();
let mut cmd2 = AssertCommand::new(bin2);
cmd2.timeout(Duration::from_secs(30));
cmd2.arg("exec")
.arg("--skip-git-repo-check")
.arg("-c")
.arg("openai_base_url=\"http://unused.local\"")
.arg(format!("openai_base_url=\"{}/v1\"", server.uri()))
.arg("-C")
.arg(&repo_root)
.arg(&prompt2)
.arg("resume")
.arg("--last");
cmd2.env("CODEX_HOME", home.path())
.env("OPENAI_API_KEY", "dummy")
.env("CODEX_RS_SSE_FIXTURE", &fixture);
.env("OPENAI_API_KEY", "dummy");
let output2 = cmd2.output().unwrap();
assert!(output2.status.success(), "resume codex-cli run failed");
assert_eq!(resp_mock.requests().len(), 2);
// Find the new session file containing the resumed marker.
let marker2_clone = marker2.clone();
+1 -1
View File
@@ -120,7 +120,7 @@ allow = [
# Used by: transitive only
"ISC",
# MIT - https://opensource.org/license/mit
# Used by: allocative, ansi-to-tui, anyhow, arboard, assert_cmd, assert_matches, async-channel, async-stream, async-trait, axum, base64, bytes, chardetng, chrono, clap, clap_complete, color-eyre, crossterm, ctor, derive_more, diffy, dirs, dotenvy, encoding_rs, env-flags, env_logger, escargot, eventsource-stream, futures, http, ignore, image, indexmap, itertools, keyring, landlock, lazy_static, libc, log, lru, maplit, mime_guess, multimap, once_cell, openssl-sys, os_info, owo-colors, path-absolutize, pathdiff, portable-pty, predicates, pretty_assertions, pulldown-cmark, rand, ratatui, ratatui-macros, regex-lite, reqwest, rmcp, schemars, serde, serde_json, serde_with, serial_test, sha1, sha2, shlex, socket2, strum, strum_macros, sys-locale, tempfile, test-log, textwrap, thiserror, time, tiny_http, tokio, tokio-stream, tokio-test, tokio-util, toml, toml_edit, tonic, tracing, tracing-appender, tracing-subscriber, tracing-test, tree-sitter, tree-sitter-bash, tree-sitter-highlight, ts-rs, uds_windows, unicode-segmentation, unicode-width, url, urlencoding, uuid, vt100, walkdir, webbrowser, which, wildmatch, wiremock, zeroize
# Used by: allocative, ansi-to-tui, anyhow, arboard, assert_cmd, assert_matches, async-channel, async-stream, async-trait, axum, base64, bytes, chardetng, chrono, clap, clap_complete, color-eyre, crossterm, ctor, derive_more, diffy, dirs, dotenvy, encoding_rs, env_logger, escargot, eventsource-stream, futures, http, ignore, image, indexmap, itertools, keyring, landlock, lazy_static, libc, log, lru, maplit, mime_guess, multimap, once_cell, openssl-sys, os_info, owo-colors, path-absolutize, pathdiff, portable-pty, predicates, pretty_assertions, pulldown-cmark, rand, ratatui, ratatui-macros, regex-lite, reqwest, rmcp, schemars, serde, serde_json, serde_with, serial_test, sha1, sha2, shlex, socket2, strum, strum_macros, sys-locale, tempfile, test-log, textwrap, thiserror, time, tiny_http, tokio, tokio-stream, tokio-test, tokio-util, toml, toml_edit, tonic, tracing, tracing-appender, tracing-subscriber, tracing-test, tree-sitter, tree-sitter-bash, tree-sitter-highlight, ts-rs, uds_windows, unicode-segmentation, unicode-width, url, urlencoding, uuid, vt100, walkdir, webbrowser, which, wildmatch, wiremock, zeroize
"MIT",
# MIT-0 - https://opensource.org/license/mit-0
# Used by: dunce
-10
View File
@@ -1,10 +0,0 @@
event: response.created
data: {"type":"response.created","response":{"id":"resp1"}}
event: response.output_item.done
data: {"type":"response.output_item.done","item":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"fixture hello"}]}}
event: response.completed
data: {"type":"response.completed","response":{"id":"resp1","output":[]}}
+27 -13
View File
@@ -1,9 +1,19 @@
#![cfg(not(target_os = "windows"))]
#![allow(clippy::expect_used, clippy::unwrap_used)]
use codex_utils_cargo_bin::find_resource;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex_exec::test_codex_exec;
use walkdir::WalkDir;
use wiremock::MockServer;
fn exec_sse_response() -> String {
responses::sse(vec![
responses::ev_response_created("resp-ephemeral"),
responses::ev_assistant_message("msg-ephemeral", "ephemeral response"),
responses::ev_completed("resp-ephemeral"),
])
}
fn session_rollout_count(home_path: &std::path::Path) -> usize {
let sessions_dir = home_path.join("sessions");
@@ -19,13 +29,15 @@ fn session_rollout_count(home_path: &std::path::Path) -> usize {
.count()
}
#[test]
fn persists_rollout_file_by_default() -> anyhow::Result<()> {
let test = test_codex_exec();
let fixture = find_resource!("tests/fixtures/cli_responses_fixture.sse")?;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn persists_rollout_file_by_default() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
let test = test_codex_exec();
let server = MockServer::start().await;
let _response_mock = responses::mount_sse_once(&server, exec_sse_response()).await;
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("default persistence behavior")
.assert()
@@ -35,13 +47,15 @@ fn persists_rollout_file_by_default() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn does_not_persist_rollout_file_in_ephemeral_mode() -> anyhow::Result<()> {
let test = test_codex_exec();
let fixture = find_resource!("tests/fixtures/cli_responses_fixture.sse")?;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn does_not_persist_rollout_file_in_ephemeral_mode() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
let test = test_codex_exec();
let server = MockServer::start().await;
let _response_mock = responses::mount_sse_once(&server, exec_sse_response()).await;
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("--ephemeral")
.arg("ephemeral behavior")
+82 -59
View File
@@ -1,6 +1,7 @@
#![allow(clippy::unwrap_used, clippy::expect_used)]
use anyhow::Context;
use codex_utils_cargo_bin::find_resource;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use core_test_support::test_codex_exec::test_codex_exec;
use pretty_assertions::assert_eq;
use serde_json::Value;
@@ -8,6 +9,7 @@ use std::string::ToString;
use tempfile::TempDir;
use uuid::Uuid;
use walkdir::WalkDir;
use wiremock::MockServer;
/// Utility: scan the sessions dir for a rollout file that contains `marker`
/// in any response_item.message.content entry. Returns the absolute path.
@@ -104,26 +106,41 @@ fn last_user_image_count(path: &std::path::Path) -> usize {
last_count
}
fn exec_fixture() -> anyhow::Result<std::path::PathBuf> {
Ok(find_resource!("tests/fixtures/cli_responses_fixture.sse")?)
}
fn exec_repo_root() -> anyhow::Result<std::path::PathBuf> {
Ok(codex_utils_cargo_bin::repo_root()?)
}
#[test]
fn exec_resume_last_appends_to_existing_file() -> anyhow::Result<()> {
fn exec_sse_response(index: usize) -> String {
let response_id = format!("resp-exec-{index}");
let message_id = format!("msg-exec-{index}");
responses::sse(vec![
responses::ev_response_created(&response_id),
responses::ev_assistant_message(&message_id, "exec response"),
responses::ev_completed(&response_id),
])
}
async fn mount_exec_responses(
server: &MockServer,
count: usize,
) -> core_test_support::responses::ResponseMock {
responses::mount_sse_sequence(server, (0..count).map(exec_sse_response).collect()).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_resume_last_appends_to_existing_file() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let test = test_codex_exec();
let fixture = exec_fixture()?;
let server = MockServer::start().await;
let _response_mock = mount_exec_responses(&server, /*count*/ 2).await;
let repo_root = exec_repo_root()?;
// 1) First run: create a session with a unique marker in the content.
let marker = format!("resume-last-{}", Uuid::new_v4());
let prompt = format!("echo {marker}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(&repo_root)
@@ -140,8 +157,7 @@ fn exec_resume_last_appends_to_existing_file() -> anyhow::Result<()> {
let marker2 = format!("resume-last-2-{}", Uuid::new_v4());
let prompt2 = format!("echo {marker2}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(&repo_root)
@@ -164,18 +180,20 @@ fn exec_resume_last_appends_to_existing_file() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn exec_resume_last_accepts_prompt_after_flag_in_json_mode() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_resume_last_accepts_prompt_after_flag_in_json_mode() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let test = test_codex_exec();
let fixture = exec_fixture()?;
let server = MockServer::start().await;
let _response_mock = mount_exec_responses(&server, /*count*/ 2).await;
let repo_root = exec_repo_root()?;
// 1) First run: create a session with a unique marker in the content.
let marker = format!("resume-last-json-{}", Uuid::new_v4());
let prompt = format!("echo {marker}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(&repo_root)
@@ -192,8 +210,7 @@ fn exec_resume_last_accepts_prompt_after_flag_in_json_mode() -> anyhow::Result<(
let marker2 = format!("resume-last-json-2-{}", Uuid::new_v4());
let prompt2 = format!("echo {marker2}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(&repo_root)
@@ -216,18 +233,20 @@ fn exec_resume_last_accepts_prompt_after_flag_in_json_mode() -> anyhow::Result<(
Ok(())
}
#[test]
fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let test = test_codex_exec();
let fixture = exec_fixture()?;
let server = MockServer::start().await;
let _response_mock = mount_exec_responses(&server, /*count*/ 5).await;
let dir_a = TempDir::new()?;
let dir_b = TempDir::new()?;
let marker_a = format!("resume-cwd-a-{}", Uuid::new_v4());
let prompt_a = format!("echo {marker_a}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(dir_a.path())
@@ -237,8 +256,7 @@ fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> {
let marker_b = format!("resume-cwd-b-{}", Uuid::new_v4());
let prompt_b = format!("echo {marker_b}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(dir_b.path())
@@ -260,8 +278,7 @@ fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> {
let session_id_b = extract_conversation_id(&path_b);
let marker_b_touch = format!("resume-cwd-b-touch-{}", Uuid::new_v4());
let prompt_b_touch = format!("echo {marker_b_touch}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(dir_b.path())
@@ -278,8 +295,7 @@ fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> {
let marker_b2 = format!("resume-cwd-b-2-{}", Uuid::new_v4());
let prompt_b2 = format!("echo {marker_b2}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(dir_a.path())
@@ -299,8 +315,7 @@ fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> {
let marker_a2 = format!("resume-cwd-a-2-{}", Uuid::new_v4());
let prompt_a2 = format!("echo {marker_a2}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(dir_a.path())
@@ -323,24 +338,29 @@ fn exec_resume_last_respects_cwd_filter_and_all_flag() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn exec_resume_accepts_global_flags_after_subcommand() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_resume_accepts_global_flags_after_subcommand() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let test = test_codex_exec();
let fixture = exec_fixture()?;
let server = MockServer::start().await;
let _response_mock = mount_exec_responses(&server, /*count*/ 2).await;
// Seed a session.
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("echo seed-resume-session")
.assert()
.success();
// Resume while passing global flags after the subcommand to ensure clap accepts them.
let base = format!("{}/v1", server.uri());
let base_config = format!("openai_base_url={}", serde_json::to_string(&base)?);
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
.arg("resume")
.arg("--last")
.arg("--config")
.arg(base_config)
.arg("--json")
.arg("--model")
.arg("gpt-5.2-codex")
@@ -355,18 +375,20 @@ fn exec_resume_accepts_global_flags_after_subcommand() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let test = test_codex_exec();
let fixture = exec_fixture()?;
let server = MockServer::start().await;
let _response_mock = mount_exec_responses(&server, /*count*/ 2).await;
let repo_root = exec_repo_root()?;
// 1) First run: create a session
let marker = format!("resume-by-id-{}", Uuid::new_v4());
let prompt = format!("echo {marker}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(&repo_root)
@@ -387,8 +409,7 @@ fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> {
let marker2 = format!("resume-by-id-2-{}", Uuid::new_v4());
let prompt2 = format!("echo {marker2}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(&repo_root)
@@ -410,17 +431,19 @@ fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn exec_resume_preserves_cli_configuration_overrides() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_resume_preserves_cli_configuration_overrides() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let test = test_codex_exec();
let fixture = exec_fixture()?;
let server = MockServer::start().await;
let _response_mock = mount_exec_responses(&server, /*count*/ 2).await;
let repo_root = exec_repo_root()?;
let marker = format!("resume-config-{}", Uuid::new_v4());
let prompt = format!("echo {marker}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("--sandbox")
.arg("workspace-write")
@@ -440,8 +463,7 @@ fn exec_resume_preserves_cli_configuration_overrides() -> anyhow::Result<()> {
let prompt2 = format!("echo {marker2}");
let output = test
.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("--sandbox")
.arg("workspace-write")
@@ -484,17 +506,19 @@ fn exec_resume_preserves_cli_configuration_overrides() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn exec_resume_accepts_images_after_subcommand() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_resume_accepts_images_after_subcommand() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let test = test_codex_exec();
let fixture = exec_fixture()?;
let server = MockServer::start().await;
let _response_mock = mount_exec_responses(&server, /*count*/ 2).await;
let repo_root = exec_repo_root()?;
let marker = format!("resume-image-{}", Uuid::new_v4());
let prompt = format!("echo {marker}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(&repo_root)
@@ -516,8 +540,7 @@ fn exec_resume_accepts_images_after_subcommand() -> anyhow::Result<()> {
let marker2 = format!("resume-image-2-{}", Uuid::new_v4());
let prompt2 = format!("echo {marker2}");
test.cmd()
.env("CODEX_RS_SSE_FIXTURE", &fixture)
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(&repo_root)
+1 -1
View File
@@ -18,7 +18,7 @@ codex_rust_crate(
test_data_extra = glob([
"src/**/*.rs",
"src/**/snapshots/**",
]) + ["//codex-rs/core:model_availability_nux_fixtures"],
]),
integration_compile_data_extra = ["src/test_backend.rs"],
extra_binaries = [
"//codex-rs/cli:codex",
+2
View File
@@ -152,6 +152,7 @@ arboard = { workspace = true }
[dev-dependencies]
codex-cli = { workspace = true }
codex-mcp = { workspace = true }
core_test_support = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
codex-utils-pty = { workspace = true }
assert_matches = { workspace = true }
@@ -162,3 +163,4 @@ rand = { workspace = true }
serial_test = { workspace = true }
vt100 = { workspace = true }
uuid = { workspace = true }
wiremock = { workspace = true }
@@ -4,11 +4,14 @@ use std::time::Duration;
use anyhow::Context;
use anyhow::Result;
use codex_models_manager::bundled_models_response;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use serde_json::Value as JsonValue;
use tempfile::tempdir;
use tokio::select;
use tokio::time::sleep;
use tokio::time::timeout;
use wiremock::MockServer;
#[tokio::test]
async fn resume_startup_does_not_consume_model_availability_nux_count() -> Result<()> {
@@ -16,6 +19,7 @@ async fn resume_startup_does_not_consume_model_availability_nux_count() -> Resul
if cfg!(windows) {
return Ok(());
}
skip_if_no_network!(Ok(()));
let repo_root = codex_utils_cargo_bin::repo_root()?;
let codex_home = tempdir()?;
@@ -68,8 +72,14 @@ trust_level = "trusted"
);
std::fs::write(codex_home.path().join("config.toml"), config_contents)?;
let fixture_path =
codex_utils_cargo_bin::find_resource!("../core/tests/cli_responses_fixture.sse")?;
let server = MockServer::start().await;
let sse = responses::sse(vec![
responses::ev_response_created("resp-seed-session"),
responses::ev_assistant_message("msg-seed-session", "seed session response"),
responses::ev_completed("resp-seed-session"),
]);
let _response_mock = responses::mount_sse_once(&server, sse).await;
let openai_base_url_config = format!("openai_base_url=\"{}/v1\"", server.uri());
let codex = if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex") {
path
} else {
@@ -85,12 +95,13 @@ trust_level = "trusted"
let exec_output = std::process::Command::new(&codex)
.arg("exec")
.arg("--skip-git-repo-check")
.arg("-c")
.arg(&openai_base_url_config)
.arg("-C")
.arg(&repo_root)
.arg("seed session for resume")
.env("CODEX_HOME", codex_home.path())
.env("OPENAI_API_KEY", "dummy")
.env("CODEX_RS_SSE_FIXTURE", fixture_path)
.output()
.context("failed to execute codex exec")?;
anyhow::ensure!(
@@ -114,6 +125,8 @@ trust_level = "trusted"
repo_root.display().to_string(),
"-c".to_string(),
"analytics.enabled=false".to_string(),
"-c".to_string(),
openai_base_url_config,
];
let spawned = codex_utils_pty::spawn_pty_process(
+36 -47
View File
@@ -8,14 +8,18 @@ use std::time::Instant;
use anyhow::Context;
use anyhow::Result;
use core_test_support::responses;
use core_test_support::skip_if_no_network;
use tempfile::tempdir;
use wiremock::MockServer;
#[test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires tmux and a locally built codex binary; run with --ignored for manual resize smoke"]
fn tmux_split_preserves_fresh_session_composer_row_after_resize_reflow() -> Result<()> {
async fn tmux_split_preserves_fresh_session_composer_row_after_resize_reflow() -> Result<()> {
if cfg!(windows) {
return Ok(());
}
skip_if_no_network!(Ok(()));
if Command::new("tmux").arg("-V").output().is_err() {
eprintln!("skipping resize smoke because tmux is unavailable");
return Ok(());
@@ -24,9 +28,9 @@ fn tmux_split_preserves_fresh_session_composer_row_after_resize_reflow() -> Resu
let repo_root = codex_utils_cargo_bin::repo_root()?;
let codex = codex_binary(&repo_root)?;
let codex_home = tempdir()?;
let fixture_dir = tempdir()?;
let fixture = fixture_dir.path().join("resize-reflow.sse");
write_fixture(&fixture)?;
let server = MockServer::start().await;
let _response_mock = responses::mount_sse_once(&server, resize_reflow_sse()).await;
let openai_base_url_config = format!("openai_base_url=\"{}/v1\"", server.uri());
write_config(
codex_home.path(),
&repo_root,
@@ -57,10 +61,11 @@ fn tmux_split_preserves_fresh_session_composer_row_after_resize_reflow() -> Resu
.arg("env")
.arg(format!("CODEX_HOME={}", codex_home.path().display()))
.arg("OPENAI_API_KEY=dummy")
.arg(format!("CODEX_RS_SSE_FIXTURE={}", fixture.display()))
.arg(codex)
.arg("-c")
.arg("analytics.enabled=false")
.arg("-c")
.arg(&openai_base_url_config)
.arg("--no-alt-screen")
.arg("-C")
.arg(&repo_root)
@@ -162,29 +167,31 @@ fn tmux_split_preserves_fresh_session_composer_row_after_resize_reflow() -> Resu
Ok(())
}
#[test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires tmux and a locally built codex binary; run with --ignored for manual resize smoke"]
fn tmux_repeated_resizes_do_not_push_composer_down() -> Result<()> {
async fn tmux_repeated_resizes_do_not_push_composer_down() -> Result<()> {
if cfg!(windows) {
return Ok(());
}
skip_if_no_network!(Ok(()));
if Command::new("tmux").arg("-V").output().is_err() {
eprintln!("skipping resize smoke because tmux is unavailable");
return Ok(());
}
run_repeated_resize_smoke(/*terminal_resize_reflow_enabled*/ false)?;
run_repeated_resize_smoke(/*terminal_resize_reflow_enabled*/ true)?;
run_repeated_resize_smoke(/*terminal_resize_reflow_enabled*/ false).await?;
run_repeated_resize_smoke(/*terminal_resize_reflow_enabled*/ true).await?;
Ok(())
}
#[test]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires tmux and a locally built codex binary; run with --ignored for manual resize smoke"]
fn tmux_width_resize_restore_keeps_visible_content_anchored() -> Result<()> {
async fn tmux_width_resize_restore_keeps_visible_content_anchored() -> Result<()> {
if cfg!(windows) {
return Ok(());
}
skip_if_no_network!(Ok(()));
if Command::new("tmux").arg("-V").output().is_err() {
eprintln!("skipping resize smoke because tmux is unavailable");
return Ok(());
@@ -193,9 +200,9 @@ fn tmux_width_resize_restore_keeps_visible_content_anchored() -> Result<()> {
let repo_root = codex_utils_cargo_bin::repo_root()?;
let codex = codex_binary(&repo_root)?;
let codex_home = tempdir()?;
let fixture_dir = tempdir()?;
let fixture = fixture_dir.path().join("resize-reflow.sse");
write_fixture(&fixture)?;
let server = MockServer::start().await;
let _response_mock = responses::mount_sse_once(&server, resize_reflow_sse()).await;
let openai_base_url_config = format!("openai_base_url=\"{}/v1\"", server.uri());
write_config(
codex_home.path(),
&repo_root,
@@ -226,10 +233,11 @@ fn tmux_width_resize_restore_keeps_visible_content_anchored() -> Result<()> {
.arg("env")
.arg(format!("CODEX_HOME={}", codex_home.path().display()))
.arg("OPENAI_API_KEY=dummy")
.arg(format!("CODEX_RS_SSE_FIXTURE={}", fixture.display()))
.arg(codex)
.arg("-c")
.arg("analytics.enabled=false")
.arg("-c")
.arg(&openai_base_url_config)
.arg("--no-alt-screen")
.arg("-C")
.arg(&repo_root)
@@ -312,13 +320,13 @@ fn tmux_width_resize_restore_keeps_visible_content_anchored() -> Result<()> {
Ok(())
}
fn run_repeated_resize_smoke(terminal_resize_reflow_enabled: bool) -> Result<()> {
async fn run_repeated_resize_smoke(terminal_resize_reflow_enabled: bool) -> Result<()> {
let repo_root = codex_utils_cargo_bin::repo_root()?;
let codex = codex_binary(&repo_root)?;
let codex_home = tempdir()?;
let fixture_dir = tempdir()?;
let fixture = fixture_dir.path().join("resize-reflow.sse");
write_fixture(&fixture)?;
let server = MockServer::start().await;
let _response_mock = responses::mount_sse_once(&server, resize_reflow_sse()).await;
let openai_base_url_config = format!("openai_base_url=\"{}/v1\"", server.uri());
write_config(
codex_home.path(),
&repo_root,
@@ -354,10 +362,11 @@ fn run_repeated_resize_smoke(terminal_resize_reflow_enabled: bool) -> Result<()>
.arg("env")
.arg(format!("CODEX_HOME={}", codex_home.path().display()))
.arg("OPENAI_API_KEY=dummy")
.arg(format!("CODEX_RS_SSE_FIXTURE={}", fixture.display()))
.arg(codex)
.arg("-c")
.arg("analytics.enabled=false")
.arg("-c")
.arg(&openai_base_url_config)
.arg("--no-alt-screen")
.arg("-C")
.arg(&repo_root)
@@ -510,33 +519,13 @@ fn write_auth(codex_home: &Path) -> Result<()> {
Ok(())
}
fn write_fixture(path: &Path) -> Result<()> {
fn resize_reflow_sse() -> String {
let text = "resize reflow sentinel says hi. This paragraph is intentionally long enough to exercise terminal wrapping, scrollback redraw, and pane resize behavior without requiring a live model response. It includes enough ordinary prose to wrap across several rows in a narrow tmux pane, then keep going so repeated split and restore cycles have visible history above the composer. If a resize path accidentally inserts blank rows or anchors the viewport lower on each pass, the composer row will drift after the pane returns to its original height.";
let created = serde_json::json!({
"type": "response.created",
"response": { "id": "resp-resize-smoke" },
});
let done = serde_json::json!({
"type": "response.output_item.done",
"item": {
"type": "message",
"role": "assistant",
"content": [
{ "type": "output_text", "text": text }
],
},
});
let completed = serde_json::json!({
"type": "response.completed",
"response": { "id": "resp-resize-smoke", "output": [] },
});
let fixture = format!(
"event: response.created\ndata: {created}\n\n\
event: response.output_item.done\ndata: {done}\n\n\
event: response.completed\ndata: {completed}\n\n"
);
std::fs::write(path, fixture)?;
Ok(())
responses::sse(vec![
responses::ev_response_created("resp-resize-smoke"),
responses::ev_assistant_message("msg-resize-smoke", text),
responses::ev_completed("resp-resize-smoke"),
])
}
fn wait_for_capture_contains(pane: &str, needle: &str, timeout: Duration) -> Result<String> {