Merge branch 'main' into jif/profile-agents

This commit is contained in:
jif-oai
2026-03-20 17:40:34 +00:00
committed by GitHub
14 changed files with 713 additions and 146 deletions
+1
View File
@@ -3121,6 +3121,7 @@ dependencies = [
"base64 0.22.1",
"codex-arg0",
"codex-core",
"codex-exec-server",
"codex-features",
"codex-protocol",
"codex-utils-absolute-path",
+10 -1
View File
@@ -53,6 +53,7 @@ use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::config_loader::LoaderOverrides;
use codex_core::default_client::DEFAULT_ORIGINATOR;
use codex_core::default_client::SetOriginatorError;
use codex_core::default_client::USER_AGENT_SUFFIX;
use codex_core::default_client::get_codex_user_agent;
@@ -78,6 +79,7 @@ use toml::Value as TomlValue;
use tracing::Instrument;
const EXTERNAL_AUTH_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
const TUI_APP_SERVER_CLIENT_NAME: &str = "codex-tui";
#[derive(Clone)]
struct ExternalAuthRefreshBridge {
@@ -551,7 +553,14 @@ impl MessageProcessor {
} = params.client_info;
session.app_server_client_name = Some(name.clone());
session.client_version = Some(version.clone());
if let Err(error) = set_default_originator(name.clone()) {
let originator = if name == TUI_APP_SERVER_CLIENT_NAME {
// TODO: Remove this temporary workaround once app-server clients no longer
// need to retain the legacy TUI `codex_cli_rs` originator behavior.
DEFAULT_ORIGINATOR.to_string()
} else {
name.clone()
};
if let Err(error) = set_default_originator(originator) {
match error {
SetOriginatorError::InvalidHeaderValue => {
let error = JSONRPCErrorError {
+1
View File
@@ -13,6 +13,7 @@ assert_cmd = { workspace = true }
base64 = { workspace = true }
codex-arg0 = { workspace = true }
codex-core = { workspace = true }
codex-exec-server = { workspace = true }
codex-features = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
+23
View File
@@ -289,6 +289,29 @@ pub fn sandbox_network_env_var() -> &'static str {
codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR
}
const REMOTE_ENV_ENV_VAR: &str = "CODEX_TEST_REMOTE_ENV";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RemoteEnvConfig {
pub container_name: String,
}
pub fn get_remote_test_env() -> Option<RemoteEnvConfig> {
if std::env::var_os(REMOTE_ENV_ENV_VAR).is_none() {
eprintln!("Skipping test because {REMOTE_ENV_ENV_VAR} is not set.");
return None;
}
let container_name = std::env::var(REMOTE_ENV_ENV_VAR)
.unwrap_or_else(|_| panic!("{REMOTE_ENV_ENV_VAR} must be set"));
assert!(
!container_name.trim().is_empty(),
"{REMOTE_ENV_ENV_VAR} must not be empty"
);
Some(RemoteEnvConfig { container_name })
}
pub fn format_with_current_shell(command: &str) -> Vec<String> {
codex_core::shell::default_user_shell().derive_exec_args(command, /*use_login_shell*/ true)
}
+293 -3
View File
@@ -1,10 +1,16 @@
use std::mem::swap;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use codex_core::CodexAuth;
use codex_core::CodexThread;
use codex_core::ModelProviderInfo;
@@ -14,6 +20,8 @@ use codex_core::config::Config;
use codex_core::models_manager::collaboration_mode_presets::CollaborationModesConfig;
use codex_core::shell::Shell;
use codex_core::shell::get_shell_by_model_provided_path;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::ExecutorFileSystem;
use codex_features::Feature;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::openai_models::ModelsResponse;
@@ -24,10 +32,13 @@ use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::SessionConfiguredEvent;
use codex_protocol::protocol::SessionSource;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde_json::Value;
use tempfile::TempDir;
use wiremock::MockServer;
use crate::RemoteEnvConfig;
use crate::get_remote_test_env;
use crate::load_default_config_for_test;
use crate::responses::WebSocketTestServer;
use crate::responses::output_value_to_text;
@@ -41,6 +52,254 @@ use wiremock::matchers::path_regex;
type ConfigMutator = dyn FnOnce(&mut Config) + Send;
type PreBuildHook = dyn FnOnce(&Path) + Send + 'static;
const TEST_MODEL_WITH_EXPERIMENTAL_TOOLS: &str = "test-gpt-5.1-codex";
const REMOTE_EXEC_SERVER_START_TIMEOUT: Duration = Duration::from_secs(5);
const REMOTE_EXEC_SERVER_POLL_INTERVAL: Duration = Duration::from_millis(25);
static REMOTE_EXEC_SERVER_INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0);
#[derive(Debug)]
struct RemoteExecServerProcess {
container_name: String,
pid: u32,
remote_exec_server_path: String,
stdout_path: String,
cleanup_paths: Vec<String>,
}
impl Drop for RemoteExecServerProcess {
fn drop(&mut self) {
let cleanup_paths = self.cleanup_paths.join(" ");
let cleanup_paths_script = if cleanup_paths.is_empty() {
String::new()
} else {
format!("rm -rf {cleanup_paths}; ")
};
let script = format!(
"if kill -0 {pid} 2>/dev/null; then kill {pid}; fi; {cleanup_paths_script}rm -f {remote_exec_server_path} {stdout_path}",
pid = self.pid,
cleanup_paths_script = cleanup_paths_script,
remote_exec_server_path = self.remote_exec_server_path,
stdout_path = self.stdout_path
);
let _ = docker_command_capture_stdout(["exec", &self.container_name, "sh", "-lc", &script]);
}
}
impl RemoteExecServerProcess {
fn register_cleanup_path(&mut self, path: &Path) {
self.cleanup_paths.push(path.display().to_string());
}
}
#[derive(Debug)]
pub struct TestEnv {
environment: codex_exec_server::Environment,
cwd: PathBuf,
_local_cwd_temp_dir: Option<TempDir>,
_remote_exec_server_process: Option<RemoteExecServerProcess>,
}
impl TestEnv {
pub async fn local() -> Result<Self> {
let local_cwd_temp_dir = TempDir::new()?;
let cwd = local_cwd_temp_dir.path().to_path_buf();
let environment =
codex_exec_server::Environment::create(/*experimental_exec_server_url*/ None).await?;
Ok(Self {
environment,
cwd,
_local_cwd_temp_dir: Some(local_cwd_temp_dir),
_remote_exec_server_process: None,
})
}
pub fn environment(&self) -> &codex_exec_server::Environment {
&self.environment
}
pub fn experimental_exec_server_url(&self) -> Option<&str> {
self.environment.experimental_exec_server_url()
}
}
pub async fn test_env() -> Result<TestEnv> {
match get_remote_test_env() {
Some(remote_env) => {
let mut remote_process = start_remote_exec_server(&remote_env)?;
let remote_ip = remote_container_ip(&remote_env.container_name)?;
let websocket_url = rewrite_websocket_host(&remote_process.listen_url, &remote_ip)?;
let environment = codex_exec_server::Environment::create(Some(websocket_url)).await?;
let cwd = remote_aware_cwd_path();
environment
.get_filesystem()
.create_directory(
&absolute_path(&cwd)?,
CreateDirectoryOptions { recursive: true },
)
.await?;
remote_process.process.register_cleanup_path(&cwd);
Ok(TestEnv {
environment,
cwd,
_local_cwd_temp_dir: None,
_remote_exec_server_process: Some(remote_process.process),
})
}
None => TestEnv::local().await,
}
}
struct RemoteExecServerStart {
process: RemoteExecServerProcess,
listen_url: String,
}
fn start_remote_exec_server(remote_env: &RemoteEnvConfig) -> Result<RemoteExecServerStart> {
let container_name = remote_env.container_name.as_str();
let instance_id = remote_exec_server_instance_id();
let remote_exec_server_path = format!("/tmp/codex-exec-server-{instance_id}");
let stdout_path = format!("/tmp/codex-exec-server-{instance_id}.stdout");
let local_binary = codex_utils_cargo_bin::cargo_bin("codex-exec-server")
.context("resolve codex-exec-server binary")?;
let local_binary = local_binary.to_string_lossy().to_string();
let remote_binary = format!("{container_name}:{remote_exec_server_path}");
docker_command_success(["cp", &local_binary, &remote_binary])?;
docker_command_success([
"exec",
container_name,
"chmod",
"+x",
&remote_exec_server_path,
])?;
let start_script = format!(
"rm -f {stdout_path}; \
nohup {remote_exec_server_path} --listen ws://0.0.0.0:0 > {stdout_path} 2>&1 & \
echo $!"
);
let pid_output =
docker_command_capture_stdout(["exec", container_name, "sh", "-lc", &start_script])?;
let pid = pid_output
.trim()
.parse::<u32>()
.with_context(|| format!("parse remote exec-server PID from {pid_output:?}"))?;
let listen_url = wait_for_remote_listen_url(container_name, &stdout_path)?;
Ok(RemoteExecServerStart {
process: RemoteExecServerProcess {
container_name: container_name.to_string(),
pid,
remote_exec_server_path,
stdout_path,
cleanup_paths: Vec::new(),
},
listen_url,
})
}
fn remote_aware_cwd_path() -> PathBuf {
PathBuf::from(format!(
"/tmp/codex-core-test-cwd-{}",
remote_exec_server_instance_id()
))
}
fn wait_for_remote_listen_url(container_name: &str, stdout_path: &str) -> Result<String> {
let deadline = Instant::now() + REMOTE_EXEC_SERVER_START_TIMEOUT;
loop {
let line = docker_command_capture_stdout([
"exec",
container_name,
"sh",
"-lc",
&format!("head -n 1 {stdout_path} 2>/dev/null || true"),
])?;
let listen_url = line.trim();
if listen_url.starts_with("ws://") {
return Ok(listen_url.to_string());
}
if Instant::now() >= deadline {
return Err(anyhow!(
"timed out waiting for remote exec-server listen URL in container `{container_name}` after {REMOTE_EXEC_SERVER_START_TIMEOUT:?}"
));
}
std::thread::sleep(REMOTE_EXEC_SERVER_POLL_INTERVAL);
}
}
fn remote_exec_server_instance_id() -> String {
let instance = REMOTE_EXEC_SERVER_INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{}-{instance}", std::process::id())
}
fn remote_container_ip(container_name: &str) -> Result<String> {
let ip = docker_command_capture_stdout([
"inspect",
"-f",
"{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}",
container_name,
])?;
let ip = ip.trim();
if ip.is_empty() {
return Err(anyhow!(
"container `{container_name}` has no IP address; cannot connect to remote exec-server"
));
}
Ok(ip.to_string())
}
fn rewrite_websocket_host(listen_url: &str, host: &str) -> Result<String> {
let Some(address) = listen_url.strip_prefix("ws://") else {
return Err(anyhow!(
"unexpected websocket listen URL `{listen_url}`; expected ws://IP:PORT"
));
};
let Some((_, port)) = address.rsplit_once(':') else {
return Err(anyhow!(
"unexpected websocket listen URL `{listen_url}`; expected ws://IP:PORT"
));
};
Ok(format!("ws://{host}:{port}"))
}
fn docker_command_success<const N: usize>(args: [&str; N]) -> Result<()> {
let output = Command::new("docker")
.args(args)
.output()
.with_context(|| format!("run docker {:?}", args))?;
if !output.status.success() {
return Err(anyhow!(
"docker {:?} failed: stdout={} stderr={}",
args,
String::from_utf8_lossy(&output.stdout).trim(),
String::from_utf8_lossy(&output.stderr).trim()
));
}
Ok(())
}
fn docker_command_capture_stdout<const N: usize>(args: [&str; N]) -> Result<String> {
let output = Command::new("docker")
.args(args)
.output()
.with_context(|| format!("run docker {:?}", args))?;
if !output.status.success() {
return Err(anyhow!(
"docker {:?} failed: stdout={} stderr={}",
args,
String::from_utf8_lossy(&output.stdout).trim(),
String::from_utf8_lossy(&output.stderr).trim()
));
}
String::from_utf8(output.stdout).context("docker stdout must be utf-8")
}
fn absolute_path(path: &Path) -> Result<AbsolutePathBuf> {
AbsolutePathBuf::try_from(path.to_path_buf())
.map_err(|err| anyhow!("invalid absolute path {}: {err}", path.display()))
}
/// A collection of different ways the model can output an apply_patch call
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@@ -124,6 +383,24 @@ impl TestCodexBuilder {
Box::pin(self.build_with_home(server, home, /*resume_from*/ None)).await
}
pub async fn build_remote_aware(
&mut self,
server: &wiremock::MockServer,
) -> anyhow::Result<TestCodex> {
let test_env = test_env().await?;
let experimental_exec_server_url =
test_env.experimental_exec_server_url().map(str::to_owned);
let cwd = test_env.cwd.to_path_buf();
self.config_mutators.push(Box::new(move |config| {
config.experimental_exec_server_url = experimental_exec_server_url;
config.cwd = cwd;
}));
let mut test = self.build(server).await?;
test._test_env = test_env;
Ok(test)
}
pub async fn build_with_streaming_server(
&mut self,
server: &StreamingSseServer,
@@ -176,7 +453,8 @@ impl TestCodexBuilder {
) -> anyhow::Result<TestCodex> {
let base_url = format!("{}/v1", server.uri());
let (config, cwd) = self.prepare_config(base_url, &home).await?;
Box::pin(self.build_from_config(config, cwd, home, resume_from)).await
Box::pin(self.build_from_config(config, cwd, home, resume_from, TestEnv::local().await?))
.await
}
async fn build_with_home_and_base_url(
@@ -186,7 +464,8 @@ impl TestCodexBuilder {
resume_from: Option<PathBuf>,
) -> anyhow::Result<TestCodex> {
let (config, cwd) = self.prepare_config(base_url, &home).await?;
Box::pin(self.build_from_config(config, cwd, home, resume_from)).await
Box::pin(self.build_from_config(config, cwd, home, resume_from, TestEnv::local().await?))
.await
}
async fn build_from_config(
@@ -195,6 +474,7 @@ impl TestCodexBuilder {
cwd: Arc<TempDir>,
home: Arc<TempDir>,
resume_from: Option<PathBuf>,
test_env: TestEnv,
) -> anyhow::Result<TestCodex> {
let auth = self.auth.clone();
let thread_manager = if config.model_catalog.is_some() {
@@ -258,6 +538,7 @@ impl TestCodexBuilder {
codex: new_conversation.thread,
session_configured: new_conversation.session_configured,
thread_manager,
_test_env: test_env,
})
}
@@ -354,6 +635,7 @@ pub struct TestCodex {
pub session_configured: SessionConfiguredEvent,
pub config: Config,
pub thread_manager: Arc<ThreadManager>,
_test_env: TestEnv,
}
impl TestCodex {
@@ -369,6 +651,14 @@ impl TestCodex {
self.cwd_path().join(rel)
}
pub fn executor_environment(&self) -> &TestEnv {
&self._test_env
}
pub fn fs(&self) -> Arc<dyn ExecutorFileSystem> {
self._test_env.environment().get_filesystem()
}
pub async fn submit_turn(&self, prompt: &str) -> Result<()> {
self.submit_turn_with_policies(
prompt,
@@ -431,7 +721,7 @@ impl TestCodex {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: self.cwd.path().to_path_buf(),
cwd: self.config.cwd.clone(),
approval_policy,
sandbox_policy,
model: session_model,
+3 -8
View File
@@ -2277,14 +2277,9 @@ async fn code_mode_can_call_hidden_dynamic_tools() -> Result<()> {
false,
)
.await?;
let test = TestCodex {
home: base_test.home,
cwd: base_test.cwd,
codex: new_thread.thread,
session_configured: new_thread.session_configured,
config: base_test.config,
thread_manager: base_test.thread_manager,
};
let mut test = base_test;
test.codex = new_thread.thread;
test.session_configured = new_thread.session_configured;
let code = r#"
import { ALL_TOOLS, hidden_dynamic_tool } from "tools.js";
+1
View File
@@ -103,6 +103,7 @@ mod prompt_caching;
mod quota_exceeded;
mod read_file;
mod realtime_conversation;
mod remote_env;
mod remote_models;
mod request_compression;
#[cfg(not(target_os = "windows"))]
+57
View File
@@ -0,0 +1,57 @@
use anyhow::Result;
use codex_exec_server::RemoveOptions;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::get_remote_test_env;
use core_test_support::test_codex::test_env;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn remote_test_env_can_connect_and_use_filesystem() -> Result<()> {
let Some(_remote_env) = get_remote_test_env() else {
return Ok(());
};
let test_env = test_env().await?;
let file_system = test_env.environment().get_filesystem();
let file_path = remote_test_file_path();
let file_path_abs = absolute_path(file_path.clone())?;
let payload = b"remote-test-env-ok".to_vec();
file_system
.write_file(&file_path_abs, payload.clone())
.await?;
let actual = file_system.read_file(&file_path_abs).await?;
assert_eq!(actual, payload);
file_system
.remove(
&file_path_abs,
RemoveOptions {
recursive: false,
force: true,
},
)
.await?;
Ok(())
}
fn absolute_path(path: PathBuf) -> Result<AbsolutePathBuf> {
AbsolutePathBuf::try_from(path.clone())
.map_err(|err| anyhow::anyhow!("invalid absolute path {}: {err}", path.display()))
}
fn remote_test_file_path() -> PathBuf {
let nanos = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(duration) => duration.as_nanos(),
Err(_) => 0,
};
PathBuf::from(format!(
"/tmp/codex-remote-test-env-{}-{nanos}.txt",
std::process::id()
))
}
+175 -111
View File
@@ -3,6 +3,7 @@
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_core::CodexAuth;
use codex_exec_server::CreateDirectoryOptions;
use codex_features::Feature;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::openai_models::ConfigShellToolType;
@@ -32,12 +33,16 @@ use core_test_support::test_codex::TestCodex;
use core_test_support::test_codex::test_codex;
use core_test_support::wait_for_event;
use core_test_support::wait_for_event_with_timeout;
use image::DynamicImage;
use image::GenericImageView;
use image::ImageBuffer;
use image::Rgba;
use image::load_from_memory;
use pretty_assertions::assert_eq;
use serde_json::Value;
use std::io::Cursor;
use std::path::Path;
use std::path::PathBuf;
use tokio::time::Duration;
use wiremock::BodyPrintLimit;
use wiremock::MockServer;
@@ -73,26 +78,78 @@ fn find_image_message(body: &Value) -> Option<&Value> {
image_messages(body).into_iter().next()
}
fn absolute_path(path: &Path) -> anyhow::Result<codex_utils_absolute_path::AbsolutePathBuf> {
codex_utils_absolute_path::AbsolutePathBuf::try_from(path.to_path_buf())
.map_err(|err| anyhow::anyhow!("invalid absolute path {}: {err}", path.display()))
}
fn png_bytes(width: u32, height: u32, rgba: [u8; 4]) -> anyhow::Result<Vec<u8>> {
let image = ImageBuffer::from_pixel(width, height, Rgba(rgba));
let mut cursor = Cursor::new(Vec::new());
DynamicImage::ImageRgba8(image).write_to(&mut cursor, image::ImageFormat::Png)?;
Ok(cursor.into_inner())
}
async fn create_workspace_directory(test: &TestCodex, rel_path: &str) -> anyhow::Result<PathBuf> {
let abs_path = test.config.cwd.join(rel_path);
test.fs()
.create_directory(
&absolute_path(&abs_path)?,
CreateDirectoryOptions { recursive: true },
)
.await?;
Ok(abs_path)
}
async fn write_workspace_file(
test: &TestCodex,
rel_path: &str,
contents: Vec<u8>,
) -> anyhow::Result<PathBuf> {
let abs_path = test.config.cwd.join(rel_path);
if let Some(parent) = abs_path.parent() {
test.fs()
.create_directory(
&absolute_path(parent)?,
CreateDirectoryOptions { recursive: true },
)
.await?;
}
test.fs()
.write_file(&absolute_path(&abs_path)?, contents)
.await?;
Ok(abs_path)
}
async fn write_workspace_png(
test: &TestCodex,
rel_path: &str,
width: u32,
height: u32,
rgba: [u8; 4],
) -> anyhow::Result<PathBuf> {
write_workspace_file(test, rel_path, png_bytes(width, height, rgba)?).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex();
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
config,
session_configured,
..
} = test_codex().build(&server).await?;
} = &test;
let rel_path = "user-turn/example.png";
let abs_path = cwd.path().join(rel_path);
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?;
}
let original_width = 2304;
let original_height = 864;
let local_image_dir = tempfile::tempdir()?;
let abs_path = local_image_dir.path().join("example.png");
let image = ImageBuffer::from_pixel(original_width, original_height, Rgba([20u8, 40, 60, 255]));
image.save(&abs_path)?;
@@ -111,7 +168,7 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> {
path: abs_path.clone(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
@@ -124,7 +181,7 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> {
.await?;
wait_for_event_with_timeout(
&codex,
codex,
|event| matches!(event, EventMsg::TurnComplete(_)),
// Empirically, image attachment can be slow under Bazel/RBE.
Duration::from_secs(10),
@@ -171,23 +228,28 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let mut builder = test_codex();
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
session_configured,
config,
..
} = test_codex().build(&server).await?;
} = &test;
let cwd = config.cwd.clone();
let rel_path = "assets/example.png";
let abs_path = cwd.path().join(rel_path);
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?;
}
let abs_path = cwd.join(rel_path);
let original_width = 2304;
let original_height = 864;
let image = ImageBuffer::from_pixel(original_width, original_height, Rgba([255u8, 0, 0, 255]));
image.save(&abs_path)?;
write_workspace_png(
&test,
rel_path,
original_width,
original_height,
[255u8, 0, 0, 255],
)
.await?;
let call_id = "view-image-call";
let arguments = serde_json::json!({ "path": rel_path }).to_string();
@@ -214,7 +276,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
@@ -228,7 +290,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
let mut tool_event = None;
wait_for_event_with_timeout(
&codex,
codex,
|event| match event {
EventMsg::ViewImageToolCall(_) => {
tool_event = Some(event.clone());
@@ -309,22 +371,25 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5
.enable(Feature::ImageDetailOriginal)
.expect("test config should allow feature update");
});
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
config,
session_configured,
..
} = builder.build(&server).await?;
} = &test;
let rel_path = "assets/original-example.png";
let abs_path = cwd.path().join(rel_path);
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?;
}
let original_width = 2304;
let original_height = 864;
let image = ImageBuffer::from_pixel(original_width, original_height, Rgba([0u8, 80, 255, 255]));
image.save(&abs_path)?;
write_workspace_png(
&test,
rel_path,
original_width,
original_height,
[0u8, 80, 255, 255],
)
.await?;
let call_id = "view-image-original";
let arguments = serde_json::json!({ "path": rel_path, "detail": "original" }).to_string();
@@ -351,7 +416,7 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
@@ -364,7 +429,7 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5
.await?;
wait_for_event_with_timeout(
&codex,
codex,
|event| matches!(event, EventMsg::TurnComplete(_)),
Duration::from_secs(10),
)
@@ -413,20 +478,16 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho
.enable(Feature::ImageDetailOriginal)
.expect("test config should allow feature update");
});
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
config,
session_configured,
..
} = builder.build(&server).await?;
} = &test;
let rel_path = "assets/unsupported-detail.png";
let abs_path = cwd.path().join(rel_path);
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?;
}
let image = ImageBuffer::from_pixel(256, 128, Rgba([0u8, 80, 255, 255]));
image.save(&abs_path)?;
write_workspace_png(&test, rel_path, 256, 128, [0u8, 80, 255, 255]).await?;
let call_id = "view-image-unsupported-detail";
let arguments = serde_json::json!({ "path": rel_path, "detail": "low" }).to_string();
@@ -453,7 +514,7 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
@@ -465,7 +526,7 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho
})
.await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let req = mock.single_request();
let body_with_tool_output = req.body_json();
@@ -499,22 +560,25 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> {
.enable(Feature::ImageDetailOriginal)
.expect("test config should allow feature update");
});
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
config,
session_configured,
..
} = builder.build(&server).await?;
} = &test;
let rel_path = "assets/null-detail.png";
let abs_path = cwd.path().join(rel_path);
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?;
}
let original_width = 2304;
let original_height = 864;
let image = ImageBuffer::from_pixel(original_width, original_height, Rgba([0u8, 80, 255, 255]));
image.save(&abs_path)?;
write_workspace_png(
&test,
rel_path,
original_width,
original_height,
[0u8, 80, 255, 255],
)
.await?;
let call_id = "view-image-null-detail";
let arguments = serde_json::json!({ "path": rel_path, "detail": null }).to_string();
@@ -541,7 +605,7 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
@@ -553,7 +617,7 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> {
})
.await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let req = mock.single_request();
let function_output = req.function_call_output(call_id);
@@ -595,22 +659,25 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a
.enable(Feature::ImageDetailOriginal)
.expect("test config should allow feature update");
});
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
config,
session_configured,
..
} = builder.build(&server).await?;
} = &test;
let rel_path = "assets/original-example-lower-model.png";
let abs_path = cwd.path().join(rel_path);
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?;
}
let original_width = 2304;
let original_height = 864;
let image = ImageBuffer::from_pixel(original_width, original_height, Rgba([0u8, 80, 255, 255]));
image.save(&abs_path)?;
write_workspace_png(
&test,
rel_path,
original_width,
original_height,
[0u8, 80, 255, 255],
)
.await?;
let call_id = "view-image-original-lower-model";
let arguments = serde_json::json!({ "path": rel_path }).to_string();
@@ -637,7 +704,7 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
@@ -650,7 +717,7 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a
.await?;
wait_for_event_with_timeout(
&codex,
codex,
|event| matches!(event, EventMsg::TurnComplete(_)),
Duration::from_secs(10),
)
@@ -702,22 +769,25 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_feat
.enable(Feature::ImageDetailOriginal)
.expect("test config should allow feature update");
});
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
config,
session_configured,
..
} = builder.build(&server).await?;
} = &test;
let rel_path = "assets/original-example-capability-only.png";
let abs_path = cwd.path().join(rel_path);
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?;
}
let original_width = 2304;
let original_height = 864;
let image = ImageBuffer::from_pixel(original_width, original_height, Rgba([0u8, 80, 255, 255]));
image.save(&abs_path)?;
write_workspace_png(
&test,
rel_path,
original_width,
original_height,
[0u8, 80, 255, 255],
)
.await?;
let call_id = "view-image-capability-only";
let arguments = serde_json::json!({ "path": rel_path }).to_string();
@@ -744,7 +814,7 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_feat
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
@@ -757,7 +827,7 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_feat
.await?;
wait_for_event_with_timeout(
&codex,
codex,
|event| matches!(event, EventMsg::TurnComplete(_)),
Duration::from_secs(10),
)
@@ -1019,16 +1089,17 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex();
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
config,
session_configured,
..
} = test_codex().build(&server).await?;
} = &test;
let rel_path = "assets";
let abs_path = cwd.path().join(rel_path);
std::fs::create_dir_all(&abs_path)?;
let abs_path = create_workspace_directory(&test, rel_path).await?;
let call_id = "view-image-directory";
let arguments = serde_json::json!({ "path": rel_path }).to_string();
@@ -1055,7 +1126,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
@@ -1067,7 +1138,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
})
.await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let req = mock.single_request();
let body_with_tool_output = req.body_json();
@@ -1092,19 +1163,18 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex();
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
config,
session_configured,
..
} = test_codex().build(&server).await?;
} = &test;
let rel_path = "assets/example.json";
let abs_path = cwd.path().join(rel_path);
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&abs_path, br#"{ "message": "hello" }"#)?;
let abs_path =
write_workspace_file(&test, rel_path, br#"{ "message": "hello" }"#.to_vec()).await?;
let call_id = "view-image-non-image";
let arguments = serde_json::json!({ "path": rel_path }).to_string();
@@ -1131,7 +1201,7 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
@@ -1143,7 +1213,7 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
})
.await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let request = mock.single_request();
assert!(
@@ -1174,15 +1244,17 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
let server = start_mock_server().await;
let mut builder = test_codex();
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
config,
session_configured,
..
} = test_codex().build(&server).await?;
} = &test;
let rel_path = "missing/example.png";
let abs_path = cwd.path().join(rel_path);
let abs_path = config.cwd.join(rel_path);
let call_id = "view-image-missing";
let arguments = serde_json::json!({ "path": rel_path }).to_string();
@@ -1209,7 +1281,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
@@ -1221,7 +1293,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
})
.await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let req = mock.single_request();
let body_with_tool_output = req.body_json();
@@ -1298,21 +1370,16 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
)
.await;
let TestCodex { codex, cwd, .. } = test_codex()
let mut builder = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(|config| {
config.model = Some(model_slug.to_string());
})
.build(&server)
.await?;
});
let test = builder.build_remote_aware(&server).await?;
let TestCodex { codex, config, .. } = &test;
let rel_path = "assets/example.png";
let abs_path = cwd.path().join(rel_path);
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?;
}
let image = ImageBuffer::from_pixel(20, 20, Rgba([255u8, 0, 0, 255]));
image.save(&abs_path)?;
write_workspace_png(&test, rel_path, 20, 20, [255u8, 0, 0, 255]).await?;
let call_id = "view-image-unsupported-model";
let arguments = serde_json::json!({ "path": rel_path }).to_string();
@@ -1336,7 +1403,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: model_slug.to_string(),
@@ -1348,7 +1415,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
})
.await?;
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
let output_text = mock
.single_request()
@@ -1390,20 +1457,17 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()>
let completion_mock = responses::mount_sse_once(&server, success_response).await;
let mut builder = test_codex();
let test = builder.build_remote_aware(&server).await?;
let TestCodex {
codex,
cwd,
config,
session_configured,
..
} = test_codex().build(&server).await?;
} = &test;
let rel_path = "assets/poisoned.png";
let abs_path = cwd.path().join(rel_path);
if let Some(parent) = abs_path.parent() {
std::fs::create_dir_all(parent)?;
}
let image = ImageBuffer::from_pixel(1024, 512, Rgba([10u8, 20, 30, 255]));
image.save(&abs_path)?;
let abs_path = write_workspace_png(&test, rel_path, 1024, 512, [10u8, 20, 30, 255]).await?;
let session_model = session_configured.model.clone();
@@ -1413,7 +1477,7 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()>
path: abs_path.clone(),
}],
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
cwd: config.cwd.clone(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,