#![allow(clippy::unwrap_used)]use std::fs;use std::path::Path;use codex_core::shell::default_user_shell;use codex_features::Feature;use codex_prompts::APPLY_PATCH_TOOL_INSTRUCTIONS;use codex_protocol::config_types::CollaborationMode;use codex_protocol::config_types::ModeKind;use codex_protocol::config_types::ReasoningSummary;use codex_protocol::config_types::Settings;use codex_protocol::config_types::WebSearchMode;use codex_protocol::models::PermissionProfile;use codex_protocol::openai_models::ReasoningEffort;use codex_protocol::permissions::NetworkSandboxPolicy;use codex_protocol::protocol::AskForApproval;use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::Op;use codex_protocol::user_input::UserInput;use core_test_support::TempDirExt;use core_test_support::responses::ev_completed;use core_test_support::responses::ev_response_created;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::TestCodex;use core_test_support::test_codex::local_selections;use core_test_support::test_codex::test_codex;use core_test_support::test_codex::turn_permission_fields;use core_test_support::wait_for_event;use pretty_assertions::assert_eq;use tempfile::TempDir;fn write_global_instructions(home: &Path) { fs::write(home.join("AGENTS.md"), "be consistent and helpful") .expect("write global instructions");}fn text_user_input(text: String) -> serde_json::Value { text_user_input_parts(vec![text])}fn text_user_input_parts(texts: Vec<String>) -> serde_json::Value { serde_json::json!({ "type": "message", "role": "user", "content": texts .into_iter() .map(|text| serde_json::json!({ "type": "input_text", "text": text })) .collect::<Vec<_>>() })}fn assert_default_env_context(text: &str, cwd: &str) { assert_env_context_fragment(text); assert!( text.contains(&format!("<cwd>{cwd}</cwd>")), "expected cwd in environment context: {text}" ); assert!( text.contains(&format!("<shell>{}</shell>", default_user_shell().name())), "expected shell in environment context: {text}" );}fn assert_env_context_fragment(text: &str) { assert!( text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG), "expected environment context fragment: {text}" ); assert!( text.contains("<current_date>") && text.contains("</current_date>"), "expected current_date in environment context: {text}" ); assert!( text.contains("<timezone>") && text.contains("</timezone>"), "expected timezone in environment context: {text}" ); assert!( text.ends_with("</environment_context>"), "expected closing environment_context tag: {text}" );}fn assert_tool_names(body: &serde_json::Value, expected_names: &[&str]) { assert_eq!( body["tools"] .as_array() .unwrap() .iter() .map(|t| { t.get("name") .and_then(|value| value.as_str()) .or_else(|| t.get("type").and_then(|value| value.as_str())) .unwrap() .to_string() }) .collect::<Vec<_>>(), expected_names );}fn normalize_newlines(text: &str) -> String { text.replace("\r\n", "\n")}#[tokio::test(flavor = "multi_thread", worker_threads = 4)]async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); use pretty_assertions::assert_eq; let server = start_mock_server().await; let req1 = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), ) .await; let req2 = mount_sse_once( &server, sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), ) .await; let TestCodex { codex, config, thread_manager, .. } = test_codex() .with_pre_build_hook(write_global_instructions) .with_config(|config| { config.model = Some("gpt-5.2".to_string()); // Keep tool expectations stable when the default web_search mode changes. config .web_search_mode .set(WebSearchMode::Cached) .expect("test web_search_mode should satisfy constraints"); config .features .enable(Feature::CollaborationModes) .expect("test config should allow feature update"); }) .build(&server) .await?; let base_instructions = thread_manager .get_models_manager() .get_model_info( config .model .as_deref() .expect("test config should have a model"), &config.to_models_manager_config(), ) .await .base_instructions; codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: Default::default(), }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: Default::default(), }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let mut expected_tools_names = if cfg!(windows) { vec!["shell_command"] } else { vec!["exec_command", "write_stdin"] }; expected_tools_names.extend([ "update_plan", "request_user_input", "apply_patch", "view_image", "tool_search", "web_search", ]); let body0 = req1.single_request().body_json(); let expected_instructions = if expected_tools_names.contains(&"apply_patch") { base_instructions } else { [base_instructions, APPLY_PATCH_TOOL_INSTRUCTIONS.to_string()].join("\n") }; assert_eq!( body0["instructions"], serde_json::json!(expected_instructions), ); assert_tool_names(&body0, &expected_tools_names); let body1 = req2.single_request().body_json(); assert_eq!( body1["instructions"], serde_json::json!(expected_instructions), ); assert_tool_names(&body1, &expected_tools_names); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); use pretty_assertions::assert_eq; let server = start_mock_server().await; let req1 = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), ) .await; let req2 = mount_sse_once( &server, sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), ) .await; let TestCodex { codex, .. } = test_codex() .with_pre_build_hook(write_global_instructions) .with_config(|config| { config .features .enable(Feature::CollaborationModes) .expect("test config should allow feature update"); config.model = Some("gpt-5.2".to_string()); }) .build(&server) .await?; codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: Default::default(), }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: Default::default(), }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let body0 = req1.single_request().body_json(); let instructions0 = body0["instructions"] .as_str() .expect("instructions should be a string"); assert!( instructions0.contains("You are"), "expected non-empty instructions" ); let body1 = req2.single_request().body_json(); let instructions1 = body1["instructions"] .as_str() .expect("instructions should be a string"); assert_eq!( normalize_newlines(instructions1), normalize_newlines(instructions0) ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn prefixes_context_and_instructions_once_and_consistently_across_requests()-> anyhow::Result<()> { skip_if_no_network!(Ok(())); use pretty_assertions::assert_eq; let server = start_mock_server().await; let req1 = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), ) .await; let req2 = mount_sse_once( &server, sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), ) .await; let TestCodex { codex, config, .. } = test_codex() .with_pre_build_hook(write_global_instructions) .with_config(|config| { config .features .enable(Feature::CollaborationModes) .expect("test config should allow feature update"); }) .build(&server) .await?; codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: Default::default(), }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: Default::default(), }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let body1 = req1.single_request().body_json(); let input1 = body1["input"].as_array().expect("input array"); assert_eq!( input1.len(), 3, "expected permissions + cached contextual user prefix + user msg" ); let ui_text = input1[1]["content"][0]["text"] .as_str() .expect("ui message text"); assert!( ui_text.contains("be consistent and helpful"), "expected user instructions in UI message: {ui_text}" ); let cwd_str = config.cwd.to_string_lossy(); let env_text = input1[1]["content"][1]["text"] .as_str() .expect("environment context text"); assert_default_env_context(env_text, &cwd_str); assert_eq!( input1[1]["content"][1]["type"].as_str(), Some("input_text"), "expected environment context bundled after UI message in cached contextual message" ); assert_eq!(input1[2], text_user_input("hello 1".to_string())); let body2 = req2.single_request().body_json(); let input2 = body2["input"].as_array().expect("input array"); assert_eq!( &input2[..input1.len()], input1.as_slice(), "expected cached prefix to be reused" ); assert_eq!(input2[input1.len()], text_user_input("hello 2".to_string())); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); use pretty_assertions::assert_eq; let server = start_mock_server().await; let req1 = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), ) .await; let req2 = mount_sse_once( &server, sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), ) .await; let TestCodex { codex, config, .. } = test_codex() .with_pre_build_hook(write_global_instructions) .with_config(|config| { config .features .enable(Feature::CollaborationModes) .expect("test config should allow feature update"); }) .build(&server) .await?; // First turn codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: Default::default(), }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let writable = TempDir::new().unwrap(); let permission_profile = PermissionProfile::workspace_write_with( &[writable.abs()], NetworkSandboxPolicy::Enabled, /*exclude_tmpdir_env_var*/ true, /*exclude_slash_tmp*/ true, ); let sandbox_policy = permission_profile .to_legacy_sandbox_policy(config.cwd.as_path()) .expect("workspace profile should have legacy projection"); core_test_support::submit_thread_settings( &codex, codex_protocol::protocol::ThreadSettingsOverrides { approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile: Some(permission_profile), effort: Some(Some(ReasoningEffort::High)), summary: Some(ReasoningSummary::Detailed), ..Default::default() }, ) .await?; // Second turn after overrides codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: Default::default(), }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let request1 = req1.single_request(); let request2 = req2.single_request(); let body1 = request1.body_json(); let body2 = request2.body_json(); // prompt_cache_key should remain constant across overrides assert_eq!( body1["prompt_cache_key"], body2["prompt_cache_key"], "prompt_cache_key should not change across overrides" ); // The entire prefix from the first request should be identical and reused // as the prefix of the second request, ensuring cache hit potential. let expected_user_message_2 = serde_json::json!({ "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "hello 2" } ] }); let expected_permissions_msg = body1["input"][0].clone(); let body1_input = body1["input"].as_array().expect("input array"); // After overriding the thread settings, emit one updated permissions message. let expected_permissions_msg_2 = body2["input"][body1_input.len()].clone(); assert_ne!( expected_permissions_msg_2, expected_permissions_msg, "expected updated permissions message after override" ); let expected_env_msg_2 = body2["input"][body1_input.len() + 1].clone(); assert_eq!(expected_env_msg_2["role"].as_str(), Some("user")); let env_text = expected_env_msg_2["content"][0]["text"] .as_str() .expect("environment context text"); assert_env_context_fragment(env_text); assert!( env_text.contains("<permission_profile type=\"managed\">") && env_text.contains("<file_system type=\"restricted\">") && env_text.contains(&format!( "<entry access=\"write\"><path>{}</path></entry>", writable.abs().display() )), "expected workspace-write filesystem profile in environment context: {env_text}" ); let mut expected_body2 = body1_input.to_vec(); expected_body2.push(expected_permissions_msg_2); expected_body2.push(expected_env_msg_2); expected_body2.push(expected_user_message_2); assert_eq!(body2["input"], serde_json::Value::Array(expected_body2)); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn override_before_first_turn_emits_environment_context() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = start_mock_server().await; let req = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), ) .await; let TestCodex { codex, .. } = test_codex().build(&server).await?; let collaboration_mode = CollaborationMode { mode: ModeKind::Default, settings: Settings { model: "gpt-5.4".to_string(), reasoning_effort: Some(ReasoningEffort::High), developer_instructions: None, }, }; core_test_support::submit_thread_settings( &codex, codex_protocol::protocol::ThreadSettingsOverrides { approval_policy: Some(AskForApproval::Never), model: Some("gpt-5.4".to_string()), effort: Some(Some(ReasoningEffort::Low)), collaboration_mode: Some(collaboration_mode), ..Default::default() }, ) .await?; codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "first message".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: Default::default(), }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let body = req.single_request().body_json(); assert_eq!(body["model"].as_str(), Some("gpt-5.4")); assert_eq!( body.get("reasoning") .and_then(|reasoning| reasoning.get("effort")) .and_then(|value| value.as_str()), Some("high") ); let input = body["input"] .as_array() .expect("input array must be present"); assert!( !input.is_empty(), "expected at least environment context and user message" ); let env_texts: Vec<&str> = input .iter() .filter_map(|msg| { msg["content"].as_array().map(|content| { content .iter() .filter_map(|item| item["text"].as_str()) .collect::<Vec<_>>() }) }) .flatten() .filter(|text| text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG)) .collect(); assert!( !env_texts.is_empty(), "expected environment context to be emitted: {env_texts:?}" ); assert!( env_texts .iter() .any(|text| text.contains("<current_date>") && text.contains("</current_date>")), "expected current_date in environment context: {env_texts:?}" ); assert!( env_texts .iter() .any(|text| text.contains("<timezone>") && text.contains("</timezone>")), "expected timezone in environment context: {env_texts:?}" ); let env_count = input .iter() .filter(|msg| { msg["content"] .as_array() .and_then(|content| { content.iter().find(|item| { item["type"].as_str() == Some("input_text") && item["text"] .as_str() .map(|text| text.starts_with(ENVIRONMENT_CONTEXT_OPEN_TAG)) .unwrap_or(false) }) }) .is_some() }) .count(); assert!( env_count >= 1, "environment context should appear at least once, found {env_count}" ); let permissions_texts: Vec<&str> = input .iter() .filter_map(|msg| { let role = msg["role"].as_str()?; if role != "developer" { return None; } msg["content"] .as_array() .and_then(|content| content.first()) .and_then(|item| item["text"].as_str()) }) .collect(); assert!( permissions_texts.iter().any(|text| { let lower = text.to_ascii_lowercase(); (lower.contains("approval policy") || lower.contains("approval_policy")) && lower.contains("never") }), "permissions message should reflect overridden approval policy: {permissions_texts:?}" ); let user_texts: Vec<&str> = input .iter() .filter_map(|msg| { msg["content"].as_array().map(|content| { content .iter() .filter_map(|item| item["text"].as_str()) .collect::<Vec<_>>() }) }) .flatten() .collect(); assert!( user_texts.contains(&"first message"), "expected user message text, got {user_texts:?}" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); use pretty_assertions::assert_eq; let server = start_mock_server().await; let req1 = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), ) .await; let req2 = mount_sse_once( &server, sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), ) .await; let TestCodex { codex, .. } = test_codex() .with_pre_build_hook(write_global_instructions) .with_config(|config| { config .features .enable(Feature::CollaborationModes) .expect("test config should allow feature update"); }) .build(&server) .await?; // First turn codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: Default::default(), }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; // Second turn using per-turn thread-settings overrides. let new_cwd = TempDir::new().unwrap(); let writable = TempDir::new().unwrap(); let permission_profile = PermissionProfile::workspace_write_with( &[writable.abs()], NetworkSandboxPolicy::Enabled, /*exclude_tmpdir_env_var*/ true, /*exclude_slash_tmp*/ true, ); let (sandbox_policy, permission_profile) = turn_permission_fields(permission_profile, new_cwd.path()); codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { environments: Some(local_selections(new_cwd.abs())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::High)), summary: Some(ReasoningSummary::Detailed), ..Default::default() }, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let request1 = req1.single_request(); let request2 = req2.single_request(); let body1 = request1.body_json(); let body2 = request2.body_json(); // prompt_cache_key should remain constant across per-turn overrides assert_eq!( body1["prompt_cache_key"], body2["prompt_cache_key"], "prompt_cache_key should not change across per-turn overrides" ); // The entire prefix from the first request should be identical and reused // as the prefix of the second request. let expected_user_message_2 = serde_json::json!({ "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "hello 2" } ] }); let expected_permissions_msg = body1["input"][0].clone(); let body1_input = body1["input"].as_array().expect("input array"); let expected_settings_update_msg = body2["input"][body1_input.len()].clone(); assert_ne!( expected_settings_update_msg, expected_permissions_msg, "expected updated permissions message after per-turn override" ); assert_eq!( expected_settings_update_msg["role"].as_str(), Some("developer") ); assert!( request2.has_message_with_input_texts("developer", |texts| { texts.iter().any(|text| text.contains("<model_switch>")) }), "expected model switch section after model override: {expected_settings_update_msg:?}" ); let expected_env_msg_2 = body2["input"][body1_input.len() + 1].clone(); assert_eq!(expected_env_msg_2["role"].as_str(), Some("user")); let env_text = expected_env_msg_2["content"][0]["text"] .as_str() .expect("environment context text"); let expected_cwd = new_cwd.path().display().to_string(); assert_default_env_context(env_text, &expected_cwd); let mut expected_body2 = body1_input.to_vec(); expected_body2.push(expected_settings_update_msg); expected_body2.push(expected_env_msg_2); expected_body2.push(expected_user_message_2); assert_eq!(body2["input"], serde_json::Value::Array(expected_body2)); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); use pretty_assertions::assert_eq; let server = start_mock_server().await; let req1 = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), ) .await; let req2 = mount_sse_once( &server, sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), ) .await; let TestCodex { codex, config, session_configured, .. } = test_codex() .with_pre_build_hook(write_global_instructions) .with_config(|config| { config .features .enable(Feature::CollaborationModes) .expect("test config should allow feature update"); }) .build(&server) .await?; let default_cwd = config.cwd.clone(); let default_approval_policy = config.permissions.approval_policy.value(); let default_sandbox_policy = &config.legacy_sandbox_policy(); let default_model = session_configured.model; let default_effort = config.model_reasoning_effort.clone(); let default_summary = config.model_reasoning_summary; codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { environments: Some(local_selections(default_cwd.clone())), approval_policy: Some(default_approval_policy), sandbox_policy: Some(default_sandbox_policy.clone()), summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { mode: codex_protocol::config_types::ModeKind::Default, settings: codex_protocol::config_types::Settings { model: default_model.clone(), reasoning_effort: default_effort.clone(), developer_instructions: None, }, }), ..Default::default() }, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { environments: Some(local_selections(default_cwd.clone())), approval_policy: Some(default_approval_policy), sandbox_policy: Some(default_sandbox_policy.clone()), summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { mode: codex_protocol::config_types::ModeKind::Default, settings: codex_protocol::config_types::Settings { model: default_model.clone(), reasoning_effort: default_effort, developer_instructions: None, }, }), ..Default::default() }, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let request1 = req1.single_request(); let request2 = req2.single_request(); let body1 = request1.body_json(); let body2 = request2.body_json(); let expected_permissions_msg = body1["input"][0].clone(); let expected_ui_msg = body1["input"][1].clone(); let default_cwd_lossy = default_cwd.to_string_lossy(); let expected_env_text_1 = expected_ui_msg["content"][1]["text"] .as_str() .expect("cached environment context text") .to_string(); assert_default_env_context(&expected_env_text_1, &default_cwd_lossy); let expected_contextual_user_msg_1 = text_user_input_parts(vec![ expected_ui_msg["content"][0]["text"] .as_str() .expect("cached user instructions text") .to_string(), expected_env_text_1, ]); let expected_user_message_1 = text_user_input("hello 1".to_string()); let expected_input_1 = serde_json::Value::Array(vec![ expected_permissions_msg.clone(), expected_contextual_user_msg_1.clone(), expected_user_message_1.clone(), ]); assert_eq!(body1["input"], expected_input_1); let expected_user_message_2 = text_user_input("hello 2".to_string()); let expected_input_2 = serde_json::Value::Array(vec![ expected_permissions_msg, expected_contextual_user_msg_1, expected_user_message_1, expected_user_message_2, ]); assert_eq!(body2["input"], expected_input_2); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); use pretty_assertions::assert_eq; let server = start_mock_server().await; let req1 = mount_sse_once( &server, sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), ) .await; let req2 = mount_sse_once( &server, sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), ) .await; let TestCodex { codex, config, session_configured, .. } = test_codex() .with_pre_build_hook(write_global_instructions) .with_config(|config| { config .features .enable(Feature::CollaborationModes) .expect("test config should allow feature update"); }) .build(&server) .await?; let default_cwd = config.cwd.clone(); let default_approval_policy = config.permissions.approval_policy.value(); let default_sandbox_policy = &config.legacy_sandbox_policy(); let default_model = session_configured.model; let default_effort = config.model_reasoning_effort.clone(); let default_summary = config.model_reasoning_summary; codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { environments: Some(local_selections(default_cwd.clone())), approval_policy: Some(default_approval_policy), sandbox_policy: Some(default_sandbox_policy.clone()), summary: Some(default_summary.unwrap_or(ReasoningSummary::Auto)), collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { mode: codex_protocol::config_types::ModeKind::Default, settings: codex_protocol::config_types::Settings { model: default_model, reasoning_effort: default_effort, developer_instructions: None, }, }), ..Default::default() }, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let (sandbox_policy, permission_profile) = turn_permission_fields(PermissionProfile::Disabled, default_cwd.as_path()); codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { environments: Some(local_selections(default_cwd.clone())), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, summary: Some(ReasoningSummary::Detailed), collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { mode: codex_protocol::config_types::ModeKind::Default, settings: codex_protocol::config_types::Settings { model: "o3".to_string(), reasoning_effort: Some(ReasoningEffort::High), developer_instructions: None, }, }), ..Default::default() }, }) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let request1 = req1.single_request(); let request2 = req2.single_request(); let body1 = request1.body_json(); let body2 = request2.body_json(); let expected_permissions_msg = body1["input"][0].clone(); let expected_ui_msg = body1["input"][1].clone(); let expected_env_text_1 = expected_ui_msg["content"][1]["text"] .as_str() .expect("cached environment context text") .to_string(); assert_default_env_context(&expected_env_text_1, &default_cwd.to_string_lossy()); let expected_contextual_user_msg_1 = text_user_input_parts(vec![ expected_ui_msg["content"][0]["text"] .as_str() .expect("cached user instructions text") .to_string(), expected_env_text_1, ]); let expected_user_message_1 = text_user_input("hello 1".to_string()); let expected_input_1 = serde_json::Value::Array(vec![ expected_permissions_msg.clone(), expected_contextual_user_msg_1.clone(), expected_user_message_1.clone(), ]); assert_eq!(body1["input"], expected_input_1); let body1_input = body1["input"].as_array().expect("input array"); let expected_settings_update_msg = body2["input"][body1_input.len()].clone(); assert_ne!( expected_settings_update_msg, expected_permissions_msg, "expected updated permissions message after policy change" ); assert_eq!( expected_settings_update_msg["role"].as_str(), Some("developer") ); assert!( request2.has_message_with_input_texts("developer", |texts| { texts.iter().any(|text| text.contains("<model_switch>")) }), "expected model switch section after model override: {expected_settings_update_msg:?}" ); let expected_env_update_msg = body2["input"][body1_input.len() + 1].clone(); assert_eq!(expected_env_update_msg["role"].as_str(), Some("user")); let expected_env_update_text = expected_env_update_msg["content"][0]["text"] .as_str() .expect("environment context text"); assert_env_context_fragment(expected_env_update_text); assert!( expected_env_update_text.contains( "<permission_profile type=\"disabled\"><file_system type=\"unrestricted\" /></permission_profile>", ), "expected disabled filesystem profile in environment context: {expected_env_update_text}" ); let expected_user_message_2 = text_user_input("hello 2".to_string()); let expected_input_2 = serde_json::Value::Array(vec![ expected_permissions_msg, expected_contextual_user_msg_1, expected_user_message_1, expected_settings_update_msg, expected_env_update_msg, expected_user_message_2, ]); assert_eq!(body2["input"], expected_input_2); Ok(())}