diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 3e27463370..1b0de3b264 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -99,6 +99,7 @@ mod request_permissions_tool; mod request_plugin_install; mod request_user_input; mod responses_api_proxy_headers; +mod responses_api_system_proxy; mod responses_lite; mod resume; mod resume_warning; diff --git a/codex-rs/core/tests/suite/responses_api_system_proxy.rs b/codex-rs/core/tests/suite/responses_api_system_proxy.rs new file mode 100644 index 0000000000..0a1f998896 --- /dev/null +++ b/codex-rs/core/tests/suite/responses_api_system_proxy.rs @@ -0,0 +1,117 @@ +//! Verifies that the effective system-proxy feature reaches both HTTP Responses API paths. + +use anyhow::Result; +use codex_config::CONFIG_TOML_FILE; +use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::EventMsg; +use codex_protocol::protocol::Op; +use core_test_support::responses::ev_completed; +use core_test_support::responses::ev_response_created; +use core_test_support::responses::mount_compact_json_once; +use core_test_support::responses::mount_sse_once; +use core_test_support::responses::sse; +use core_test_support::responses::start_mock_server; +use core_test_support::skip_if_no_network; +use core_test_support::test_codex::test_codex; +use core_test_support::wait_for_event; +use pretty_assertions::assert_eq; +use std::process::Command; + +const SUBPROCESS_ENV: &str = "CODEX_RESPONSES_SYSTEM_PROXY_TEST_CHILD"; +const SUBPROCESS_SENTINEL: &str = "responses system proxy child ran"; +const TEST_NAME: &str = + "suite::responses_api_system_proxy::responses_and_compact_use_enabled_system_proxy"; +const TARGET_BASE_URL: &str = "http://responses-api.invalid/v1"; + +#[cfg_attr( + not(target_os = "linux"), + ignore = "system proxy environment fallback is Linux-specific" +)] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn responses_and_compact_use_enabled_system_proxy() -> Result<()> { + if std::env::var_os(SUBPROCESS_ENV).is_none() { + return run_test_in_subprocess().await; + } + + eprintln!("{SUBPROCESS_SENTINEL}"); + skip_if_no_network!(Ok(())); + + let server = start_mock_server().await; + let mut builder = test_codex() + .with_pre_build_hook(|codex_home| { + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + "[features]\nrespect_system_proxy = true\n", + ) + .expect("system-proxy feature config should be written"); + }) + .with_config(|config| { + config.model_provider.base_url = Some(TARGET_BASE_URL.to_string()); + }); + let test = builder.build_with_auto_env(&server).await?; + assert!(test.config.respect_system_proxy); + + test.submit_turn("exercise the Responses API proxy route") + .await?; + test.codex.submit(Op::Compact).await?; + wait_for_event(&test.codex, |event| { + matches!(event, EventMsg::TurnComplete(_)) + }) + .await; + + Ok(()) +} + +async fn run_test_in_subprocess() -> Result<()> { + skip_if_no_network!(Ok(())); + + let proxy = start_mock_server().await; + let responses_mock = mount_sse_once( + &proxy, + sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), + ) + .await; + let compact_mock = mount_compact_json_once( + &proxy, + serde_json::json!({ + "output": [ResponseItem::Compaction { + id: None, + encrypted_content: "encrypted compacted history".to_string(), + internal_chat_message_metadata_passthrough: None, + }] + }), + ) + .await; + + // Reqwest disables its default environment proxy matcher in CGI processes. The route-aware + // client resolves `HTTP_PROXY` explicitly, so this marker makes the test fail if either + // Responses endpoint regresses to the default client. + let output = Command::new(std::env::current_exe()?) + .args(["--exact", TEST_NAME, "--nocapture"]) + .env(SUBPROCESS_ENV, "1") + .env("HTTP_PROXY", proxy.uri()) + .env("REQUEST_METHOD", "GET") + .env_remove("http_proxy") + .env_remove("ALL_PROXY") + .env_remove("all_proxy") + .env_remove("NO_PROXY") + .env_remove("no_proxy") + .output()?; + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + output.status.success(), + "system-proxy subprocess failed\nstdout:\n{}\nstderr:\n{stderr}", + String::from_utf8_lossy(&output.stdout), + ); + assert!( + stderr.contains(SUBPROCESS_SENTINEL), + "system-proxy subprocess did not execute the expected test\nstderr:\n{stderr}" + ); + + assert_eq!(responses_mock.single_request().path(), "/v1/responses"); + assert_eq!( + compact_mock.single_request().path(), + "/v1/responses/compact" + ); + Ok(()) +}