mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
[codex] cover app-owned HTTP requests [ci changed_files]
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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!();
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user