Merge branch 'main' into nicholasclark/2026-03-23-server-spans

This commit is contained in:
nicholasclark-openai
2026-03-25 09:52:21 -07:00
committed by GitHub
506 changed files with 27632 additions and 12180 deletions
@@ -185,6 +185,11 @@ impl Respond for CodexAppsJsonRpcResponder {
{
"name": "calendar_create_event",
"description": "Create a calendar event.",
"annotations": {
"readOnlyHint": false,
"destructiveHint": false,
"openWorldHint": false
},
"inputSchema": {
"type": "object",
"properties": {
@@ -209,6 +214,9 @@ impl Respond for CodexAppsJsonRpcResponder {
{
"name": "calendar_list_events",
"description": "List calendar events.",
"annotations": {
"readOnlyHint": true
},
"inputSchema": {
"type": "object",
"properties": {
@@ -241,6 +249,9 @@ impl Respond for CodexAppsJsonRpcResponder {
tools.push(json!({
"name": format!("calendar_timezone_option_{index}"),
"description": format!("Read timezone option {index}."),
"annotations": {
"readOnlyHint": true
},
"inputSchema": {
"type": "object",
"properties": {
+31
View File
@@ -14,6 +14,7 @@ use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
use codex_utils_absolute_path::AbsolutePathBuf;
use regex_lite::Regex;
use std::path::Path;
use std::path::PathBuf;
pub mod apps_test_server;
@@ -104,6 +105,36 @@ pub fn test_absolute_path(unix_path: &str) -> AbsolutePathBuf {
test_absolute_path_with_windows(unix_path, /*windows_path*/ None)
}
pub trait PathExt {
fn abs(&self) -> AbsolutePathBuf;
}
impl PathExt for Path {
fn abs(&self) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(self.to_path_buf()).expect("path should already be absolute")
}
}
pub trait PathBufExt {
fn abs(&self) -> AbsolutePathBuf;
}
impl PathBufExt for PathBuf {
fn abs(&self) -> AbsolutePathBuf {
self.as_path().abs()
}
}
pub trait TempDirExt {
fn abs(&self) -> AbsolutePathBuf;
}
impl TempDirExt for TempDir {
fn abs(&self) -> AbsolutePathBuf {
self.path().abs()
}
}
pub fn test_tmp_path() -> AbsolutePathBuf {
test_absolute_path_with_windows("/tmp", Some(r"C:\Users\codex\AppData\Local\Temp"))
}
+9 -11
View File
@@ -37,7 +37,10 @@ use serde_json::Value;
use tempfile::TempDir;
use wiremock::MockServer;
use crate::PathBufExt;
use crate::PathExt;
use crate::RemoteEnvConfig;
use crate::TempDirExt;
use crate::get_remote_test_env;
use crate::load_default_config_for_test;
use crate::responses::WebSocketTestServer;
@@ -297,8 +300,7 @@ fn docker_command_capture_stdout<const N: usize>(args: [&str; N]) -> Result<Stri
}
fn absolute_path(path: &Path) -> Result<AbsolutePathBuf> {
AbsolutePathBuf::try_from(path.to_path_buf())
.map_err(|err| anyhow!("invalid absolute path {}: {err}", path.display()))
Ok(path.abs())
}
/// A collection of different ways the model can output an apply_patch call
@@ -393,7 +395,7 @@ impl TestCodexBuilder {
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;
config.cwd = cwd.abs();
}));
let mut test = self.build(server).await?;
@@ -556,7 +558,7 @@ impl TestCodexBuilder {
};
let cwd = Arc::new(TempDir::new()?);
let mut config = load_default_config_for_test(home).await;
config.cwd = cwd.path().to_path_buf();
config.cwd = cwd.abs();
config.model_provider = model_provider;
for hook in self.pre_build_hooks.drain(..) {
hook(home.path());
@@ -616,12 +618,7 @@ fn ensure_test_model_catalog(config: &mut Config) -> Result<()> {
.unwrap_or_else(|| panic!("missing bundled model gpt-5.1-codex"));
model.slug = TEST_MODEL_WITH_EXPERIMENTAL_TOOLS.to_string();
model.display_name = TEST_MODEL_WITH_EXPERIMENTAL_TOOLS.to_string();
model.experimental_supported_tools = vec![
"test_sync_tool".to_string(),
"read_file".to_string(),
"grep_files".to_string(),
"list_dir".to_string(),
];
model.experimental_supported_tools = vec!["test_sync_tool".to_string()];
config.model_catalog = Some(ModelsResponse {
models: vec![model],
});
@@ -721,8 +718,9 @@ impl TestCodex {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: self.config.cwd.clone(),
cwd: self.config.cwd.to_path_buf(),
approval_policy,
approvals_reviewer: None,
sandbox_policy,
model: session_model,
effort: None,
@@ -310,6 +310,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
@@ -919,6 +920,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<(
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
@@ -1000,6 +1002,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() ->
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
@@ -1151,6 +1154,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff(
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
@@ -1215,6 +1219,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change(
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
@@ -1287,6 +1292,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()>
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
@@ -1359,6 +1365,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
+1
View File
@@ -558,6 +558,7 @@ async fn submit_turn(
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy,
approvals_reviewer: None,
sandbox_policy,
model: session_model,
effort: None,
+166 -10
View File
@@ -294,13 +294,14 @@ async fn returns_fresh_tokens_as_is() -> Result<()> {
.await;
let ctx = RefreshTokenTestContext::new(&server)?;
let initial_last_refresh = Utc::now() - Duration::days(1);
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
let stale_refresh = Utc::now() - Duration::days(9);
let fresh_access_token = access_token_with_expiration(Utc::now() + Duration::hours(1));
let initial_tokens = build_tokens(&fresh_access_token, INITIAL_REFRESH_TOKEN);
let initial_auth = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(initial_tokens.clone()),
last_refresh: Some(initial_last_refresh),
last_refresh: Some(stale_refresh),
};
ctx.write_auth(&initial_auth)?;
@@ -325,7 +326,7 @@ async fn returns_fresh_tokens_as_is() -> Result<()> {
#[serial_test::serial(auth_refresh)]
#[tokio::test]
async fn refreshes_token_when_last_refresh_is_stale() -> Result<()> {
async fn refreshes_token_when_access_token_is_expired() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
@@ -340,13 +341,14 @@ async fn refreshes_token_when_last_refresh_is_stale() -> Result<()> {
.await;
let ctx = RefreshTokenTestContext::new(&server)?;
let stale_refresh = Utc::now() - Duration::days(9);
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
let fresh_refresh = Utc::now() - Duration::days(1);
let expired_access_token = access_token_with_expiration(Utc::now() - Duration::hours(1));
let initial_tokens = build_tokens(&expired_access_token, INITIAL_REFRESH_TOKEN);
let initial_auth = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(initial_tokens.clone()),
last_refresh: Some(stale_refresh),
last_refresh: Some(fresh_refresh),
};
ctx.write_auth(&initial_auth)?;
@@ -373,7 +375,7 @@ async fn refreshes_token_when_last_refresh_is_stale() -> Result<()> {
.as_ref()
.context("last_refresh should be recorded")?;
assert!(
*refreshed_at >= stale_refresh,
*refreshed_at >= fresh_refresh,
"last_refresh should advance"
);
@@ -543,6 +545,153 @@ async fn refresh_token_returns_permanent_error_for_expired_refresh_token() -> Re
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[tokio::test]
async fn refresh_token_does_not_retry_after_permanent_failure() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/oauth/token"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": {
"code": "refresh_token_reused"
}
})))
.expect(1)
.mount(&server)
.await;
let ctx = RefreshTokenTestContext::new(&server)?;
let initial_last_refresh = Utc::now() - Duration::days(1);
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
let initial_auth = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(initial_tokens.clone()),
last_refresh: Some(initial_last_refresh),
};
ctx.write_auth(&initial_auth)?;
let first_err = ctx
.auth_manager
.refresh_token()
.await
.err()
.context("first refresh should fail")?;
assert_eq!(
first_err.failed_reason(),
Some(RefreshTokenFailedReason::Exhausted)
);
let second_err = ctx
.auth_manager
.refresh_token()
.await
.err()
.context("second refresh should fail without retrying")?;
assert_eq!(
second_err.failed_reason(),
Some(RefreshTokenFailedReason::Exhausted)
);
let stored = ctx.load_auth()?;
assert_eq!(stored, initial_auth);
let cached_auth = ctx
.auth_manager
.auth()
.await
.context("auth should remain cached")?;
let cached = cached_auth
.get_token_data()
.context("token data should remain cached")?;
assert_eq!(cached, initial_tokens);
server.verify().await;
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[tokio::test]
async fn refresh_token_reloads_changed_auth_after_permanent_failure() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/oauth/token"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"error": {
"code": "refresh_token_reused"
}
})))
.expect(1)
.mount(&server)
.await;
let ctx = RefreshTokenTestContext::new(&server)?;
let initial_last_refresh = Utc::now() - Duration::days(1);
let initial_tokens = build_tokens(INITIAL_ACCESS_TOKEN, INITIAL_REFRESH_TOKEN);
let initial_auth = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(initial_tokens.clone()),
last_refresh: Some(initial_last_refresh),
};
ctx.write_auth(&initial_auth)?;
let first_err = ctx
.auth_manager
.refresh_token()
.await
.err()
.context("first refresh should fail")?;
assert_eq!(
first_err.failed_reason(),
Some(RefreshTokenFailedReason::Exhausted)
);
let fresh_refresh = Utc::now() - Duration::hours(1);
let disk_tokens = build_tokens("disk-access-token", "disk-refresh-token");
let disk_auth = AuthDotJson {
auth_mode: Some(AuthMode::Chatgpt),
openai_api_key: None,
tokens: Some(disk_tokens.clone()),
last_refresh: Some(fresh_refresh),
};
save_auth(
ctx.codex_home.path(),
&disk_auth,
AuthCredentialsStoreMode::File,
)?;
ctx.auth_manager
.refresh_token()
.await
.context("refresh should reload changed auth without retrying")?;
let stored = ctx.load_auth()?;
assert_eq!(stored, disk_auth);
let cached_auth = ctx
.auth_manager
.auth_cached()
.context("auth should be cached")?;
let cached = cached_auth
.get_token_data()
.context("token data should reload from disk")?;
assert_eq!(cached, disk_tokens);
let requests = server.received_requests().await.unwrap_or_default();
assert_eq!(
requests.len(),
1,
"expected only the initial refresh request"
);
server.verify().await;
Ok(())
}
#[serial_test::serial(auth_refresh)]
#[tokio::test]
async fn refresh_token_returns_transient_error_on_server_failure() -> Result<()> {
@@ -867,7 +1016,7 @@ impl Drop for EnvGuard {
}
}
fn minimal_jwt() -> String {
fn jwt_with_payload(payload: serde_json::Value) -> String {
#[derive(Serialize)]
struct Header {
alg: &'static str,
@@ -878,7 +1027,6 @@ fn minimal_jwt() -> String {
alg: "none",
typ: "JWT",
};
let payload = json!({ "sub": "user-123" });
fn b64(data: &[u8]) -> String {
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
@@ -898,6 +1046,14 @@ fn minimal_jwt() -> String {
format!("{header_b64}.{payload_b64}.{signature_b64}")
}
fn minimal_jwt() -> String {
jwt_with_payload(json!({ "sub": "user-123" }))
}
fn access_token_with_expiration(expires_at: chrono::DateTime<Utc>) -> String {
jwt_with_payload(json!({ "sub": "user-123", "exp": expires_at.timestamp() }))
}
fn build_tokens(access_token: &str, refresh_token: &str) -> TokenData {
let id_token = IdTokenInfo {
raw_jwt: minimal_jwt(),
+3 -2
View File
@@ -1,5 +1,6 @@
use assert_cmd::Command as AssertCommand;
use codex_core::auth::CODEX_API_KEY_ENV_VAR;
use codex_git_utils::collect_git_info;
use codex_protocol::protocol::GitInfo;
use codex_utils_cargo_bin::find_resource;
use core_test_support::fs_wait;
@@ -596,7 +597,7 @@ async fn integration_git_info_unit_test() {
.unwrap();
// 3. Test git info collection directly
let git_info = codex_core::git_info::collect_git_info(&git_repo).await;
let git_info = collect_git_info(&git_repo).await;
// 4. Verify git info is present and contains expected data
assert!(git_info.is_some(), "Git info should be collected");
@@ -608,7 +609,7 @@ async fn integration_git_info_unit_test() {
git_info.commit_hash.is_some(),
"Git info should contain commit_hash"
);
let commit_hash = git_info.commit_hash.as_ref().unwrap();
let commit_hash = &git_info.commit_hash.as_ref().unwrap().0;
assert_eq!(commit_hash.len(), 40, "Commit hash should be 40 characters");
assert!(
commit_hash.chars().all(|c| c.is_ascii_hexdigit()),
+6 -3
View File
@@ -42,6 +42,7 @@ use codex_protocol::protocol::SessionMeta;
use codex_protocol::protocol::SessionMetaLine;
use codex_protocol::protocol::SessionSource;
use codex_protocol::user_input::UserInput;
use core_test_support::PathBufExt;
use core_test_support::apps_test_server::AppsTestServer;
use core_test_support::load_default_config_for_test;
use core_test_support::responses::ev_completed;
@@ -1105,7 +1106,7 @@ async fn skills_append_to_developer_message() {
.with_home(codex_home.clone())
.with_auth(CodexAuth::from_api_key("Test API Key"))
.with_config(move |config| {
config.cwd = codex_home_path;
config.cwd = codex_home_path.abs();
});
let codex = builder
.build(&server)
@@ -1308,8 +1309,9 @@ async fn user_turn_collaboration_mode_overrides_model_and_effort() -> anyhow::Re
text: "hello".into(),
text_elements: Vec::new(),
}],
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: session_configured.model.clone(),
effort: Some(ReasoningEffort::Low),
@@ -1425,8 +1427,9 @@ async fn user_turn_explicit_reasoning_summary_overrides_model_catalog_default()
text: "hello".into(),
text_elements: Vec::new(),
}],
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: session_configured.model,
effort: None,
+1
View File
@@ -2319,6 +2319,7 @@ text(
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: test.session_configured.model.clone(),
effort: None,
@@ -176,8 +176,9 @@ async fn collaboration_instructions_added_on_user_turn() -> Result<()> {
text: "hello".into(),
text_elements: Vec::new(),
}],
cwd: test.config.cwd.clone(),
cwd: test.config.cwd.to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: test.config.permissions.sandbox_policy.get().clone(),
model: test.session_configured.model.clone(),
effort: None,
@@ -291,8 +292,9 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu
text: "hello".into(),
text_elements: Vec::new(),
}],
cwd: test.config.cwd.clone(),
cwd: test.config.cwd.to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: test.config.permissions.sandbox_policy.get().clone(),
model: test.session_configured.model.clone(),
effort: None,
+7
View File
@@ -1658,6 +1658,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() {
final_output_json_schema: None,
cwd: resumed.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: resumed.session_configured.model.clone(),
effort: None,
@@ -1748,6 +1749,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: previous_model.to_string(),
effort: None,
@@ -1772,6 +1774,7 @@ async fn pre_sampling_compact_runs_on_switch_to_smaller_context_model() {
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: next_model.to_string(),
effort: None,
@@ -1882,6 +1885,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
final_output_json_schema: None,
cwd: initial.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: previous_model.to_string(),
effort: None,
@@ -1930,6 +1934,7 @@ async fn pre_sampling_compact_runs_after_resume_and_switch_to_smaller_model() {
final_output_json_schema: None,
cwd: resumed.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: next_model.to_string(),
effort: None,
@@ -3132,6 +3137,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: previous_model.to_string(),
effort: None,
@@ -3156,6 +3162,7 @@ async fn snapshot_request_shape_pre_turn_compaction_strips_incoming_model_switch
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: next_model.to_string(),
effort: None,
+190 -33
View File
@@ -16,6 +16,9 @@ use codex_core::ThreadManager;
use codex_core::compact::SUMMARIZATION_PROMPT;
use codex_core::config::Config;
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::Settings;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::WarningEvent;
@@ -307,10 +310,10 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<()
return Ok(());
}
// 1. Arrange mocked SSE responses for the initial flow plus the second compact.
// 1. Arrange mocked SSE responses as a single ordered stream so assertions
// observe the real request sequence instead of per-mock duplicate captures.
let server = MockServer::start().await;
let mut request_log = mount_initial_flow(&server).await;
request_log.extend(mount_second_compact_flow(&server).await);
let request_log = mount_second_compact_sequence(&server).await;
// 2. Drive the conversation through compact -> resume -> fork -> compact -> resume.
let (_home, config, manager, base) = start_test_conversation(&server, None).await;
@@ -346,7 +349,12 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<()
let resumed_again = resume_conversation(&manager, &config, forked_path).await;
user_turn(&resumed_again, AFTER_SECOND_RESUME).await;
let mut requests = gather_request_bodies(&request_log);
let mut requests = request_log
.requests()
.into_iter()
.map(|request| request.body_json())
.collect::<Vec<_>>();
requests.iter_mut().for_each(normalize_line_endings);
normalize_compact_prompts(&mut requests);
let input_after_compact = json!(requests[requests.len() - 2]["input"]);
let input_after_resume = json!(requests[requests.len() - 1]["input"]);
@@ -379,21 +387,39 @@ async fn compact_resume_after_second_compaction_preserves_history() -> Result<()
);
let seeded_user_prefix = &first_request_user_texts[..first_turn_user_index];
let summary_after_second_compact =
extract_summary_user_text(&requests[requests.len() - 3], SUMMARY_TEXT);
let mut expected_after_second_compact_user_texts =
vec!["AFTER_FORK".to_string(), summary_after_second_compact];
extract_summary_user_text(&requests[requests.len() - 2], SUMMARY_TEXT);
let mut expected_after_second_compact_user_texts = vec![
"hello world".to_string(),
"AFTER_COMPACT".to_string(),
"AFTER_RESUME".to_string(),
"AFTER_FORK".to_string(),
summary_after_second_compact.clone(),
];
expected_after_second_compact_user_texts.extend_from_slice(seeded_user_prefix);
expected_after_second_compact_user_texts.push("AFTER_COMPACT_2".to_string());
let mut expected_fork_local_user_texts =
vec!["AFTER_FORK".to_string(), summary_after_second_compact];
expected_fork_local_user_texts.extend_from_slice(seeded_user_prefix);
expected_fork_local_user_texts.push("AFTER_COMPACT_2".to_string());
let final_user_texts = json_message_input_texts(&requests[requests.len() - 1], "user");
let (final_last, final_prefix) = final_user_texts
.split_last()
.unwrap_or_else(|| panic!("after-second-resume request missing user messages"));
assert_eq!(final_last, AFTER_SECOND_RESUME);
assert!(
final_prefix.starts_with(&expected_after_second_compact_user_texts),
"after-second-resume user texts should preserve post-compact user history prefix"
);
let final_seeded_suffix = &final_prefix[expected_after_second_compact_user_texts.len()..];
let matched_prefix_len = if let Some(start) = final_prefix
.windows(expected_after_second_compact_user_texts.len())
.position(|window| window == expected_after_second_compact_user_texts)
{
start + expected_after_second_compact_user_texts.len()
} else if let Some(start) = final_prefix
.windows(expected_fork_local_user_texts.len())
.position(|window| window == expected_fork_local_user_texts)
{
start + expected_fork_local_user_texts.len()
} else {
panic!("after-second-resume user texts should preserve post-compact user history prefix");
};
let final_seeded_suffix = &final_prefix[matched_prefix_len..];
if seeded_user_prefix.is_empty() {
assert!(
final_seeded_suffix.is_empty(),
@@ -502,6 +528,141 @@ async fn snapshot_rollback_past_compaction_replays_append_only_history() -> Resu
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
/// Scenario: rolling back a turn that introduced persistent pre-turn context
/// diffs should trim those context updates so the next request includes them
/// only once.
async fn snapshot_rollback_followup_turn_trims_context_updates() -> Result<()> {
if network_disabled() {
println!("Skipping test because network is disabled in this sandbox");
return Ok(());
}
const MODEL: &str = "gpt-5.1-codex";
const TURN_ONE_USER: &str = "turn 1 user";
const TURN_TWO_USER: &str = "turn 2 user";
const FOLLOWUP_USER: &str = "follow-up user";
const ROLLED_BACK_DEV_INSTRUCTIONS: &str = "ROLLED_BACK_DEV_INSTRUCTIONS";
const PRETURN_CONTEXT_DIFF_CWD: &str = "PRETURN_CONTEXT_DIFF_CWD";
let server = MockServer::start().await;
let request_log = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_assistant_message("m1", "turn 1 assistant"),
ev_completed("r1"),
]),
sse(vec![
ev_assistant_message("m2", "turn 2 assistant"),
ev_completed("r2"),
]),
sse(vec![ev_completed("r3")]),
],
)
.await;
let (_home, config, _manager, conversation) =
start_test_conversation(&server, Some(MODEL)).await;
user_turn(&conversation, TURN_ONE_USER).await;
let override_cwd = config.cwd.join(PRETURN_CONTEXT_DIFF_CWD)?;
std::fs::create_dir_all(&override_cwd)?;
conversation
.submit(Op::OverrideTurnContext {
cwd: Some(override_cwd.to_path_buf()),
approval_policy: None,
approvals_reviewer: None,
sandbox_policy: None,
windows_sandbox_level: None,
model: None,
effort: None,
summary: None,
service_tier: None,
collaboration_mode: Some(CollaborationMode {
mode: ModeKind::Default,
settings: Settings {
model: MODEL.to_string(),
reasoning_effort: None,
developer_instructions: Some(ROLLED_BACK_DEV_INSTRUCTIONS.to_string()),
},
}),
personality: None,
})
.await?;
user_turn(&conversation, TURN_TWO_USER).await;
conversation
.submit(Op::ThreadRollback { num_turns: 1 })
.await?;
let rollback_event = wait_for_event(&conversation, |ev| {
matches!(ev, EventMsg::ThreadRolledBack(_))
})
.await;
let EventMsg::ThreadRolledBack(rollback_event) = rollback_event else {
panic!("expected thread rolled back event");
};
assert_eq!(rollback_event.num_turns, 1);
user_turn(&conversation, FOLLOWUP_USER).await;
let requests = request_log.requests();
assert_eq!(requests.len(), 3);
let before_rollback_developer_count = requests[1]
.message_input_texts("developer")
.iter()
.filter(|text| text.contains(ROLLED_BACK_DEV_INSTRUCTIONS))
.count();
assert_eq!(before_rollback_developer_count, 1);
assert_eq!(
requests[1]
.message_input_texts("user")
.iter()
.filter(|text| text.contains(PRETURN_CONTEXT_DIFF_CWD))
.count(),
1
);
let after_rollback_developer_count = requests[2]
.message_input_texts("developer")
.iter()
.filter(|text| text.contains(ROLLED_BACK_DEV_INSTRUCTIONS))
.count();
assert_eq!(after_rollback_developer_count, 1);
let after_rollback_user_texts = requests[2].message_input_texts("user");
assert_eq!(
after_rollback_user_texts
.iter()
.filter(|text| text.contains(PRETURN_CONTEXT_DIFF_CWD))
.count(),
1
);
assert_eq!(
after_rollback_user_texts.last().map(String::as_str),
Some(FOLLOWUP_USER)
);
insta::assert_snapshot!(
"rollback_followup_turn_trims_context_updates",
context_snapshot::format_labeled_requests_snapshot(
"rollback trims pre-turn override context updates before the follow-up request",
&[
("rolled-back turn request", &requests[1]),
("follow-up request after rollback", &requests[2]),
],
&ContextSnapshotOptions::default()
.strip_capability_instructions()
.render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 96 }),
)
);
Ok(())
}
fn normalize_line_endings(value: &mut Value) {
match value {
Value::String(text) => {
@@ -594,33 +755,29 @@ async fn mount_initial_flow(server: &MockServer) -> Vec<ResponseMock> {
vec![first, compact, after_compact, after_resume, after_fork]
}
async fn mount_second_compact_flow(server: &MockServer) -> Vec<ResponseMock> {
async fn mount_second_compact_sequence(server: &MockServer) -> ResponseMock {
let sse1 = sse(vec![
ev_assistant_message("m1", FIRST_REPLY),
ev_completed("r1"),
]);
let sse2 = sse(vec![
ev_assistant_message("m2", SUMMARY_TEXT),
ev_completed("r2"),
]);
let sse3 = sse(vec![
ev_assistant_message("m3", "AFTER_COMPACT_REPLY"),
ev_completed("r3"),
]);
let sse4 = sse(vec![ev_completed("r4")]);
let sse5 = sse(vec![ev_completed("r5")]);
let sse6 = sse(vec![
ev_assistant_message("m4", SUMMARY_TEXT),
ev_completed("r6"),
]);
let sse7 = sse(vec![ev_completed("r7")]);
let sse8 = sse(vec![ev_completed("r8")]);
// Keep this matcher broad enough to survive prompt-shape differences across
// platforms/config (history may include either marker text or compact prompt
// fragments), but explicitly exclude the final resume turn so these two
// one-shot mocks cannot race for the same request.
let match_second_compact = |req: &wiremock::Request| {
let body = std::str::from_utf8(&req.body).unwrap_or("");
(body.contains("AFTER_FORK")
|| body_contains_text(body, SUMMARIZATION_PROMPT)
|| body.contains(&json_fragment(FIRST_REPLY)))
&& !body.contains(&format!("\"text\":\"{AFTER_SECOND_RESUME}\""))
};
let second_compact = mount_sse_once_match(server, match_second_compact, sse6).await;
let match_after_second_resume = |req: &wiremock::Request| {
let body = std::str::from_utf8(&req.body).unwrap_or("");
body.contains(&format!("\"text\":\"{AFTER_SECOND_RESUME}\""))
};
let after_second_resume = mount_sse_once_match(server, match_after_second_resume, sse7).await;
vec![second_compact, after_second_resume]
mount_sse_sequence(server, vec![sse1, sse2, sse3, sse4, sse5, sse6, sse7, sse8]).await
}
async fn start_test_conversation(
+2 -3
View File
@@ -6,7 +6,6 @@ use std::string::ToString;
use codex_core::exec::ExecCapturePolicy;
use codex_core::exec::ExecParams;
use codex_core::exec::ExecToolCallOutput;
use codex_core::exec::SandboxType;
use codex_core::exec::process_exec_tool_call;
use codex_core::sandboxing::SandboxPermissions;
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
@@ -14,12 +13,12 @@ use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_sandboxing::SandboxType;
use codex_sandboxing::get_platform_sandbox;
use tempfile::TempDir;
use codex_core::error::Result;
use codex_core::get_platform_sandbox;
fn skip_test() -> bool {
if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) {
eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test.");
+2
View File
@@ -51,6 +51,7 @@ async fn submit_user_turn(
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy,
approvals_reviewer: None,
sandbox_policy,
model: session_model,
effort: None,
@@ -131,6 +132,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> {
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
+17 -6
View File
@@ -1,3 +1,4 @@
use codex_core::ForkSnapshot;
use codex_core::NewThread;
use codex_core::parse_turn_item;
use codex_protocol::items::TurnItem;
@@ -110,7 +111,13 @@ async fn fork_thread_twice_drops_to_first_message() {
thread: codex_fork1,
..
} = thread_manager
.fork_thread(1, config_for_fork.clone(), base_path.clone(), false, None)
.fork_thread(
ForkSnapshot::TruncateBeforeNthUserMessage(1),
config_for_fork.clone(),
base_path.clone(),
/*persist_extended_history*/ false,
/*parent_trace*/ None,
)
.await
.expect("fork 1");
@@ -118,9 +125,8 @@ async fn fork_thread_twice_drops_to_first_message() {
// GetHistory on fork1 flushed; the file is ready.
let fork1_items = read_items(&fork1_path);
assert!(fork1_items.len() > expected_after_first.len());
pretty_assertions::assert_eq!(
serde_json::to_value(&fork1_items[..expected_after_first.len()]).unwrap(),
serde_json::to_value(&fork1_items).unwrap(),
serde_json::to_value(&expected_after_first).unwrap()
);
@@ -129,7 +135,13 @@ async fn fork_thread_twice_drops_to_first_message() {
thread: codex_fork2,
..
} = thread_manager
.fork_thread(0, config_for_fork.clone(), fork1_path.clone(), false, None)
.fork_thread(
ForkSnapshot::TruncateBeforeNthUserMessage(0),
config_for_fork.clone(),
fork1_path.clone(),
/*persist_extended_history*/ false,
/*parent_trace*/ None,
)
.await
.expect("fork 2");
@@ -143,9 +155,8 @@ async fn fork_thread_twice_drops_to_first_message() {
.unwrap_or(0);
let expected_after_second: Vec<RolloutItem> = fork1_items[..cut_last_on_fork1].to_vec();
let fork2_items = read_items(&fork2_path);
assert!(fork2_items.len() > expected_after_second.len());
pretty_assertions::assert_eq!(
serde_json::to_value(&fork2_items[..expected_after_second.len()]).unwrap(),
serde_json::to_value(&fork2_items).unwrap(),
serde_json::to_value(&expected_after_second).unwrap()
);
}
-145
View File
@@ -1,145 +0,0 @@
#![cfg(not(target_os = "windows"))]
use anyhow::Result;
use core_test_support::responses::mount_function_call_agent_response;
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::test_codex;
use std::collections::HashSet;
use std::path::Path;
use std::process::Command as StdCommand;
const MODEL_WITH_TOOL: &str = "test-gpt-5.1-codex";
fn ripgrep_available() -> bool {
StdCommand::new("rg")
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
macro_rules! skip_if_ripgrep_missing {
($ret:expr $(,)?) => {{
if !ripgrep_available() {
eprintln!("rg not available in PATH; skipping test");
return $ret;
}
}};
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn grep_files_tool_collects_matches() -> Result<()> {
skip_if_no_network!(Ok(()));
skip_if_ripgrep_missing!(Ok(()));
let server = start_mock_server().await;
let test = build_test_codex(&server).await?;
let search_dir = test.cwd.path().join("src");
std::fs::create_dir_all(&search_dir)?;
let alpha = search_dir.join("alpha.rs");
let beta = search_dir.join("beta.rs");
let gamma = search_dir.join("gamma.txt");
std::fs::write(&alpha, "alpha needle\n")?;
std::fs::write(&beta, "beta needle\n")?;
std::fs::write(&gamma, "needle in text but excluded\n")?;
let call_id = "grep-files-collect";
let arguments = serde_json::json!({
"pattern": "needle",
"path": search_dir.to_string_lossy(),
"include": "*.rs",
})
.to_string();
let mocks =
mount_function_call_agent_response(&server, call_id, &arguments, "grep_files").await;
test.submit_turn("please find uses of needle").await?;
let req = mocks.completion.single_request();
let (content_opt, success_opt) = req
.function_call_output_content_and_success(call_id)
.expect("tool output present");
let content = content_opt.expect("content present");
let success = success_opt.unwrap_or(true);
assert!(
success,
"expected success for matches, got content={content}"
);
let entries = collect_file_names(&content);
assert_eq!(entries.len(), 2, "content: {content}");
assert!(
entries.contains("alpha.rs"),
"missing alpha.rs in {entries:?}"
);
assert!(
entries.contains("beta.rs"),
"missing beta.rs in {entries:?}"
);
assert!(
!entries.contains("gamma.txt"),
"txt file should be filtered out: {entries:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn grep_files_tool_reports_empty_results() -> Result<()> {
skip_if_no_network!(Ok(()));
skip_if_ripgrep_missing!(Ok(()));
let server = start_mock_server().await;
let test = build_test_codex(&server).await?;
let search_dir = test.cwd.path().join("logs");
std::fs::create_dir_all(&search_dir)?;
std::fs::write(search_dir.join("output.txt"), "no hits here")?;
let call_id = "grep-files-empty";
let arguments = serde_json::json!({
"pattern": "needle",
"path": search_dir.to_string_lossy(),
"limit": 5,
})
.to_string();
let mocks =
mount_function_call_agent_response(&server, call_id, &arguments, "grep_files").await;
test.submit_turn("search again").await?;
let req = mocks.completion.single_request();
let (content_opt, success_opt) = req
.function_call_output_content_and_success(call_id)
.expect("tool output present");
let content = content_opt.expect("content present");
if let Some(success) = success_opt {
assert!(!success, "expected success=false content={content}");
}
assert_eq!(content, "No matches found.");
Ok(())
}
#[allow(clippy::expect_used)]
async fn build_test_codex(server: &wiremock::MockServer) -> Result<TestCodex> {
let mut builder = test_codex().with_model(MODEL_WITH_TOOL);
builder.build(server).await
}
fn collect_file_names(content: &str) -> HashSet<String> {
content
.lines()
.filter_map(|line| {
if line.trim().is_empty() {
return None;
}
Path::new(line)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
})
.collect()
}
@@ -23,7 +23,14 @@ async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
.features
.enable(Feature::ChildAgentsMd)
.expect("test config should allow feature update");
std::fs::write(config.cwd.join("AGENTS.md"), "be nice").expect("write AGENTS.md");
std::fs::write(
config
.cwd
.join("AGENTS.md")
.expect("absolute AGENTS.md path"),
"be nice",
)
.expect("write AGENTS.md");
});
let test = builder.build(&server).await.expect("build test codex");
+426
View File
@@ -174,6 +174,69 @@ if payload.get("prompt") == {blocked_prompt_json}:
Ok(())
}
fn write_pre_tool_use_hook(
home: &Path,
matcher: Option<&str>,
mode: &str,
reason: &str,
) -> Result<()> {
let script_path = home.join("pre_tool_use_hook.py");
let log_path = home.join("pre_tool_use_hook_log.jsonl");
let mode_json = serde_json::to_string(mode).context("serialize pre tool use mode")?;
let reason_json = serde_json::to_string(reason).context("serialize pre tool use reason")?;
let script = format!(
r#"import json
from pathlib import Path
import sys
log_path = Path(r"{log_path}")
mode = {mode_json}
reason = {reason_json}
payload = json.load(sys.stdin)
with log_path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(payload) + "\n")
if mode == "json_deny":
print(json.dumps({{
"hookSpecificOutput": {{
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": reason
}}
}}))
elif mode == "exit_2":
sys.stderr.write(reason + "\n")
raise SystemExit(2)
"#,
log_path = log_path.display(),
mode_json = mode_json,
reason_json = reason_json,
);
let mut group = serde_json::json!({
"hooks": [{
"type": "command",
"command": format!("python3 {}", script_path.display()),
"statusMessage": "running pre tool use hook",
}]
});
if let Some(matcher) = matcher {
group["matcher"] = Value::String(matcher.to_string());
}
let hooks = serde_json::json!({
"hooks": {
"PreToolUse": [group]
}
});
fs::write(&script_path, script).context("write pre tool use hook script")?;
fs::write(home.join("hooks.json"), hooks.to_string()).context("write hooks.json")?;
Ok(())
}
fn write_session_start_hook_recording_transcript(home: &Path) -> Result<()> {
let script_path = home.join("session_start_hook.py");
let log_path = home.join("session_start_hook_log.jsonl");
@@ -253,6 +316,15 @@ fn read_stop_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
.collect()
}
fn read_pre_tool_use_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
fs::read_to_string(home.join("pre_tool_use_hook_log.jsonl"))
.context("read pre tool use hook log")?
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str(line).context("parse pre tool use hook log line"))
.collect()
}
fn read_session_start_hook_inputs(home: &Path) -> Result<Vec<serde_json::Value>> {
fs::read_to_string(home.join("session_start_hook_log.jsonl"))
.context("read session start hook log")?
@@ -849,3 +921,357 @@ async fn blocked_queued_prompt_does_not_strand_earlier_accepted_prompt() -> Resu
server.shutdown().await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pre_tool_use_blocks_shell_command_before_execution() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let call_id = "pretooluse-shell-command";
let marker = std::env::temp_dir().join("pretooluse-shell-command-marker");
let command = format!("printf blocked > {}", marker.display());
let args = serde_json::json!({ "command": command });
let responses = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-1"),
core_test_support::responses::ev_function_call(
call_id,
"shell_command",
&serde_json::to_string(&args)?,
),
ev_completed("resp-1"),
]),
sse(vec![
ev_response_created("resp-2"),
ev_assistant_message("msg-1", "hook blocked it"),
ev_completed("resp-2"),
]),
],
)
.await;
let mut builder = test_codex()
.with_pre_build_hook(|home| {
if let Err(error) =
write_pre_tool_use_hook(home, Some("^Bash$"), "json_deny", "blocked by pre hook")
{
panic!("failed to write pre tool use hook test fixture: {error}");
}
})
.with_config(|config| {
config
.features
.enable(Feature::CodexHooks)
.expect("test config should allow feature update");
});
let test = builder.build(&server).await?;
if marker.exists() {
fs::remove_file(&marker).context("remove leftover pre tool use marker")?;
}
test.submit_turn_with_policy(
"run the blocked shell command",
codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
)
.await?;
let requests = responses.requests();
assert_eq!(requests.len(), 2);
let output_item = requests[1].function_call_output(call_id);
let output = output_item
.get("output")
.and_then(Value::as_str)
.expect("shell command output string");
assert!(
output.contains("Bash command blocked by hook: blocked by pre hook"),
"blocked tool output should surface the hook reason",
);
assert!(
output.contains(&format!("Command: {command}")),
"blocked tool output should surface the blocked command",
);
assert!(
!marker.exists(),
"blocked command should not create marker file"
);
let hook_inputs = read_pre_tool_use_hook_inputs(test.codex_home_path())?;
assert_eq!(hook_inputs.len(), 1);
assert_eq!(hook_inputs[0]["hook_event_name"], "PreToolUse");
assert_eq!(hook_inputs[0]["tool_name"], "Bash");
assert_eq!(hook_inputs[0]["tool_use_id"], call_id);
assert_eq!(hook_inputs[0]["tool_input"]["command"], command);
let transcript_path = hook_inputs[0]["transcript_path"]
.as_str()
.expect("pre tool use hook transcript_path");
assert!(
!transcript_path.is_empty(),
"pre tool use hook should receive a non-empty transcript_path",
);
assert!(
Path::new(transcript_path).exists(),
"pre tool use hook transcript_path should be materialized on disk",
);
assert!(
hook_inputs[0]["turn_id"]
.as_str()
.is_some_and(|turn_id| !turn_id.is_empty())
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pre_tool_use_blocks_local_shell_before_execution() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let call_id = "pretooluse-local-shell";
let marker = std::env::temp_dir().join("pretooluse-local-shell-marker");
let command = vec![
"/bin/sh".to_string(),
"-c".to_string(),
format!("printf blocked > {}", marker.display()),
];
let responses = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-1"),
core_test_support::responses::ev_local_shell_call(
call_id,
"completed",
command.iter().map(String::as_str).collect(),
),
ev_completed("resp-1"),
]),
sse(vec![
ev_response_created("resp-2"),
ev_assistant_message("msg-1", "local shell blocked"),
ev_completed("resp-2"),
]),
],
)
.await;
let mut builder = test_codex()
.with_pre_build_hook(|home| {
if let Err(error) =
write_pre_tool_use_hook(home, Some("^Bash$"), "json_deny", "blocked local shell")
{
panic!("failed to write pre tool use hook test fixture: {error}");
}
})
.with_config(|config| {
config
.features
.enable(Feature::CodexHooks)
.expect("test config should allow feature update");
});
let test = builder.build(&server).await?;
if marker.exists() {
fs::remove_file(&marker).context("remove leftover local shell marker")?;
}
test.submit_turn("run the blocked local shell command")
.await?;
let requests = responses.requests();
assert_eq!(requests.len(), 2);
let output_item = requests[1].function_call_output(call_id);
let output = output_item
.get("output")
.and_then(Value::as_str)
.expect("local shell output string");
assert!(
output.contains("Bash command blocked by hook: blocked local shell"),
"blocked local shell output should surface the hook reason",
);
assert!(
output.contains(&format!(
"Command: {}",
codex_shell_command::parse_command::shlex_join(&command)
)),
"blocked local shell output should surface the blocked command",
);
assert!(
!marker.exists(),
"blocked local shell command should not execute"
);
let hook_inputs = read_pre_tool_use_hook_inputs(test.codex_home_path())?;
assert_eq!(hook_inputs.len(), 1);
assert_eq!(
hook_inputs[0]["tool_input"]["command"],
codex_shell_command::parse_command::shlex_join(&command),
);
assert!(
hook_inputs[0]["turn_id"]
.as_str()
.is_some_and(|turn_id| !turn_id.is_empty())
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pre_tool_use_blocks_exec_command_before_execution() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let call_id = "pretooluse-exec-command";
let marker = std::env::temp_dir().join("pretooluse-exec-command-marker");
let command = format!("printf blocked > {}", marker.display());
let args = serde_json::json!({ "cmd": command });
let responses = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-1"),
core_test_support::responses::ev_function_call(
call_id,
"exec_command",
&serde_json::to_string(&args)?,
),
ev_completed("resp-1"),
]),
sse(vec![
ev_response_created("resp-2"),
ev_assistant_message("msg-1", "exec command blocked"),
ev_completed("resp-2"),
]),
],
)
.await;
let mut builder = test_codex()
.with_pre_build_hook(|home| {
if let Err(error) =
write_pre_tool_use_hook(home, Some("^Bash$"), "exit_2", "blocked exec command")
{
panic!("failed to write pre tool use hook test fixture: {error}");
}
})
.with_config(|config| {
config.use_experimental_unified_exec_tool = true;
config
.features
.enable(Feature::CodexHooks)
.expect("test config should allow feature update");
config
.features
.enable(Feature::UnifiedExec)
.expect("test config should allow feature update");
});
let test = builder.build(&server).await?;
if marker.exists() {
fs::remove_file(&marker).context("remove leftover exec marker")?;
}
test.submit_turn("run the blocked exec command").await?;
let requests = responses.requests();
assert_eq!(requests.len(), 2);
let output_item = requests[1].function_call_output(call_id);
let output = output_item
.get("output")
.and_then(Value::as_str)
.expect("exec command output string");
assert!(
output.contains("Bash command blocked by hook: blocked exec command"),
"blocked exec command output should surface the hook reason",
);
assert!(
output.contains(&format!("Command: {command}")),
"blocked exec command output should surface the blocked command",
);
assert!(!marker.exists(), "blocked exec command should not execute");
let hook_inputs = read_pre_tool_use_hook_inputs(test.codex_home_path())?;
assert_eq!(hook_inputs.len(), 1);
assert_eq!(hook_inputs[0]["tool_use_id"], call_id);
assert_eq!(hook_inputs[0]["tool_input"]["command"], command);
assert!(
hook_inputs[0]["turn_id"]
.as_str()
.is_some_and(|turn_id| !turn_id.is_empty())
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pre_tool_use_does_not_fire_for_non_shell_tools() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let call_id = "pretooluse-update-plan";
let args = serde_json::json!({
"plan": [{
"step": "watch the tide",
"status": "pending",
}]
});
let responses = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-1"),
core_test_support::responses::ev_function_call(
call_id,
"update_plan",
&serde_json::to_string(&args)?,
),
ev_completed("resp-1"),
]),
sse(vec![
ev_response_created("resp-2"),
ev_assistant_message("msg-1", "plan updated"),
ev_completed("resp-2"),
]),
],
)
.await;
let mut builder = test_codex()
.with_pre_build_hook(|home| {
if let Err(error) = write_pre_tool_use_hook(home, None, "json_deny", "should not fire")
{
panic!("failed to write pre tool use hook test fixture: {error}");
}
})
.with_config(|config| {
config
.features
.enable(Feature::CodexHooks)
.expect("test config should allow feature update");
});
let test = builder.build(&server).await?;
test.submit_turn("update the plan").await?;
let requests = responses.requests();
assert_eq!(requests.len(), 2);
let output_item = requests[1].function_call_output(call_id);
let output = output_item
.get("output")
.and_then(Value::as_str)
.expect("update plan output string");
assert!(
!output.contains("should not fire"),
"non-shell tool output should not be blocked by PreToolUse",
);
let hook_log_path = test.codex_home_path().join("pre_tool_use_hook_log.jsonl");
assert!(
!hook_log_path.exists(),
"non-shell tools should not trigger pre tool use hooks",
);
Ok(())
}
@@ -122,6 +122,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -205,6 +206,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()>
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
+5
View File
@@ -522,6 +522,7 @@ async fn plan_mode_emits_plan_item_from_proposed_plan_block() -> anyhow::Result<
final_output_json_schema: None,
cwd: std::env::current_dir()?,
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
@@ -598,6 +599,7 @@ async fn plan_mode_strips_plan_from_agent_messages() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: std::env::current_dir()?,
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
@@ -706,6 +708,7 @@ async fn plan_mode_streaming_citations_are_stripped_across_added_deltas_and_done
final_output_json_schema: None,
cwd: std::env::current_dir()?,
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
@@ -892,6 +895,7 @@ async fn plan_mode_streaming_proposed_plan_tag_split_across_added_and_delta_is_p
final_output_json_schema: None,
cwd: std::env::current_dir()?,
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
@@ -1005,6 +1009,7 @@ async fn plan_mode_handles_missing_plan_close_tag() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: std::env::current_dir()?,
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,
+1
View File
@@ -80,6 +80,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> {
final_output_json_schema: Some(serde_json::from_str(SCHEMA)?),
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
-167
View File
@@ -1,167 +0,0 @@
#![cfg(not(target_os = "windows"))]
use core_test_support::responses::mount_function_call_agent_response;
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 pretty_assertions::assert_eq;
use serde_json::json;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "disabled until we enable list_dir tool"]
async fn list_dir_tool_returns_entries() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let test = test_codex().build(&server).await?;
let dir_path = test.cwd.path().join("sample_dir");
std::fs::create_dir(&dir_path)?;
std::fs::write(dir_path.join("alpha.txt"), "first file")?;
std::fs::create_dir(dir_path.join("nested"))?;
let dir_path = dir_path.to_string_lossy().to_string();
let call_id = "list-dir-call";
let arguments = json!({
"dir_path": dir_path,
"offset": 1,
"limit": 2,
})
.to_string();
let mocks = mount_function_call_agent_response(&server, call_id, &arguments, "list_dir").await;
test.submit_turn("list directory contents").await?;
let req = mocks.completion.single_request();
let (content_opt, _) = req
.function_call_output_content_and_success(call_id)
.expect("function_call_output present");
let output = content_opt.expect("output content present in tool output");
assert_eq!(output, "E1: [file] alpha.txt\nE2: [dir] nested");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "disabled until we enable list_dir tool"]
async fn list_dir_tool_depth_one_omits_children() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let test = test_codex().build(&server).await?;
let dir_path = test.cwd.path().join("depth_one");
std::fs::create_dir(&dir_path)?;
std::fs::write(dir_path.join("alpha.txt"), "alpha")?;
std::fs::create_dir(dir_path.join("nested"))?;
std::fs::write(dir_path.join("nested").join("beta.txt"), "beta")?;
let dir_path = dir_path.to_string_lossy().to_string();
let call_id = "list-dir-depth1";
let arguments = json!({
"dir_path": dir_path,
"offset": 1,
"limit": 10,
"depth": 1,
})
.to_string();
let mocks = mount_function_call_agent_response(&server, call_id, &arguments, "list_dir").await;
test.submit_turn("list directory contents depth one")
.await?;
let req = mocks.completion.single_request();
let (content_opt, _) = req
.function_call_output_content_and_success(call_id)
.expect("function_call_output present");
let output = content_opt.expect("output content present in tool output");
assert_eq!(output, "E1: [file] alpha.txt\nE2: [dir] nested");
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "disabled until we enable list_dir tool"]
async fn list_dir_tool_depth_two_includes_children_only() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let test = test_codex().build(&server).await?;
let dir_path = test.cwd.path().join("depth_two");
std::fs::create_dir(&dir_path)?;
std::fs::write(dir_path.join("alpha.txt"), "alpha")?;
let nested = dir_path.join("nested");
std::fs::create_dir(&nested)?;
std::fs::write(nested.join("beta.txt"), "beta")?;
let deeper = nested.join("grand");
std::fs::create_dir(&deeper)?;
std::fs::write(deeper.join("gamma.txt"), "gamma")?;
let dir_path_string = dir_path.to_string_lossy().to_string();
let call_id = "list-dir-depth2";
let arguments = json!({
"dir_path": dir_path_string,
"offset": 1,
"limit": 10,
"depth": 2,
})
.to_string();
let mocks = mount_function_call_agent_response(&server, call_id, &arguments, "list_dir").await;
test.submit_turn("list directory contents depth two")
.await?;
let req = mocks.completion.single_request();
let (content_opt, _) = req
.function_call_output_content_and_success(call_id)
.expect("function_call_output present");
let output = content_opt.expect("output content present in tool output");
assert_eq!(
output,
"E1: [file] alpha.txt\nE2: [dir] nested\nE3: [file] nested/beta.txt\nE4: [dir] nested/grand"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "disabled until we enable list_dir tool"]
async fn list_dir_tool_depth_three_includes_grandchildren() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let test = test_codex().build(&server).await?;
let dir_path = test.cwd.path().join("depth_three");
std::fs::create_dir(&dir_path)?;
std::fs::write(dir_path.join("alpha.txt"), "alpha")?;
let nested = dir_path.join("nested");
std::fs::create_dir(&nested)?;
std::fs::write(nested.join("beta.txt"), "beta")?;
let deeper = nested.join("grand");
std::fs::create_dir(&deeper)?;
std::fs::write(deeper.join("gamma.txt"), "gamma")?;
let dir_path_string = dir_path.to_string_lossy().to_string();
let call_id = "list-dir-depth3";
let arguments = json!({
"dir_path": dir_path_string,
"offset": 1,
"limit": 10,
"depth": 3,
})
.to_string();
let mocks = mount_function_call_agent_response(&server, call_id, &arguments, "list_dir").await;
test.submit_turn("list directory contents depth three")
.await?;
let req = mocks.completion.single_request();
let (content_opt, _) = req
.function_call_output_content_and_success(call_id)
.expect("function_call_output present");
let output = content_opt.expect("output content present in tool output");
assert_eq!(
output,
"E1: [file] alpha.txt\nE2: [dir] nested\nE3: [file] nested/beta.txt\nE4: [dir] nested/grand\nE5: [file] nested/grand/gamma.txt"
);
Ok(())
}
+1
View File
@@ -61,6 +61,7 @@ async fn submit_skill_turn(test: &TestCodex, skill_path: PathBuf, prompt: &str)
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
-3
View File
@@ -75,7 +75,6 @@ mod deprecation_notice;
mod exec;
mod exec_policy;
mod fork_thread;
mod grep_files;
mod hierarchical_agents;
#[cfg(not(target_os = "windows"))]
mod hooks;
@@ -83,7 +82,6 @@ mod image_rollout;
mod items;
mod js_repl;
mod json_result;
mod list_dir;
mod live_cli;
mod live_reload;
mod memories;
@@ -101,7 +99,6 @@ mod personality_migration;
mod plugins;
mod prompt_caching;
mod quota_exceeded;
mod read_file;
mod realtime_conversation;
mod remote_env;
mod remote_models;
@@ -126,6 +126,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -162,6 +163,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<(
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: next_model.to_string(),
effort: test.config.model_reasoning_effort,
@@ -221,6 +223,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -257,6 +260,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: next_model.to_string(),
effort: test.config.model_reasoning_effort,
@@ -398,6 +402,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -418,6 +423,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result<
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: text_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -525,6 +531,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -545,6 +552,7 @@ async fn generated_image_is_replayed_for_image_capable_models() -> Result<()> {
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -655,6 +663,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -675,6 +684,7 @@ async fn model_change_from_generated_image_to_text_preserves_prior_generated_ima
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: text_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -787,6 +797,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -815,6 +826,7 @@ async fn thread_rollback_after_generated_image_drops_entire_image_turn_history()
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: image_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -969,6 +981,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: large_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -1027,6 +1040,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result<
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: smaller_model_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -121,6 +121,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -144,6 +145,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
final_output_json_schema: None,
cwd: preturn_context_diff_cwd,
approval_policy: AskForApproval::OnRequest,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -222,6 +224,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
final_output_json_schema: None,
cwd: cwd_one.clone(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -245,6 +248,7 @@ async fn snapshot_model_visible_layout_cwd_change_does_not_refresh_agents() -> R
final_output_json_schema: None,
cwd: cwd_two,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -354,6 +358,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
final_output_json_schema: None,
cwd: resume_override_cwd,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: resumed.session_configured.model.clone(),
effort: resumed.config.model_reasoning_effort,
@@ -96,6 +96,7 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> {
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: codex_protocol::protocol::AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: test.session_configured.model.clone(),
effort: None,
@@ -102,6 +102,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
+2 -1
View File
@@ -1024,6 +1024,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_call() {
.features
.disable(Feature::GhostCommit)
.expect("test config should allow feature update");
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::Never);
})
.build(&server)
.await
@@ -1040,7 +1041,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_call() {
.await
.unwrap();
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TokenCount(_))).await;
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
logs_assert(|lines: &[&str]| {
let line = lines
@@ -1,4 +1,5 @@
use anyhow::Result;
use codex_core::ForkSnapshot;
use codex_core::config::Constrained;
use codex_execpolicy::Policy;
use codex_protocol::models::DeveloperInstructions;
@@ -419,7 +420,13 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
fork_config.permissions.approval_policy = Constrained::allow_any(AskForApproval::UnlessTrusted);
let forked = initial
.thread_manager
.fork_thread(usize::MAX, fork_config, rollout_path, false, None)
.fork_thread(
ForkSnapshot::Interrupted,
fork_config,
rollout_path,
/*persist_extended_history*/ false,
/*parent_trace*/ None,
)
.await?;
forked
.thread
@@ -436,14 +443,14 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> {
let body4 = req4.single_request().body_json();
let input4 = body4["input"].as_array().expect("input array");
let permissions_fork = permissions_texts(input4);
assert_eq!(permissions_fork.len(), permissions_base.len() + 2);
assert_eq!(permissions_fork.len(), permissions_base.len() + 1);
assert_eq!(
&permissions_fork[..permissions_base.len()],
permissions_base.as_slice()
);
let new_permissions = &permissions_fork[permissions_base.len()..];
assert_eq!(new_permissions.len(), 2);
assert_eq!(new_permissions[0], new_permissions[1]);
assert_eq!(new_permissions.len(), 1);
assert_eq!(permissions_fork, permissions_resume);
assert!(!permissions_base.contains(&new_permissions[0]));
Ok(())
+13
View File
@@ -103,6 +103,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -153,6 +154,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result<
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -210,6 +212,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -273,6 +276,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -324,6 +328,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -361,6 +366,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -427,6 +433,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -464,6 +471,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -543,6 +551,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -580,6 +589,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: test.config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: test.config.model_reasoning_effort,
@@ -696,6 +706,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -814,6 +825,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
@@ -851,6 +863,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: remote_slug.to_string(),
effort: test.config.model_reasoning_effort,
+11 -6
View File
@@ -16,7 +16,7 @@ use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::user_input::UserInput;
use codex_utils_absolute_path::AbsolutePathBuf;
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;
@@ -689,7 +689,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
let new_cwd = TempDir::new().unwrap();
let writable = TempDir::new().unwrap();
let new_policy = SandboxPolicy::WorkspaceWrite {
writable_roots: vec![AbsolutePathBuf::try_from(writable.path()).unwrap()],
writable_roots: vec![writable.abs()],
read_only_access: Default::default(),
network_access: true,
exclude_tmpdir_env_var: true,
@@ -703,6 +703,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
}],
cwd: new_cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: new_policy.clone(),
model: "o3".to_string(),
effort: Some(ReasoningEffort::High),
@@ -813,8 +814,9 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
text: "hello 1".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
cwd: default_cwd.to_path_buf(),
approval_policy: default_approval_policy,
approvals_reviewer: None,
sandbox_policy: default_sandbox_policy.clone(),
model: default_model.clone(),
effort: default_effort,
@@ -833,8 +835,9 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
text: "hello 2".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
cwd: default_cwd.to_path_buf(),
approval_policy: default_approval_policy,
approvals_reviewer: None,
sandbox_policy: default_sandbox_policy.clone(),
model: default_model.clone(),
effort: default_effort,
@@ -937,8 +940,9 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
text: "hello 1".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
cwd: default_cwd.to_path_buf(),
approval_policy: default_approval_policy,
approvals_reviewer: None,
sandbox_policy: default_sandbox_policy.clone(),
model: default_model,
effort: default_effort,
@@ -957,8 +961,9 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
text: "hello 2".into(),
text_elements: Vec::new(),
}],
cwd: default_cwd.clone(),
cwd: default_cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: "o3".to_string(),
effort: Some(ReasoningEffort::High),
-42
View File
@@ -1,42 +0,0 @@
#![cfg(not(target_os = "windows"))]
use core_test_support::responses::mount_function_call_agent_response;
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 pretty_assertions::assert_eq;
use serde_json::json;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "disabled until we enable read_file tool"]
async fn read_file_tool_returns_requested_lines() -> anyhow::Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let test = test_codex().build(&server).await?;
let file_path = test.cwd.path().join("sample.txt");
std::fs::write(&file_path, "first\nsecond\nthird\nfourth\n")?;
let file_path = file_path.to_string_lossy().to_string();
let call_id = "read-file-call";
let arguments = json!({
"file_path": file_path,
"offset": 2,
"limit": 2,
})
.to_string();
let mocks = mount_function_call_agent_response(&server, call_id, &arguments, "read_file").await;
test.submit_turn("please inspect sample.txt").await?;
let req = mocks.completion.single_request();
let (output_text_opt, _) = req
.function_call_output_content_and_success(call_id)
.expect("output present");
let output_text = output_text_opt.expect("output text present");
assert_eq!(output_text, "L2: second\nL3: third");
Ok(())
}
@@ -173,6 +173,7 @@ async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<(
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: requested_model.to_string(),
effort: None,
@@ -231,6 +232,7 @@ async fn namespaced_model_slug_uses_catalog_metadata_without_fallback_warning()
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: config.permissions.approval_policy.value(),
approvals_reviewer: None,
sandbox_policy: config.permissions.sandbox_policy.get().clone(),
model: requested_model.to_string(),
effort: None,
@@ -394,6 +396,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REMOTE_MODEL_SLUG.to_string(),
effort: None,
@@ -612,6 +615,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: model.to_string(),
effort: None,
@@ -193,6 +193,7 @@ async fn submit_turn(
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy,
approvals_reviewer: None,
sandbox_policy,
model: session_model,
effort: None,
@@ -145,6 +145,7 @@ async fn submit_turn(
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy,
approvals_reviewer: None,
sandbox_policy,
model: session_model,
effort: None,
@@ -138,6 +138,7 @@ async fn request_user_input_round_trip_for_mode(mode: ModeKind) -> anyhow::Resul
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -255,6 +256,7 @@ where
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
+1 -1
View File
@@ -28,7 +28,7 @@ fn resume_history(
let turn_ctx = TurnContextItem {
turn_id: Some(turn_id.clone()),
trace_id: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
current_date: None,
timezone: None,
approval_policy: config.permissions.approval_policy.value(),
+2 -1
View File
@@ -17,6 +17,7 @@ use codex_protocol::protocol::ReviewTarget;
use codex_protocol::protocol::RolloutItem;
use codex_protocol::protocol::RolloutLine;
use codex_protocol::user_input::UserInput;
use core_test_support::PathBufExt;
use core_test_support::load_sse_fixture_with_id_from_str;
use core_test_support::responses::ResponseMock;
use core_test_support::responses::mount_sse_sequence;
@@ -817,7 +818,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() {
let codex_home = Arc::new(TempDir::new().unwrap());
let initial_cwd_path = initial_cwd.path().to_path_buf();
let codex = new_conversation_for_server(&server, codex_home.clone(), move |config| {
config.cwd = initial_cwd_path;
config.cwd = initial_cwd_path.abs();
})
.await;
+6
View File
@@ -131,6 +131,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
@@ -325,6 +326,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
@@ -528,6 +530,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: text_only_model_slug.to_string(),
effort: None,
@@ -642,6 +645,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
@@ -803,6 +807,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
@@ -1049,6 +1054,7 @@ async fn streamable_http_with_oauth_round_trip_impl() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
@@ -45,6 +45,7 @@ async fn openai_model_header_mismatch_emits_warning_event_and_warning_item() ->
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
@@ -143,6 +144,7 @@ async fn response_model_field_mismatch_emits_warning_when_header_matches_request
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
@@ -228,6 +230,7 @@ async fn openai_model_header_mismatch_only_emits_one_warning_per_turn() -> Resul
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
@@ -277,6 +280,7 @@ async fn openai_model_header_casing_only_mismatch_does_not_warn() -> Result<()>
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: REQUESTED_MODEL.to_string(),
effort: test.config.model_reasoning_effort,
+48 -1
View File
@@ -86,7 +86,7 @@ fn tool_search_output_tools(request: &ResponsesRequest, call_id: &str) -> Vec<Va
.unwrap_or_default()
}
fn configure_apps(config: &mut Config, apps_base_url: &str) {
fn configure_apps_without_tool_search(config: &mut Config, apps_base_url: &str) {
config
.features
.enable(Feature::Apps)
@@ -105,6 +105,14 @@ fn configure_apps(config: &mut Config, apps_base_url: &str) {
config.model_catalog = Some(model_catalog);
}
fn configure_apps(config: &mut Config, apps_base_url: &str) {
configure_apps_without_tool_search(config, apps_base_url);
config
.features
.enable(Feature::ToolSearch)
.expect("test config should allow feature update");
}
fn configured_builder(apps_base_url: String) -> TestCodexBuilder {
test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
@@ -169,6 +177,45 @@ async fn search_tool_flag_adds_tool_search() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn tool_search_disabled_by_default_exposes_apps_tools_directly() -> Result<()> {
skip_if_no_network!(Ok(()));
let server = start_mock_server().await;
let apps_server = AppsTestServer::mount_searchable(&server).await?;
let mock = mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-1"),
ev_assistant_message("msg-1", "done"),
ev_completed("resp-1"),
]),
)
.await;
let mut builder = test_codex()
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
.with_config(move |config| {
configure_apps_without_tool_search(config, apps_server.chatgpt_base_url.as_str())
});
let test = builder.build(&server).await?;
test.submit_turn_with_policies(
"list tools",
AskForApproval::Never,
SandboxPolicy::DangerFullAccess,
)
.await?;
let body = mock.single_request().body_json();
let tools = tool_names(&body);
assert!(!tools.iter().any(|name| name == TOOL_SEARCH_TOOL_NAME));
assert!(tools.iter().any(|name| name == CALENDAR_CREATE_TOOL));
assert!(tools.iter().any(|name| name == CALENDAR_LIST_TOOL));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn search_tool_is_hidden_for_api_key_auth() -> Result<()> {
skip_if_no_network!(Ok(()));
+34 -3
View File
@@ -72,7 +72,7 @@ async fn wait_for_snapshot(codex_home: &Path) -> Result<PathBuf> {
}
async fn wait_for_file_contents(path: &Path) -> Result<String> {
let deadline = Instant::now() + Duration::from_secs(5);
let deadline = Instant::now() + Duration::from_secs(15);
loop {
match fs::read_to_string(path).await {
Ok(contents) => return Ok(contents),
@@ -164,6 +164,7 @@ async fn run_snapshot_command_with_options(
final_output_json_schema: None,
cwd,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -254,6 +255,7 @@ async fn run_shell_command_snapshot_with_options(
final_output_json_schema: None,
cwd,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -324,6 +326,7 @@ async fn run_tool_turn_on_harness(
final_output_json_schema: None,
cwd,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -528,7 +531,9 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> {
let script = "apply_patch <<'EOF'\n*** Begin Patch\n*** Add File: snapshot-apply.txt\n+hello from snapshot\n*** End Patch\nEOF\n";
let args = json!({
"command": script,
"timeout_ms": 1_000,
// The intercepted apply_patch path self-invokes codex, which can take
// longer than a second in Bazel macOS test environments.
"timeout_ms": 5_000,
});
let call_id = "shell-snapshot-apply-patch";
let responses = vec![
@@ -555,6 +560,7 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.clone(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model,
effort: None,
@@ -569,7 +575,32 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> {
let snapshot_content = fs::read_to_string(&snapshot_path).await?;
assert_posix_snapshot_sections(&snapshot_content);
wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await;
let mut saw_patch_begin = false;
let mut patch_end = None;
wait_for_event(&codex, |ev| match ev {
EventMsg::PatchApplyBegin(begin) if begin.call_id == call_id => {
saw_patch_begin = true;
false
}
EventMsg::PatchApplyEnd(end) if end.call_id == call_id => {
patch_end = Some(end.clone());
false
}
EventMsg::TurnComplete(_) => true,
_ => false,
})
.await;
assert!(
saw_patch_begin,
"expected apply_patch to emit PatchApplyBegin"
);
let patch_end = patch_end.expect("expected apply_patch to emit PatchApplyEnd");
assert!(
patch_end.success,
"expected apply_patch to finish successfully: stdout={:?} stderr={:?}",
patch_end.stdout, patch_end.stderr,
);
assert_eq!(
wait_for_file_contents(&target).await?,
@@ -65,6 +65,7 @@ async fn submit_turn_with_policies(
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy,
approvals_reviewer: None,
sandbox_policy,
model: test.session_configured.model.clone(),
effort: None,
+1
View File
@@ -74,6 +74,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> {
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -0,0 +1,23 @@
---
source: core/tests/suite/compact_resume_fork.rs
expression: "context_snapshot::format_labeled_requests_snapshot(\"rollback trims pre-turn override context updates before the follow-up request\",\n&[(\"rolled-back turn request\", &requests[1]),\n(\"follow-up request after rollback\", &requests[2]),],\n&ContextSnapshotOptions::default().strip_capability_instructions().render_mode(ContextSnapshotRenderMode::KindWithTextPrefix\n{ max_chars: 96 }),)"
---
Scenario: rollback trims pre-turn override context updates before the follow-up request
## rolled-back turn request
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:turn 1 user
03:message/assistant:turn 1 assistant
04:message/developer:<collaboration_mode>ROLLED_BACK_DEV_INSTRUCTIONS</collaboration_mode>
05:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
06:message/user:turn 2 user
## follow-up request after rollback
00:message/developer:<PERMISSIONS_INSTRUCTIONS>
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
02:message/user:turn 1 user
03:message/assistant:turn 1 assistant
04:message/developer:<collaboration_mode>ROLLED_BACK_DEV_INSTRUCTIONS</collaboration_mode>
05:message/user:<ENVIRONMENT_CONTEXT:cwd=PRETURN_CONTEXT_DIFF_CWD>
06:message/user:follow-up user
@@ -23,6 +23,4 @@ Scenario: rollback past compaction replay after rollback
01:message/user:<COMPACTION_SUMMARY>\nSUMMARY_ONLY_CONTEXT
02:message/developer:<PERMISSIONS_INSTRUCTIONS>
03:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
04:message/developer:<PERMISSIONS_INSTRUCTIONS>
05:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
06:message/user:AFTER_ROLLBACK
04:message/user:AFTER_ROLLBACK
@@ -395,6 +395,7 @@ async fn mcp_call_marks_thread_memory_mode_polluted_when_configured() -> Result<
final_output_json_schema: None,
cwd: test.cwd_path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: test.session_configured.model.clone(),
effort: None,
@@ -85,6 +85,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()>
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -155,6 +156,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -235,6 +237,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -330,6 +333,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<()
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -433,6 +437,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -42,6 +42,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> {
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -358,6 +359,7 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
+1
View File
@@ -486,6 +486,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> {
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
+26
View File
@@ -192,6 +192,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> {
final_output_json_schema: None,
cwd,
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -325,6 +326,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -407,6 +409,7 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -492,6 +495,7 @@ async fn unified_exec_respects_workdir_override() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -589,6 +593,7 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -668,6 +673,7 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -748,6 +754,7 @@ async fn unified_exec_full_lifecycle_with_background_end_event() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -882,6 +889,7 @@ async fn unified_exec_emits_terminal_interaction_for_write_stdin() -> Result<()>
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1023,6 +1031,7 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<(
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1187,6 +1196,7 @@ async fn unified_exec_emits_one_begin_and_one_end_event() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1289,6 +1299,7 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1411,6 +1422,7 @@ async fn unified_exec_defaults_to_pipe() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1505,6 +1517,7 @@ async fn unified_exec_can_enable_tty() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1590,6 +1603,7 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1725,6 +1739,7 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1897,6 +1912,7 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<()
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1978,6 +1994,7 @@ async fn unified_exec_keeps_long_running_session_after_turn_end() -> Result<()>
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -2070,6 +2087,7 @@ async fn unified_exec_interrupt_preserves_long_running_session() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -2178,6 +2196,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -2317,6 +2336,7 @@ PY
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -2435,6 +2455,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -2535,6 +2556,7 @@ PY
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -2634,6 +2656,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
// Important!
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
@@ -2743,6 +2766,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
model: session_model,
effort: None,
@@ -2842,6 +2866,7 @@ async fn unified_exec_runs_on_all_platforms() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -2981,6 +3006,7 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> {
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
+3 -1
View File
@@ -10,6 +10,7 @@ use codex_protocol::protocol::Op;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::protocol::TurnAbortReason;
use codex_protocol::user_input::UserInput;
use core_test_support::PathBufExt;
use core_test_support::assert_regex_match;
use core_test_support::responses;
use core_test_support::responses::ev_assistant_message;
@@ -46,7 +47,7 @@ async fn user_shell_cmd_ls_and_cat_in_temp_dir() {
let server = start_mock_server().await;
let cwd_path = cwd.path().to_path_buf();
let mut builder = test_codex().with_config(move |config| {
config.cwd = cwd_path;
config.cwd = cwd_path.abs();
});
let codex = builder
.build(&server)
@@ -176,6 +177,7 @@ async fn user_shell_command_does_not_replace_active_turn() -> anyhow::Result<()>
final_output_json_schema: None,
cwd: fixture.cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: fixture.session_configured.model.clone(),
effort: None,
+36 -36
View File
@@ -41,7 +41,6 @@ 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;
@@ -78,11 +77,6 @@ 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());
@@ -91,14 +85,11 @@ fn png_bytes(width: u32, height: u32, rgba: [u8; 4]) -> anyhow::Result<Vec<u8>>
}
async fn create_workspace_directory(test: &TestCodex, rel_path: &str) -> anyhow::Result<PathBuf> {
let abs_path = test.config.cwd.join(rel_path);
let abs_path = test.config.cwd.join(rel_path)?;
test.fs()
.create_directory(
&absolute_path(&abs_path)?,
CreateDirectoryOptions { recursive: true },
)
.create_directory(&abs_path, CreateDirectoryOptions { recursive: true })
.await?;
Ok(abs_path)
Ok(abs_path.into_path_buf())
}
async fn write_workspace_file(
@@ -106,19 +97,14 @@ async fn write_workspace_file(
rel_path: &str,
contents: Vec<u8>,
) -> anyhow::Result<PathBuf> {
let abs_path = test.config.cwd.join(rel_path);
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 },
)
.create_directory(&parent, CreateDirectoryOptions { recursive: true })
.await?;
}
test.fs()
.write_file(&absolute_path(&abs_path)?, contents)
.await?;
Ok(abs_path)
test.fs().write_file(&abs_path, contents).await?;
Ok(abs_path.into_path_buf())
}
async fn write_workspace_png(
@@ -168,8 +154,9 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> {
path: abs_path.clone(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -239,7 +226,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
let cwd = config.cwd.clone();
let rel_path = "assets/example.png";
let abs_path = cwd.join(rel_path);
let abs_path = cwd.join(rel_path)?;
let original_width = 2304;
let original_height = 864;
write_workspace_png(
@@ -276,8 +263,9 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: cwd.clone(),
cwd: cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -310,7 +298,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> {
_ => unreachable!("stored event must be ViewImageToolCall"),
};
assert_eq!(tool_event.call_id, call_id);
assert_eq!(tool_event.path, abs_path);
assert_eq!(tool_event.path, abs_path.to_path_buf());
let req = mock.single_request();
let body = req.body_json();
@@ -416,8 +404,9 @@ async fn view_image_tool_can_preserve_original_resolution_when_requested_on_gpt5
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -514,8 +503,9 @@ async fn view_image_tool_errors_clearly_for_unsupported_detail_values() -> anyho
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -605,8 +595,9 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -704,8 +695,9 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -814,8 +806,9 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_feat
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -917,6 +910,7 @@ await codex.emitImage(out);
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1036,6 +1030,7 @@ console.log(out.type);
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1126,8 +1121,9 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1201,8 +1197,9 @@ async fn view_image_tool_errors_for_non_image_files() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1254,7 +1251,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
} = &test;
let rel_path = "missing/example.png";
let abs_path = config.cwd.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();
@@ -1281,8 +1278,9 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> {
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -1403,8 +1401,9 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an
text_elements: Vec::new(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: model_slug.to_string(),
effort: None,
@@ -1477,8 +1476,9 @@ async fn replaces_invalid_local_image_after_bad_request() -> anyhow::Result<()>
path: abs_path.clone(),
}],
final_output_json_schema: None,
cwd: config.cwd.clone(),
cwd: config.cwd.to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_model,
effort: None,
@@ -156,6 +156,7 @@ async fn websocket_fallback_hides_first_websocket_retry_stream_error() -> Result
final_output_json_schema: None,
cwd: cwd.path().to_path_buf(),
approval_policy: AskForApproval::Never,
approvals_reviewer: None,
sandbox_policy: SandboxPolicy::DangerFullAccess,
model: session_configured.model.clone(),
effort: None,