[codex] cover app-owned HTTP requests [ci changed_files]

This commit is contained in:
Cooper Gamble
2026-06-03 23:44:25 +00:00
parent ec2e111e04
commit d766866d3c
52 changed files with 1415 additions and 358 deletions
+38 -30
View File
@@ -24,6 +24,7 @@ use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::RemoveOptions;
use codex_extension_api::empty_extension_registry;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::built_in_model_providers;
@@ -210,6 +211,7 @@ pub fn turn_permission_fields(
pub struct TestCodexBuilder {
config_mutators: Vec<Box<ConfigMutator>>,
auth: CodexAuth,
auth_manager: Option<Arc<AuthManager>>,
pre_build_hooks: Vec<Box<PreBuildHook>>,
workspace_setups: Vec<Box<WorkspaceSetup>>,
home: Option<Arc<TempDir>>,
@@ -232,6 +234,11 @@ impl TestCodexBuilder {
self
}
pub fn with_auth_manager(mut self, auth_manager: Arc<AuthManager>) -> Self {
self.auth_manager = Some(auth_manager);
self
}
pub fn with_model(self, model: &str) -> Self {
let new_model = model.to_string();
self.with_config(move |config| {
@@ -464,13 +471,16 @@ impl TestCodexBuilder {
test_env: TestEnv,
environment_manager: Arc<codex_exec_server::EnvironmentManager>,
) -> anyhow::Result<TestCodex> {
let auth = self.auth.clone();
let auth_manager = self
.auth_manager
.clone()
.unwrap_or_else(|| codex_core::test_support::auth_manager_from_auth(self.auth.clone()));
let state_db = codex_core::init_state_db(&config).await;
let thread_store = thread_store_from_config(&config, state_db.clone());
let installation_id = resolve_installation_id(&config.codex_home).await?;
let thread_manager = ThreadManager::new(
&config,
codex_core::test_support::auth_manager_from_auth(auth.clone()),
Arc::clone(&auth_manager),
SessionSource::Exec,
Arc::clone(&environment_manager),
empty_extension_registry(),
@@ -483,42 +493,39 @@ impl TestCodexBuilder {
let thread_manager = Arc::new(thread_manager);
let user_shell_override = self.user_shell_override.clone();
let new_conversation = match (resume_from, user_shell_override) {
(Some(path), Some(user_shell_override)) => {
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
Box::pin(
let new_conversation =
match (resume_from, user_shell_override) {
(Some(path), Some(user_shell_override)) => Box::pin(
codex_core::test_support::resume_thread_from_rollout_with_user_shell_override(
thread_manager.as_ref(),
config.clone(),
path,
auth_manager,
Arc::clone(&auth_manager),
user_shell_override,
),
)
.await?
}
(Some(path), None) => {
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
Box::pin(thread_manager.resume_thread_from_rollout(
config.clone(),
path,
auth_manager,
/*parent_trace*/ None,
))
.await?
}
(None, Some(user_shell_override)) => {
Box::pin(
codex_core::test_support::start_thread_with_user_shell_override(
thread_manager.as_ref(),
.await?,
(Some(path), None) => {
Box::pin(thread_manager.resume_thread_from_rollout(
config.clone(),
user_shell_override,
),
)
.await?
}
(None, None) => Box::pin(thread_manager.start_thread(config.clone())).await?,
};
path,
Arc::clone(&auth_manager),
/*parent_trace*/ None,
))
.await?
}
(None, Some(user_shell_override)) => {
Box::pin(
codex_core::test_support::start_thread_with_user_shell_override(
thread_manager.as_ref(),
config.clone(),
user_shell_override,
),
)
.await?
}
(None, None) => Box::pin(thread_manager.start_thread(config.clone())).await?,
};
Ok(TestCodex {
home,
@@ -1031,6 +1038,7 @@ pub fn test_codex() -> TestCodexBuilder {
TestCodexBuilder {
config_mutators: vec![],
auth: CodexAuth::from_api_key("dummy"),
auth_manager: None,
pre_build_hooks: vec![],
workspace_setups: vec![],
home: None,
+129
View File
@@ -9,8 +9,11 @@ use codex_core::resolve_installation_id;
use codex_core::thread_store_from_config;
use codex_extension_api::empty_extension_registry;
use codex_features::Feature;
use codex_http_state::HttpStateStore;
use codex_http_state::HttpStateSurface;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
use codex_login::default_client::originator;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::WireApi;
@@ -58,6 +61,7 @@ use core_test_support::responses::ev_completed_with_tokens;
use core_test_support::responses::ev_message_item_added;
use core_test_support::responses::ev_output_text_delta;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_response_sequence;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::mount_sse_once_match;
use core_test_support::responses::mount_sse_sequence;
@@ -71,6 +75,7 @@ use dunce::canonicalize as normalize_path;
use futures::StreamExt;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::ffi::OsString;
use std::io::Write;
use std::num::NonZeroU64;
use std::sync::Arc;
@@ -943,6 +948,130 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
}
}
struct EnvGuard {
key: &'static str,
original: Option<OsString>,
}
impl EnvGuard {
fn set(key: &'static str, value: String) -> Self {
let original = std::env::var_os(key);
// SAFETY: the test is serialized with the other auth-refresh tests.
unsafe {
std::env::set_var(key, &value);
}
Self { key, original }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
// SAFETY: the guard restores the original environment value before other tests run.
unsafe {
match &self.original {
Some(value) => std::env::set_var(self.key, value),
None => std::env::remove_var(self.key),
}
}
}
}
#[serial_test::serial(auth_refresh)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn response_401_refresh_preserves_native_http_state_surface() {
skip_if_no_network!();
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/oauth/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"access_token": "new-access-token",
"refresh_token": "new-refresh-token",
"state": "new-state",
})))
.expect(1)
.mount(&server)
.await;
let responses_mock = mount_response_sequence(
&server,
vec![
ResponseTemplate::new(401).set_body_string("unauthorized"),
ResponseTemplate::new(401).set_body_string("unauthorized"),
ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_raw(
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
"text/event-stream",
),
],
)
.await;
let home = Arc::new(TempDir::new().unwrap());
write_auth_json(
home.as_ref(),
/*openai_api_key*/ None,
"pro",
"old-access-token",
Some("acc-123"),
);
let store = HttpStateStore::new(home.path().to_path_buf());
store
.set(HttpStateSurface::CodexExec, "old-state".to_string())
.unwrap();
let _refresh_endpoint = EnvGuard::set(
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
format!("{}/oauth/token", server.uri()),
);
let auth_manager = AuthManager::shared(
home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
)
.await;
let mut builder = test_codex()
.with_home(home)
.with_auth_manager(auth_manager)
.with_config(|config| {
config.model_catalog =
Some(bundled_models_response().expect("bundled models should parse"));
});
let test = builder.build(&server).await.unwrap();
test.submit_turn("hello").await.unwrap();
assert_eq!(
store.get(HttpStateSurface::CodexExec).unwrap(),
Some("new-state".to_string())
);
let requests = responses_mock.requests();
assert_eq!(requests.len(), 3);
assert_eq!(
requests[0].header("authorization").as_deref(),
Some("Bearer old-access-token")
);
assert_eq!(
requests[1].header("authorization").as_deref(),
Some("Bearer old-access-token")
);
assert_eq!(
requests[2].header("authorization").as_deref(),
Some("Bearer new-access-token")
);
let refresh_request = server
.received_requests()
.await
.unwrap_or_default()
.into_iter()
.find(|request| request.url.path() == "/oauth/token")
.expect("refresh request");
let refresh_body: serde_json::Value =
serde_json::from_slice(&refresh_request.body).expect("refresh JSON body");
assert_eq!(refresh_body["surface"], "codex_exec");
server.verify().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn includes_base_instructions_override_in_request() {
skip_if_no_network!();
+41 -4
View File
@@ -5,6 +5,8 @@ use std::path::Path;
use anyhow::Context;
use anyhow::Result;
use codex_http_state::HttpStateStore;
use codex_http_state::HttpStateSurface;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use core_test_support::apps_test_server::AppsTestServer;
@@ -33,6 +35,11 @@ use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;
const INITIAL_INTEGRITY_STATE: &str = "state-n";
const INTEGRITY_STATE_HEADER_NAME: &str = "x-oai-is";
const INTEGRITY_STATE_UPDATE_HEADER_NAME: &str = "x-oai-is-update";
const UNTRUSTED_INTEGRITY_STATE_UPDATE: &str = "state-n-plus-one";
fn write_post_tool_use_hook(home: &Path) -> Result<()> {
let script_path = home.join("post_tool_use_hook.py");
let log_path = home.join("post_tool_use_hook_log.jsonl");
@@ -95,10 +102,17 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res
"file_size": 11,
"use_case": "codex",
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"file_id": "file_123",
"upload_url": format!("{}/upload/file_123", server.uri()),
})))
.respond_with(
ResponseTemplate::new(200)
.insert_header(
INTEGRITY_STATE_UPDATE_HEADER_NAME,
UNTRUSTED_INTEGRITY_STATE_UPDATE,
)
.set_body_json(json!({
"file_id": "file_123",
"upload_url": format!("{}/upload/file_123", server.uri()),
})),
)
.expect(1)
.mount(&server)
.await;
@@ -150,6 +164,12 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res
if let Err(error) = write_post_tool_use_hook(home) {
panic!("failed to write apps file post tool use hook fixture: {error}");
}
HttpStateStore::new(home.to_path_buf())
.set(
HttpStateSurface::CodexExec,
INITIAL_INTEGRITY_STATE.to_string(),
)
.expect("initial HTTP state should store");
})
.with_config(move |config| {
trust_discovered_hooks(config);
@@ -218,6 +238,23 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res
"file_size_bytes": 11,
})
);
assert_eq!(
HttpStateStore::new(test.codex_home_path().to_path_buf())
.get(HttpStateSurface::CodexExec)?,
Some(INITIAL_INTEGRITY_STATE.to_string())
);
let file_create_request = server
.received_requests()
.await
.context("capture file upload requests")?
.into_iter()
.find(|request| request.url.path() == "/files")
.context("find file create request")?;
assert!(
!file_create_request
.headers
.contains_key(INTEGRITY_STATE_HEADER_NAME)
);
server.verify().await;
Ok(())