use anyhow::Result;use base64::Engine;use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;use core_test_support::responses::ev_apply_patch_custom_tool_call;use core_test_support::responses::ev_apply_patch_shell_command_call_via_heredoc;use core_test_support::responses::ev_shell_command_call;use core_test_support::test_codex::ApplyPatchModelOutput;use pretty_assertions::assert_eq;use std::fs;use std::path::PathBuf;use std::sync::atomic::AtomicI32;use std::sync::atomic::Ordering;use std::time::Duration;use std::time::SystemTime;use std::time::UNIX_EPOCH;use codex_exec_server::CreateDirectoryOptions;use codex_exec_server::LOCAL_ENVIRONMENT_ID;use codex_exec_server::REMOTE_ENVIRONMENT_ID;use codex_exec_server::RemoveOptions;use codex_features::Feature;use codex_protocol::models::PermissionProfile;use codex_protocol::permissions::FileSystemAccessMode;use codex_protocol::permissions::FileSystemPath;use codex_protocol::permissions::FileSystemSandboxEntry;use codex_protocol::permissions::FileSystemSandboxPolicy;use codex_protocol::permissions::FileSystemSpecialPath;use codex_protocol::permissions::NetworkSandboxPolicy;use codex_protocol::protocol::AskForApproval;use codex_protocol::protocol::EventMsg;use codex_protocol::protocol::Op;use codex_protocol::protocol::SandboxPolicy;use codex_protocol::protocol::TurnEnvironmentSelection;use codex_protocol::user_input::UserInput;#[cfg(target_os = "linux")]use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_path_uri::PathUri;use core_test_support::PathBufExt;use core_test_support::assert_regex_match;use core_test_support::get_remote_test_env;use core_test_support::responses::ev_assistant_message;use core_test_support::responses::ev_completed;use core_test_support::responses::ev_function_call;use core_test_support::responses::ev_response_created;use core_test_support::responses::ev_shell_command_call_with_args;use core_test_support::responses::mount_sse_sequence;use core_test_support::responses::sse;use core_test_support::responses::start_mock_server;use core_test_support::skip_if_no_network;use core_test_support::skip_if_remote;use core_test_support::skip_if_wine_exec;use core_test_support::test_codex::TestCodexBuilder;use core_test_support::test_codex::TestCodexHarness;use core_test_support::test_codex::local;use core_test_support::test_codex::test_codex;use core_test_support::test_codex::turn_permission_fields;use core_test_support::wait_for_event;use core_test_support::wait_for_event_with_timeout;use serde_json::json;use wiremock::Mock;use wiremock::Respond;use wiremock::ResponseTemplate;use wiremock::matchers::method;use wiremock::matchers::path_regex;pub async fn apply_patch_harness() -> Result<TestCodexHarness> { apply_patch_harness_with(|builder| builder).await}async fn apply_patch_harness_with( configure: impl FnOnce(TestCodexBuilder) -> TestCodexBuilder,) -> Result<TestCodexHarness> { let builder = configure(test_codex()); // Box harness construction so apply_patch_cli tests do not inline the // full test-thread startup path into each test future. Box::pin(TestCodexHarness::with_remote_env_builder(builder)).await}async fn submit_without_wait(harness: &TestCodexHarness, prompt: &str) -> Result<()> { submit_without_wait_with_turn_permissions( harness, prompt, SandboxPolicy::DangerFullAccess, /*permission_profile*/ None, ) .await}async fn submit_without_wait_with_turn_permissions( harness: &TestCodexHarness, prompt: &str, sandbox_policy: SandboxPolicy, permission_profile: Option<PermissionProfile>,) -> Result<()> { let test = harness.test(); let session_model = test.session_configured.model.clone(); test.codex .submit(Op::UserInput { items: vec![UserInput::Text { text: prompt.into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { mode: codex_protocol::config_types::ModeKind::Default, settings: codex_protocol::config_types::Settings { model: session_model, reasoning_effort: None, developer_instructions: None, }, }), ..Default::default() }, }) .await?; Ok(())}fn restrictive_workspace_write_profile() -> PermissionProfile { PermissionProfile::workspace_write_with( &[], NetworkSandboxPolicy::Restricted, /*exclude_tmpdir_env_var*/ true, /*exclude_slash_tmp*/ true, )}fn workspace_write_with_read_only_root(read_only_root: AbsolutePathBuf) -> PermissionProfile { let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { path: FileSystemPath::Path { path: read_only_root, }, access: FileSystemAccessMode::Read, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, }, ]); PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, NetworkSandboxPolicy::Restricted, )}#[cfg(unix)]fn workspace_write_with_unreadable_path(unreadable_path: AbsolutePathBuf) -> PermissionProfile { let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ FileSystemSandboxEntry { path: FileSystemPath::Path { path: unreadable_path, }, access: FileSystemAccessMode::Deny, }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::project_roots(/*subpath*/ None), }, access: FileSystemAccessMode::Write, }, ]); PermissionProfile::from_runtime_permissions( &file_system_sandbox_policy, NetworkSandboxPolicy::Restricted, )}#[cfg(unix)]fn create_file_symlink(source: &std::path::Path, link: &std::path::Path) -> std::io::Result<()> { std::os::unix::fs::symlink(source, link)}#[cfg(windows)]fn create_file_symlink(source: &std::path::Path, link: &std::path::Path) -> std::io::Result<()> { std::os::windows::fs::symlink_file(source, link)}#[cfg(not(any(unix, windows)))]fn create_file_symlink(_source: &std::path::Path, _link: &std::path::Path) -> std::io::Result<()> { Err(std::io::Error::new( std::io::ErrorKind::Unsupported, "file symlinks are unsupported on this platform", ))}pub async fn mount_apply_patch( harness: &TestCodexHarness, call_id: &str, patch: &str, assistant_msg: &str,) { mount_sse_sequence( harness.server(), apply_patch_responses( call_id, patch, assistant_msg, ev_apply_patch_custom_tool_call, ), ) .await;}async fn mount_apply_patch_model_output( harness: &TestCodexHarness, call_id: &str, patch: &str, assistant_msg: &str, model_output: ApplyPatchModelOutput,) { let apply_patch_call = match model_output { ApplyPatchModelOutput::ShellCommandViaHeredoc => { ev_apply_patch_shell_command_call_via_heredoc } }; mount_sse_sequence( harness.server(), apply_patch_responses(call_id, patch, assistant_msg, apply_patch_call), ) .await;}fn apply_patch_responses( call_id: &str, patch: &str, assistant_msg: &str, apply_patch_call: fn(&str, &str) -> serde_json::Value,) -> Vec<String> { vec![ sse(vec![ ev_response_created("resp-1"), apply_patch_call(call_id, patch), ev_completed("resp-1"), ]), sse(vec![ ev_assistant_message("msg-1", assistant_msg), ev_completed("resp-2"), ]), ]}#[cfg(target_os = "linux")]#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_uses_codex_self_exe_with_linux_sandbox_helper_alias() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let codex_linux_sandbox_exe = harness .test() .config .codex_linux_sandbox_exe .as_ref() .expect("linux test config should include codex-linux-sandbox helper"); assert_eq!( codex_linux_sandbox_exe .file_name() .and_then(|name| name.to_str()), Some(CODEX_LINUX_SANDBOX_ARG0), ); let patch = "*** Begin Patch\n*** Add File: helper-alias.txt\n+hello\n*** End Patch"; let call_id = "apply-helper-alias"; mount_apply_patch(&harness, call_id, patch, "done").await; harness.submit("please apply helper alias patch").await?; let out = harness.apply_patch_output(call_id).await; assert_regex_match( r"(?s)^Exit code: 0.*Success\. Updated the following files:\nA helper-alias\.txt\n?$", &out, ); assert_eq!(harness.read_file_text("helper-alias.txt").await?, "hello\n"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_multiple_operations_integration() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?; // Seed workspace state harness.write_file("modify.txt", "line1\nline2\n").await?; harness.write_file("delete.txt", "obsolete\n").await?; let patch = "*** Begin Patch\n*** Add File: nested/new.txt\n+created\n*** Delete File: delete.txt\n*** Update File: modify.txt\n@@\n-line2\n+changed\n*** End Patch"; let call_id = "apply-multi-ops"; mount_apply_patch(&harness, call_id, patch, "done").await; harness.submit("please apply multi-ops patch").await?; let out = harness.apply_patch_output(call_id).await; let expected = r"(?s)^Exit code: 0Wall time: [0-9]+(?:\.[0-9]+)? secondsOutput:Success. Updated the following files:A nested/new.txtM modify.txtD delete.txt?$"; assert_regex_match(expected, &out); assert_eq!(harness.read_file_text("nested/new.txt").await?, "created\n"); assert_eq!( harness.read_file_text("modify.txt").await?, "line1\nchanged\n" ); assert!(!harness.path_exists("delete.txt").await?); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_multiple_chunks() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness .write_file("multi.txt", "line1\nline2\nline3\nline4\n") .await?; let patch = "*** Begin Patch\n*** Update File: multi.txt\n@@\n-line2\n+changed2\n@@\n-line4\n+changed4\n*** End Patch"; let call_id = "apply-multi-chunks"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("apply multi-chunk patch").await?; assert_eq!( harness.read_file_text("multi.txt").await?, "line1\nchanged2\nline3\nchanged4\n" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_moves_file_to_new_directory() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness.write_file("old/name.txt", "old content\n").await?; let patch = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-old content\n+new content\n*** End Patch"; let call_id = "apply-move"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("apply move patch").await?; assert!(!harness.path_exists("old/name.txt").await?); assert_eq!( harness.read_file_text("renamed/dir/name.txt").await?, "new content\n" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_updates_file_appends_trailing_newline() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness .write_file("no_newline.txt", "no newline at end") .await?; let patch = "*** Begin Patch\n*** Update File: no_newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch"; let call_id = "apply-append-nl"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("apply newline patch").await?; let contents = harness.read_file_text("no_newline.txt").await?; assert!(contents.ends_with('\n')); assert_eq!(contents, "first line\nsecond line\n"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_insert_only_hunk_modifies_file() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness .write_file("insert_only.txt", "alpha\nomega\n") .await?; let patch = "*** Begin Patch\n*** Update File: insert_only.txt\n@@\n alpha\n+beta\n omega\n*** End Patch"; let call_id = "apply-insert-only"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("insert lines via apply_patch").await?; assert_eq!( harness.read_file_text("insert_only.txt").await?, "alpha\nbeta\nomega\n" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_move_overwrites_existing_destination() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness.write_file("old/name.txt", "from\n").await?; harness .write_file("renamed/dir/name.txt", "existing\n") .await?; let patch = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-from\n+new\n*** End Patch"; let call_id = "apply-move-overwrite"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("apply move overwrite patch").await?; assert!(!harness.path_exists("old/name.txt").await?); assert_eq!( harness.read_file_text("renamed/dir/name.txt").await?, "new\n" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_move_without_content_change_has_no_turn_diff() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let test = harness.test(); let codex = test.codex.clone(); harness.write_file("old/name.txt", "same\n").await?; let patch = "*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/name.txt\n@@\n same\n*** End Patch"; let call_id = "apply-move-no-change"; mount_apply_patch(&harness, call_id, patch, "ok").await; submit_without_wait(&harness, "rename without content change").await?; let mut saw_turn_diff = false; wait_for_event(&codex, |event| match event { EventMsg::TurnDiff(_) => { saw_turn_diff = true; false } EventMsg::TurnComplete(_) => true, _ => false, }) .await; assert!(!saw_turn_diff, "pure rename should not emit a turn diff"); assert!(!harness.path_exists("old/name.txt").await?); assert_eq!(harness.read_file_text("renamed/name.txt").await?, "same\n"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_add_overwrites_existing_file() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness.write_file("duplicate.txt", "old content\n").await?; let patch = "*** Begin Patch\n*** Add File: duplicate.txt\n+new content\n*** End Patch"; let call_id = "apply-add-overwrite"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("apply add overwrite patch").await?; assert_eq!( harness.read_file_text("duplicate.txt").await?, "new content\n" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_rejects_invalid_hunk_header() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let patch = "*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch"; let call_id = "apply-invalid-header"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("apply invalid header patch").await?; let out = harness.apply_patch_output(call_id).await; assert!( out.contains("apply_patch verification failed"), "expected verification failure message" ); assert!( out.contains("is not a valid hunk header"), "expected parse diagnostics in output: {out:?}" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_reports_missing_context() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness.write_file("modify.txt", "line1\nline2\n").await?; let patch = "*** Begin Patch\n*** Update File: modify.txt\n@@\n-missing\n+changed\n*** End Patch"; let call_id = "apply-missing-context"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("apply missing context patch").await?; let out = harness.apply_patch_output(call_id).await; assert!( out.contains("apply_patch verification failed"), "expected verification failure message" ); assert!(out.contains("Failed to find expected lines in")); assert_eq!( harness.read_file_text("modify.txt").await?, "line1\nline2\n" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_reports_missing_target_file() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let patch = "*** Begin Patch\n*** Update File: missing.txt\n@@\n-nope\n+better\n*** End Patch"; let call_id = "apply-missing-file"; mount_apply_patch(&harness, call_id, patch, "fail").await; harness.submit("attempt to update a missing file").await?; let out = harness.apply_patch_output(call_id).await; assert!( out.contains("apply_patch verification failed"), "expected verification failure message" ); assert!( out.contains("Failed to read file to update"), "expected missing file diagnostics: {out}" ); assert!( out.contains("missing.txt"), "expected missing file path in diagnostics: {out}" ); assert!(!harness.path_exists("missing.txt").await?); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_delete_missing_file_reports_error() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let patch = "*** Begin Patch\n*** Delete File: missing.txt\n*** End Patch"; let call_id = "apply-delete-missing"; mount_apply_patch(&harness, call_id, patch, "fail").await; harness.submit("attempt to delete missing file").await?; let out = harness.apply_patch_output(call_id).await; assert!( out.contains("apply_patch verification failed"), "expected verification failure message: {out}" ); assert!( out.contains("Failed to read"), "missing delete diagnostics should mention read failure: {out}" ); assert!( out.contains("missing.txt"), "missing delete diagnostics should surface target path: {out}" ); assert!(!harness.path_exists("missing.txt").await?); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_rejects_empty_patch() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let patch = "*** Begin Patch\n*** End Patch"; let call_id = "apply-empty"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("apply empty patch").await?; let out = harness.apply_patch_output(call_id).await; assert!( out.contains("patch rejected: empty patch"), "expected rejection for empty patch: {out}" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_delete_directory_reports_verification_error() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness.create_dir_all("dir").await?; let patch = "*** Begin Patch\n*** Delete File: dir\n*** End Patch"; let call_id = "apply-delete-dir"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("delete a directory via apply_patch").await?; let out = harness.apply_patch_output(call_id).await; assert!(out.contains("apply_patch verification failed")); assert!(out.contains("Failed to read")); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_rejects_path_traversal_outside_workspace() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let escape_path = harness .test() .config .cwd .parent() .expect("cwd should have parent") .join("escape.txt"); harness.remove_abs_path(&escape_path).await?; let patch = "*** Begin Patch\n*** Add File: ../escape.txt\n+outside\n*** End Patch"; let call_id = "apply-path-traversal"; mount_apply_patch(&harness, call_id, patch, "fail").await; harness .submit_with_permission_profile( "attempt to escape workspace via apply_patch", restrictive_workspace_write_profile(), ) .await?; let out = harness.apply_patch_output(call_id).await; assert!( out.contains( "patch rejected: writing outside of the project; rejected by user approval settings" ), "expected rejection message for path traversal: {out}" ); assert!( !harness.abs_path_exists(&escape_path).await?, "path traversal should be rejected; tool output: {out}" ); Ok(())}#[cfg(unix)]#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn intercepted_apply_patch_verification_uses_local_sandbox() -> Result<()> { skip_if_no_network!(Ok(())); skip_if_remote!(Ok(()), "symlink setup needs local filesystem link creation"); let harness = apply_patch_harness().await?; let denied_target = harness.path("denied-target.txt"); std::fs::write(&denied_target, "outside content\n")?; let link_rel = "soft-link.txt"; create_file_symlink(&denied_target, &harness.path(link_rel))?; let patch = format!( r#"*** Begin Patch*** Update File: {link_rel}@@-outside content+pwned*** End Patch"# ); let call_id = "apply-sandboxed-read"; mount_apply_patch_model_output( &harness, call_id, &patch, "fail", ApplyPatchModelOutput::ShellCommandViaHeredoc, ) .await; harness .submit_with_permission_profile( "attempt to read denied target via intercepted apply_patch", workspace_write_with_unreadable_path(AbsolutePathBuf::try_from(denied_target.clone())?), ) .await?; let out = harness.function_call_stdout(call_id).await; assert!( serde_json::from_str::<serde_json::Value>(&out).is_err(), "expected heredoc apply_patch output to be plain text" ); assert!( out.contains("apply_patch verification failed"), "expected sandboxed verification failure: {out}" ); assert!( out.contains("Failed to read"), "expected read failure: {out}" ); assert_eq!( std::fs::read_to_string(&denied_target)?, "outside content\n", "verification failure should leave the denied target unchanged" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_does_not_write_through_symlink_escape_outside_workspace() -> Result<()> { skip_if_no_network!(Ok(())); skip_if_remote!( Ok(()), "link escape setup needs local filesystem link creation" ); let test_root = tempfile::tempdir_in(std::env::current_dir()?)?; let work_dir = AbsolutePathBuf::try_from(test_root.path().join("work"))?; let outside_dir = AbsolutePathBuf::try_from(test_root.path().join("outside"))?; std::fs::create_dir_all(work_dir.as_path())?; std::fs::create_dir_all(outside_dir.as_path())?; let harness_work_dir = work_dir.clone(); let harness = apply_patch_harness_with(move |builder| { builder.with_config(move |config| { config.cwd = harness_work_dir; }) }) .await?; let original_contents = "original outside content\n"; let outside_file = outside_dir.join("victim.txt"); std::fs::write(&outside_file, original_contents)?; let link_rel = "soft-link.txt"; let link_path = harness.path(link_rel); match create_file_symlink(&outside_file, &link_path) { Ok(()) => {} Err(error) if cfg!(windows) => { eprintln!("Skipping Windows symlink apply_patch sandbox test: {error}"); return Ok(()); } Err(error) => return Err(error.into()), } let patch = format!( r#"*** Begin Patch*** Update File: {link_rel}@@-original outside content+pwned*** End Patch"# ); let call_id = "apply-symlink-escape"; mount_apply_patch(&harness, call_id, &patch, "fail").await; harness .submit_with_permission_profile( "attempt to escape workspace via apply_patch link", workspace_write_with_read_only_root(outside_dir.clone()), ) .await?; let out = harness.apply_patch_output(call_id).await; assert_eq!( std::fs::read_to_string(&outside_file)?, original_contents, "symlink escape should not modify the outside victim; tool output: {out}", ); let metadata = std::fs::symlink_metadata(&link_path)?; assert!(metadata.file_type().is_symlink()); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_preserves_existing_hard_link_outside_workspace() -> Result<()> { skip_if_no_network!(Ok(())); skip_if_remote!( Ok(()), "link setup needs local filesystem hard link creation" ); let test_root = tempfile::tempdir_in(std::env::current_dir()?)?; let work_dir = AbsolutePathBuf::try_from(test_root.path().join("work"))?; let outside_dir = AbsolutePathBuf::try_from(test_root.path().join("outside"))?; std::fs::create_dir_all(work_dir.as_path())?; std::fs::create_dir_all(outside_dir.as_path())?; let harness_work_dir = work_dir.clone(); let harness = apply_patch_harness_with(move |builder| { builder.with_config(move |config| { config.cwd = harness_work_dir; }) }) .await?; let outside_file = outside_dir.join("victim.txt"); std::fs::write(&outside_file, "original outside content\n")?; let link_rel = "hard-link.txt"; let link_path = harness.path(link_rel); std::fs::hard_link(&outside_file, &link_path)?; let patch = format!( r#"*** Begin Patch*** Update File: {link_rel}@@-original outside content+updated through existing hard link*** End Patch"# ); let call_id = "apply-hard-link"; mount_apply_patch(&harness, call_id, &patch, "ok").await; harness .submit_with_permission_profile( "update existing hard link via apply_patch", workspace_write_with_read_only_root(outside_dir.clone()), ) .await?; let out = harness.apply_patch_output(call_id).await; if cfg!(windows) { assert!( out.contains("patch rejected: writing outside of the project"), "Windows sandboxing intentionally rejects writes through existing hard links to files outside the workspace; tool output: {out}" ); assert_eq!( std::fs::read_to_string(&outside_file)?, "original outside content\n", "Windows rejection must leave the outside hard-link target unchanged" ); assert_eq!( std::fs::read_to_string(&link_path)?, "original outside content\n", "Windows rejection must leave the workspace hard-link path unchanged" ); std::fs::write(&outside_file, "post-reject outside write\n")?; assert_eq!( std::fs::read_to_string(&link_path)?, "post-reject outside write\n", "Windows rejection must not unlink or replace an existing hard link" ); return Ok(()); } assert!( out.contains("Success. Updated the following files:"), "apply_patch should intentionally allow updates through existing hard links; tool output: {out}" ); assert_eq!( std::fs::read_to_string(&outside_file)?, "updated through existing hard link\n", "apply_patch intentionally preserves existing hard-link semantics; the outside path observes the shared inode update" ); assert_eq!( std::fs::read_to_string(&link_path)?, "updated through existing hard link\n", "apply_patch intentionally preserves existing hard-link semantics; the workspace path observes the same update" ); std::fs::write(&outside_file, "post-apply outside write\n")?; assert_eq!( std::fs::read_to_string(&link_path)?, "post-apply outside write\n", "apply_patch must not unlink or replace an existing hard link; later writes through either path should still be visible" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace() -> Result<()> { // TODO(anp): Remove after apply-patch fixtures use target-native paths. skip_if_wine_exec!(Ok(()), "asserts POSIX workspace traversal behavior"); skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let escape_path = harness .test() .config .cwd .parent() .expect("cwd should have parent") .join("escape-move.txt"); harness.remove_abs_path(&escape_path).await?; harness.write_file("stay.txt", "from\n").await?; let patch = "*** Begin Patch\n*** Update File: stay.txt\n*** Move to: ../escape-move.txt\n@@\n-from\n+to\n*** End Patch"; let call_id = "apply-move-traversal"; mount_apply_patch(&harness, call_id, patch, "fail").await; harness .submit_with_permission_profile( "attempt move traversal via apply_patch", restrictive_workspace_write_profile(), ) .await?; let out = harness.apply_patch_output(call_id).await; assert!( out.contains( "patch rejected: writing outside of the project; rejected by user approval settings" ), "expected rejection message for path traversal: {out}" ); assert!( !harness.abs_path_exists(&escape_path).await?, "move path traversal should be rejected; tool output: {out}" ); assert_eq!(harness.read_file_text("stay.txt").await?, "from\n"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_verification_failure_has_no_side_effects() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; // Compose a patch that would create a file, then fail verification on an update. let call_id = "apply-partial-no-side-effects"; let patch = "*** Begin Patch\n*** Add File: created.txt\n+hello\n*** Update File: missing.txt\n@@\n-old\n+new\n*** End Patch"; mount_apply_patch(&harness, call_id, patch, "failed").await; harness.submit("attempt partial apply patch").await?; assert!( !harness.path_exists("created.txt").await?, "verification failure should prevent any filesystem changes" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_shell_command_heredoc_with_cd_updates_relative_workdir() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?; // Prepare a file inside a subdir; update it via cd && apply_patch heredoc form. harness.write_file("sub/in_sub.txt", "before\n").await?; let script = "cd sub && apply_patch <<'EOF'\n*** Begin Patch\n*** Update File: in_sub.txt\n@@\n-before\n+after\n*** End Patch\nEOF\n"; let call_id = "shell-heredoc-cd"; let bodies = vec![ sse(vec![ ev_response_created("resp-1"), ev_shell_command_call(call_id, script), ev_completed("resp-1"), ]), sse(vec![ ev_assistant_message("msg-1", "ok"), ev_completed("resp-2"), ]), ]; mount_sse_sequence(harness.server(), bodies).await; harness.submit("apply via shell heredoc with cd").await?; let out = harness.function_call_stdout(call_id).await; assert!( out.contains("Success."), "expected successful apply_patch invocation via shell_command: {out}" ); assert_eq!(harness.read_file_text("sub/in_sub.txt").await?, "after\n"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_can_use_shell_command_output_as_patch_input() -> Result<()> { skip_if_no_network!(Ok(())); skip_if_remote!( Ok(()), "shell_command output producer runs in the test runner, not in the remote apply_patch workspace", ); let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4").with_windows_cmd_shell()) .await?; let source_contents = "line1\nnaïve café\nline3\n"; harness.write_file("source.txt", source_contents).await?; let read_call_id = "read-source"; let apply_call_id = "apply-from-read"; fn stdout_from_shell_output(output: &str) -> String { let normalized = output.replace("\r\n", "\n").replace('\r', "\n"); normalized .split_once("Output:\n") .map(|x| x.1) .unwrap_or("") .trim_end_matches('\n') .to_string() } fn function_call_output_text(body: &serde_json::Value, call_id: &str) -> String { body.get("input") .and_then(serde_json::Value::as_array) .and_then(|items| { items.iter().find(|item| { item.get("type").and_then(serde_json::Value::as_str) == Some("function_call_output") && item.get("call_id").and_then(serde_json::Value::as_str) == Some(call_id) }) }) .and_then(|item| item.get("output").and_then(serde_json::Value::as_str)) .expect("function_call_output output string") .to_string() } struct DynamicApplyFromRead { num_calls: AtomicI32, read_call_id: String, apply_call_id: String, } impl Respond for DynamicApplyFromRead { fn respond(&self, request: &wiremock::Request) -> ResponseTemplate { let call_num = self.num_calls.fetch_add(1, Ordering::SeqCst); match call_num { 0 => { let command = if cfg!(windows) { // Encode the nested PowerShell script so `cmd.exe /c` does not leave the // read command wrapped in quotes, and suppress progress records so the // shell tool only returns the file contents back to apply_patch. let script = "$ProgressPreference = 'SilentlyContinue'; [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); [System.IO.File]::ReadAllText('source.txt', [System.Text.UTF8Encoding]::new($false))"; let encoded = BASE64_STANDARD.encode( script .encode_utf16() .flat_map(u16::to_le_bytes) .collect::<Vec<u8>>(), ); format!( "powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand {encoded}" ) } else { "cat source.txt".to_string() }; let args = json!({ "command": command, "login": false, }); let body = sse(vec![ ev_response_created("resp-1"), ev_shell_command_call_with_args(&self.read_call_id, &args), ev_completed("resp-1"), ]); ResponseTemplate::new(200) .insert_header("content-type", "text/event-stream") .set_body_string(body) } 1 => { let body_json: serde_json::Value = request.body_json().expect("request body should be json"); let read_output = function_call_output_text(&body_json, &self.read_call_id); let stdout = stdout_from_shell_output(&read_output); let patch_lines = stdout .lines() .map(|line| format!("+{line}")) .collect::<Vec<_>>() .join("\n"); let patch = format!( "*** Begin Patch\n*** Add File: target.txt\n{patch_lines}\n*** End Patch" ); let body = sse(vec![ ev_response_created("resp-2"), ev_apply_patch_custom_tool_call(&self.apply_call_id, &patch), ev_completed("resp-2"), ]); ResponseTemplate::new(200) .insert_header("content-type", "text/event-stream") .set_body_string(body) } 2 => { let body = sse(vec![ ev_assistant_message("msg-1", "ok"), ev_completed("resp-3"), ]); ResponseTemplate::new(200) .insert_header("content-type", "text/event-stream") .set_body_string(body) } _ => panic!("no response for call {call_num}"), } } } let responder = DynamicApplyFromRead { num_calls: AtomicI32::new(0), read_call_id: read_call_id.to_string(), apply_call_id: apply_call_id.to_string(), }; Mock::given(method("POST")) .and(path_regex(".*/responses$")) .respond_with(responder) .expect(3) .mount(harness.server()) .await; harness .submit("read source.txt, then apply it to target.txt") .await?; let target_contents = harness.read_file_text("target.txt").await?; assert_eq!(target_contents, source_contents); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_custom_tool_streaming_emits_updated_changes() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness_with(|builder| { builder.with_config(|config| { config .features .enable(Feature::ApplyPatchStreamingEvents) .expect("enable apply_patch streaming events"); }) }) .await?; let test = harness.test(); let codex = test.codex.clone(); let call_id = "apply-patch-streaming"; let patch = "*** Begin Patch\n*** Add File: streamed.txt\n+hello\n+world\n*** End Patch"; mount_sse_sequence( harness.server(), vec![ sse(vec![ ev_response_created("resp-1"), json!({ "type": "response.output_item.added", "item": { "type": "custom_tool_call", "call_id": call_id, "name": "apply_patch", "input": "", } }), json!({ "type": "response.custom_tool_call_input.delta", "call_id": call_id, "delta": "*** Begin Patch\n", }), json!({ "type": "response.custom_tool_call_input.delta", "call_id": call_id, "delta": "*** Add File: streamed.txt\n+hello", }), json!({ "type": "response.custom_tool_call_input.delta", "call_id": call_id, "delta": "\n+world\n*** End Patch", }), ev_apply_patch_custom_tool_call(call_id, patch), ev_completed("resp-1"), ]), sse(vec![ ev_assistant_message("msg-1", "done"), ev_completed("resp-2"), ]), ], ) .await; submit_without_wait(&harness, "create streamed file").await?; let mut updates = Vec::new(); wait_for_event(&codex, |event| match event { EventMsg::PatchApplyUpdated(update) => { updates.push(update.clone()); false } EventMsg::TurnComplete(_) => true, _ => false, }) .await; assert_eq!( updates .iter() .map(|update| update.call_id.as_str()) .collect::<Vec<_>>(), vec![call_id, call_id] ); assert_eq!( updates .first() .expect("first update") .changes .get(&std::path::PathBuf::from("streamed.txt")), Some(&codex_protocol::protocol::FileChange::Add { content: String::new(), }) ); assert_eq!( updates .last() .expect("last update") .changes .get(&std::path::PathBuf::from("streamed.txt")), Some(&codex_protocol::protocol::FileChange::Add { content: "hello\nworld\n".to_string(), }) ); assert_eq!( harness.read_file_text("streamed.txt").await?, "hello\nworld\n" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?; let test = harness.test(); let codex = test.codex.clone(); // Prepare a file inside a subdir; update it via cd && apply_patch heredoc form. harness.write_file("sub/in_sub.txt", "before\n").await?; let script = "cd sub && apply_patch <<'EOF'\n*** Begin Patch\n*** Update File: in_sub.txt\n@@\n-before\n+after\n*** End Patch\nEOF\n"; let call_id = "shell-heredoc-cd"; let args = json!({ "command": script, "timeout_ms": 30_000 }); let bodies = vec![ sse(vec![ ev_response_created("resp-1"), ev_function_call(call_id, "shell_command", &serde_json::to_string(&args)?), ev_completed("resp-1"), ]), sse(vec![ ev_assistant_message("msg-1", "ok"), ev_completed("resp-2"), ]), ]; mount_sse_sequence(harness.server(), bodies).await; submit_without_wait(&harness, "apply via shell heredoc with cd").await?; let mut saw_turn_diff = None; let mut saw_patch_begin = false; let mut patch_end_success = None; wait_for_event(&codex, |event| match event { EventMsg::PatchApplyBegin(begin) => { saw_patch_begin = true; assert_eq!(begin.call_id, call_id); false } EventMsg::PatchApplyEnd(end) => { assert_eq!(end.call_id, call_id); patch_end_success = Some(end.success); false } EventMsg::TurnDiff(ev) => { saw_turn_diff = Some(ev.unified_diff.clone()); false } EventMsg::TurnComplete(_) => true, _ => false, }) .await; assert!(saw_patch_begin, "expected PatchApplyBegin event"); let patch_end_success = patch_end_success.expect("expected PatchApplyEnd event to capture success flag"); assert!(patch_end_success); let diff = saw_turn_diff.expect("expected TurnDiff event"); assert!(diff.contains("diff --git"), "diff header missing: {diff:?}"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_turn_diff_paths_stay_repo_relative_when_session_cwd_is_nested() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness_with(|builder| { builder .with_model("gpt-5.4") .with_config(|config| { config.cwd = config.cwd.join("subdir"); }) .with_workspace_setup(|cwd, fs| async move { let cwd_uri = PathUri::from_path(&cwd)?; fs.create_directory( &cwd_uri, CreateDirectoryOptions { recursive: true }, /*sandbox*/ None, ) .await?; let repo_root = cwd.parent().expect("nested cwd should have parent"); let git_uri = PathUri::from_path(repo_root.join(".git"))?; let repo_file_uri = PathUri::from_path(repo_root.join("repo.txt"))?; fs.write_file( &git_uri, b"gitdir: /tmp/fake-worktree\n".to_vec(), /*sandbox*/ None, ) .await?; fs.write_file(&repo_file_uri, b"before\n".to_vec(), /*sandbox*/ None) .await?; Ok(()) }) }) .await?; let test = harness.test(); let codex = test.codex.clone(); let repo_root = harness .test() .config .cwd .parent() .expect("nested cwd should have parent"); let call_id = "apply-nested-cwd-repo-relative"; let patch = "*** Begin Patch\n*** Update File: ../repo.txt\n@@\n-before\n+after\n*** End Patch"; mount_apply_patch(&harness, call_id, patch, "updated repo-relative path").await; submit_without_wait(&harness, "update file outside nested cwd but inside repo").await?; let mut last_diff: Option<String> = None; wait_for_event(&codex, |event| match event { EventMsg::TurnDiff(ev) => { last_diff = Some(ev.unified_diff.clone()); false } EventMsg::TurnComplete(_) => true, _ => false, }) .await; let diff = last_diff.expect("expected TurnDiff event after update"); assert!( diff.contains("diff --git a/repo.txt b/repo.txt"), "diff should stay repo-relative: {diff:?}" ); assert!( !diff.contains(repo_root.as_path().to_string_lossy().as_ref()), "diff should not leak absolute repo paths: {diff:?}" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?; let test = harness.test(); let codex = test.codex.clone(); harness.write_file("invalid.txt", "ok\n").await?; let script = "apply_patch <<'EOF'\n*** Begin Patch\n*** Update File: invalid.txt\n@@\n-nope\n+changed\n*** End Patch\nEOF\n"; let call_id = "shell-apply-failure"; let args = json!({ "command": script, "timeout_ms": 5_000 }); let bodies = vec![ sse(vec![ ev_response_created("resp-1"), ev_function_call(call_id, "shell_command", &serde_json::to_string(&args)?), ev_completed("resp-1"), ]), sse(vec![ ev_assistant_message("msg-1", "fail"), ev_completed("resp-2"), ]), ]; mount_sse_sequence(harness.server(), bodies).await; submit_without_wait(&harness, "apply patch via shell").await?; let mut saw_turn_diff = false; wait_for_event(&codex, |event| match event { EventMsg::TurnDiff(_) => { saw_turn_diff = true; false } EventMsg::TurnComplete(_) => true, _ => false, }) .await; assert!( !saw_turn_diff, "turn diff should not be emitted when shell apply_patch fails verification" ); let out = harness.function_call_stdout(call_id).await; assert!( out.contains("Failed to find expected lines in"), "expected failure diagnostics: {out}" ); assert!( out.contains("invalid.txt"), "expected file path in output: {out}" ); assert_eq!(harness.read_file_text("invalid.txt").await?, "ok\n"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_shell_accepts_lenient_heredoc_wrapped_patch() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let file_name = "lenient.txt"; let patch_inner = format!("*** Begin Patch\n*** Add File: {file_name}\n+lenient\n*** End Patch\n"); let call_id = "apply-lenient"; mount_apply_patch_model_output( &harness, call_id, patch_inner.as_str(), "ok", ApplyPatchModelOutput::ShellCommandViaHeredoc, ) .await; harness.submit("apply lenient heredoc patch").await?; let out = harness.function_call_stdout(call_id).await; assert!( serde_json::from_str::<serde_json::Value>(&out).is_err(), "expected heredoc apply_patch output to be plain text" ); assert!( out.contains("Success. Updated the following files:"), "expected successful apply_patch output: {out}" ); assert!( out.contains(&format!("A {file_name}")), "expected created file in apply_patch output: {out}" ); assert_eq!(harness.read_file_text(file_name).await?, "lenient\n"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_end_of_file_anchor() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness.write_file("tail.txt", "alpha\nlast\n").await?; let patch = "*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch"; let call_id = "apply-eof"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("apply EOF-anchored patch").await?; assert_eq!(harness.read_file_text("tail.txt").await?, "alpha\nend\n"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_cli_missing_second_chunk_context_rejected() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness.write_file("two_chunks.txt", "a\nb\nc\nd\n").await?; // First chunk has @@, second chunk intentionally omits @@ to trigger parse error. let patch = "*** Begin Patch\n*** Update File: two_chunks.txt\n@@\n-b\n+B\n\n-d\n+D\n*** End Patch"; let call_id = "apply-missing-ctx-2nd"; mount_apply_patch(&harness, call_id, patch, "fail").await; harness.submit("apply missing context second chunk").await?; let out = harness.apply_patch_output(call_id).await; assert!(out.contains("apply_patch verification failed")); assert!( out.contains("Failed to find expected lines in"), "expected hunk context diagnostics: {out}" ); // Original file unchanged on failure assert_eq!( harness.read_file_text("two_chunks.txt").await?, "a\nb\nc\nd\n" ); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_emits_turn_diff_event_with_unified_diff() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let test = harness.test(); let codex = test.codex.clone(); let call_id = "apply-diff-event"; let file = "udiff.txt"; let patch = format!("*** Begin Patch\n*** Add File: {file}\n+hello\n*** End Patch\n"); mount_apply_patch(&harness, call_id, patch.as_str(), "ok").await; submit_without_wait(&harness, "emit diff").await?; let mut saw_turn_diff = None; wait_for_event(&codex, |event| match event { EventMsg::TurnDiff(ev) => { saw_turn_diff = Some(ev.unified_diff.clone()); false } EventMsg::TurnComplete(_) => true, _ => false, }) .await; let diff = saw_turn_diff.expect("expected TurnDiff event"); // Basic markers of a unified diff with file addition assert!(diff.contains("diff --git"), "diff header missing: {diff:?}"); assert!(diff.contains("--- /dev/null") || diff.contains("--- a/")); assert!(diff.contains("+++ b/")); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_turn_diff_tracks_local_and_remote_environment_paths() -> Result<()> { // TODO(anp): Remove after shared-cwd helpers use target-native paths. skip_if_wine_exec!( Ok(()), "requires a cwd valid in local POSIX and remote Windows environments" ); skip_if_no_network!(Ok(())); let Some(_remote_env) = get_remote_test_env() else { return Ok(()); }; let server = start_mock_server().await; let mut builder = test_codex(); let test = builder.build_with_remote_and_local_env(&server).await?; let file_name = "shared-turn-diff.txt"; let shared_cwd = PathBuf::from(format!( "/tmp/codex-remote-turn-diff-{}", SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() )) .abs(); let shared_cwd_uri = PathUri::from_path(&shared_cwd)?; let _ = fs::remove_dir_all(shared_cwd.as_path()); test.fs() .remove( &shared_cwd_uri, RemoveOptions { recursive: true, force: true, }, /*sandbox*/ None, ) .await?; fs::create_dir_all(shared_cwd.as_path())?; test.fs() .create_directory( &shared_cwd_uri, CreateDirectoryOptions { recursive: true }, /*sandbox*/ None, ) .await?; let local_patch = format!( "*** Begin Patch\n*** Environment ID: {LOCAL_ENVIRONMENT_ID}\n*** Add File: {file_name}\n+local\n*** End Patch" ); let remote_patch = format!( "*** Begin Patch\n*** Environment ID: {REMOTE_ENVIRONMENT_ID}\n*** Add File: {file_name}\n+remote\n*** End Patch" ); mount_sse_sequence( &server, vec![ sse(vec![ ev_response_created("resp-local"), ev_apply_patch_custom_tool_call("call-local", &local_patch), ev_completed("resp-local"), ]), sse(vec![ ev_response_created("resp-remote"), ev_apply_patch_custom_tool_call("call-remote", &remote_patch), ev_completed("resp-remote"), ]), sse(vec![ ev_response_created("resp-done"), ev_assistant_message("msg-done", "done"), ev_completed("resp-done"), ]), ], ) .await; let (sandbox_policy, permission_profile) = turn_permission_fields(PermissionProfile::Disabled, test.config.cwd.as_path()); let environments = vec![ local(shared_cwd.clone()), TurnEnvironmentSelection { environment_id: REMOTE_ENVIRONMENT_ID.to_string(), cwd: PathUri::from_abs_path(&shared_cwd), }, ]; test.codex .submit(Op::UserInput { items: vec![UserInput::Text { text: "apply matching patches to local and remote environments".into(), text_elements: Vec::new(), }], final_output_json_schema: None, responsesapi_client_metadata: None, additional_context: Default::default(), thread_settings: codex_protocol::protocol::ThreadSettingsOverrides { environments: Some(codex_protocol::protocol::TurnEnvironmentSelections::new( test.config.cwd.clone(), environments, )), approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(sandbox_policy), permission_profile, collaboration_mode: Some(codex_protocol::config_types::CollaborationMode { mode: codex_protocol::config_types::ModeKind::Default, settings: codex_protocol::config_types::Settings { model: test.session_configured.model.clone(), reasoning_effort: None, developer_instructions: None, }, }), ..Default::default() }, }) .await?; let mut last_diff = None; wait_for_event(&test.codex, |event| match event { EventMsg::TurnDiff(ev) => { last_diff = Some(ev.unified_diff.clone()); false } EventMsg::TurnComplete(_) => true, _ => false, }) .await; assert_eq!(fs::read_to_string(shared_cwd.join(file_name))?, "local\n"); assert_eq!( test.fs() .read_file_text( &PathUri::from_path(shared_cwd.join(file_name))?, /*sandbox*/ None, ) .await?, "remote\n" ); let diff = last_diff.expect("expected TurnDiff event"); assert_eq!( diff, r#"diff --git a/local/shared-turn-diff.txt b/local/shared-turn-diff.txtnew file mode 100644index 0000000000000000000000000000000000000000..40830374235df1c19661a2901b7ca73cc9499f3d--- /dev/null+++ b/local/shared-turn-diff.txt@@ -0,0 +1 @@+localdiff --git a/remote/shared-turn-diff.txt b/remote/shared-turn-diff.txtnew file mode 100644index 0000000000000000000000000000000000000000..9c998f7b995a7327177b38a90d1385170df2b94b--- /dev/null+++ b/remote/shared-turn-diff.txt@@ -0,0 +1 @@+remote"# ); let _ = fs::remove_dir_all(shared_cwd.as_path()); test.fs() .remove( &shared_cwd_uri, RemoveOptions { recursive: true, force: true, }, /*sandbox*/ None, ) .await?; Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let test = harness.test(); let codex = test.codex.clone(); let call1 = "agg-1"; let call2 = "agg-2"; let patch1 = "*** Begin Patch\n*** Add File: agg/a.txt\n+v1\n*** End Patch"; let patch2 = "*** Begin Patch\n*** Update File: agg/a.txt\n@@\n-v1\n+v2\n*** Add File: agg/b.txt\n+B\n*** End Patch"; let s1 = sse(vec![ ev_response_created("resp-1"), ev_apply_patch_custom_tool_call(call1, patch1), ev_completed("resp-1"), ]); let s2 = sse(vec![ ev_response_created("resp-2"), ev_apply_patch_custom_tool_call(call2, patch2), ev_completed("resp-2"), ]); let s3 = sse(vec![ ev_assistant_message("msg-1", "ok"), ev_completed("resp-3"), ]); mount_sse_sequence(harness.server(), vec![s1, s2, s3]).await; submit_without_wait(&harness, "aggregate diffs").await?; let mut last_diff: Option<String> = None; wait_for_event(&codex, |event| match event { EventMsg::TurnDiff(ev) => { last_diff = Some(ev.unified_diff.clone()); false } EventMsg::TurnComplete(_) => true, _ => false, }) .await; let diff = last_diff.expect("expected TurnDiff after two patches"); assert!(diff.contains("agg/a.txt"), "diff missing a.txt"); assert!(diff.contains("agg/b.txt"), "diff missing b.txt"); // Final content reflects v2 for a.txt assert!(diff.contains("+v2\n") || diff.contains("v2\n")); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; let test = harness.test(); let codex = test.codex.clone(); let call_success = "agg-success"; let call_failure = "agg-failure"; let patch_success = "*** Begin Patch\n*** Add File: partial/success.txt\n+ok\n*** End Patch"; let patch_failure = "*** Begin Patch\n*** Update File: partial/success.txt\n@@\n-missing\n+new\n*** End Patch"; let responses = vec![ sse(vec![ ev_response_created("resp-1"), ev_apply_patch_custom_tool_call(call_success, patch_success), ev_completed("resp-1"), ]), sse(vec![ ev_response_created("resp-2"), ev_apply_patch_custom_tool_call(call_failure, patch_failure), ev_completed("resp-2"), ]), sse(vec![ ev_assistant_message("msg-1", "failed"), ev_completed("resp-3"), ]), ]; mount_sse_sequence(harness.server(), responses).await; submit_without_wait(&harness, "apply patch twice with failure").await?; let mut last_diff: Option<String> = None; wait_for_event_with_timeout( &codex, |event| match event { EventMsg::TurnDiff(ev) => { last_diff = Some(ev.unified_diff.clone()); false } EventMsg::TurnComplete(_) => true, _ => false, }, Duration::from_secs(30), ) .await; let diff = last_diff.expect("expected TurnDiff after failed patch"); assert!( diff.contains("partial/success.txt"), "diff should still include the successful addition: {diff}" ); assert!( diff.contains("+ok"), "diff should include contents from successful patch: {diff}" ); let failure_out = harness.custom_tool_call_output(call_failure).await; assert!( failure_out.contains("apply_patch verification failed"), "expected verification failure output: {failure_out}" ); assert!( failure_out.contains("Failed to find expected lines in"), "expected missing context diagnostics: {failure_out}" ); assert_eq!(harness.read_file_text("partial/success.txt").await?, "ok\n"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_clears_aggregated_diff_after_inexact_delta() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness_with(|builder| { builder.with_workspace_setup(|cwd, fs| async move { let binary_path_uri = PathUri::from_path(cwd.join("binary.dat"))?; fs.write_file( &binary_path_uri, vec![0xff, 0xfe, 0xfd], /*sandbox*/ None, ) .await?; Ok(()) }) }) .await?; let test = harness.test(); let codex = test.codex.clone(); let call_success = "agg-success"; let call_inexact = "agg-inexact"; let patch_success = "*** Begin Patch\n*** Add File: partial/success.txt\n+ok\n*** End Patch"; let patch_inexact = "*** Begin Patch\n*** Add File: binary.dat\n+text\n*** End Patch"; let responses = vec![ sse(vec![ ev_response_created("resp-1"), ev_apply_patch_custom_tool_call(call_success, patch_success), ev_completed("resp-1"), ]), sse(vec![ ev_response_created("resp-2"), ev_apply_patch_custom_tool_call(call_inexact, patch_inexact), ev_completed("resp-2"), ]), sse(vec![ ev_assistant_message("msg-1", "done"), ev_completed("resp-3"), ]), ]; mount_sse_sequence(harness.server(), responses).await; submit_without_wait(&harness, "apply patch twice with inexact delta").await?; let mut last_diff: Option<String> = None; wait_for_event_with_timeout( &codex, |event| match event { EventMsg::TurnDiff(ev) => { last_diff = Some(ev.unified_diff.clone()); false } EventMsg::TurnComplete(_) => true, _ => false, }, Duration::from_secs(30), ) .await; assert_eq!( last_diff.as_deref(), Some(""), "inexact delta should clear the aggregate diff" ); assert_eq!(harness.read_file_text("partial/success.txt").await?, "ok\n"); assert_eq!(harness.read_file_text("binary.dat").await?, "text\n"); Ok(())}#[tokio::test(flavor = "multi_thread", worker_threads = 2)]async fn apply_patch_change_context_disambiguates_target() -> Result<()> { skip_if_no_network!(Ok(())); let harness = apply_patch_harness().await?; harness .write_file("multi_ctx.txt", "fn a\nx=10\ny=2\nfn b\nx=10\ny=20\n") .await?; let patch = "*** Begin Patch\n*** Update File: multi_ctx.txt\n@@ fn b\n-x=10\n+x=11\n*** End Patch"; let call_id = "apply-ctx"; mount_apply_patch(&harness, call_id, patch, "ok").await; harness.submit("apply with change_context").await?; let contents = harness.read_file_text("multi_ctx.txt").await?; assert_eq!(contents, "fn a\nx=10\ny=2\nfn b\nx=11\ny=20\n"); Ok(())}