Codex Handbook
core/src/tools/handlers/apply_patch_tests.rs 291 lines
use super::*;use codex_apply_patch::MaybeApplyPatchVerified;use codex_exec_server::LOCAL_FS;use codex_protocol::permissions::FileSystemSandboxPolicy;use codex_protocol::protocol::FileChange;use core_test_support::PathBufExt;use core_test_support::PathExt;use pretty_assertions::assert_eq;use serde_json::json;use std::collections::HashMap;use std::path::PathBuf;use std::sync::Arc;use tempfile::TempDir;use tokio::sync::Mutex;use crate::session::tests::make_session_and_context;use crate::tools::context::ToolInvocation;use crate::tools::hook_names::HookToolName;use crate::tools::registry::PostToolUsePayload;use crate::tools::registry::PreToolUsePayload;use crate::turn_diff_tracker::TurnDiffTracker;fn sample_patch() -> &'static str {    r#"*** Begin Patch*** Add File: hello.txt+hello*** End Patch"#}async fn invocation_for_payload(payload: ToolPayload) -> ToolInvocation {    let (session, turn) = make_session_and_context().await;    ToolInvocation {        session: session.into(),        turn: turn.into(),        cancellation_token: tokio_util::sync::CancellationToken::new(),        tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),        call_id: "call-apply-patch".to_string(),        tool_name: codex_tools::ToolName::plain("apply_patch"),        source: crate::tools::context::ToolCallSource::Direct,        payload,    }}#[tokio::test]async fn pre_tool_use_payload_uses_freeform_patch_input() {    let patch = sample_patch();    let payload = ToolPayload::Custom {        input: patch.to_string(),    };    let invocation = invocation_for_payload(payload).await;    let handler = ApplyPatchHandler::default();    assert_eq!(        handler.pre_tool_use_payload(&invocation),        Some(PreToolUsePayload {            tool_name: HookToolName::apply_patch(),            tool_input: json!({ "command": patch }),        })    );}#[tokio::test]async fn post_tool_use_payload_uses_patch_input_and_tool_output() {    let patch = sample_patch();    let payload = ToolPayload::Custom {        input: patch.to_string(),    };    let invocation = invocation_for_payload(payload).await;    let output = ApplyPatchToolOutput::from_text("Success. Updated files.".to_string());    let handler = ApplyPatchHandler::default();    assert_eq!(        handler.post_tool_use_payload(&invocation, &output),        Some(PostToolUsePayload {            tool_name: HookToolName::apply_patch(),            tool_use_id: "call-apply-patch".to_string(),            tool_input: json!({ "command": patch }),            tool_response: json!("Success. Updated files."),        })    );}#[test]fn diff_consumer_streams_apply_patch_changes() {    let mut consumer = ApplyPatchArgumentDiffConsumer::default();    assert!(        consumer            .push_delta("call-1".to_string(), "*** Begin Patch\n")            .is_none()    );    let event = consumer        .push_delta("call-1".to_string(), "*** Add File: hello.txt\n+hello")        .expect("progress event");    assert_eq!(        (event.call_id, event.changes),        (            "call-1".to_string(),            HashMap::from([(                PathBuf::from("hello.txt"),                FileChange::Add {                    content: String::new(),                },            )]),        )    );    assert!(        consumer            .push_delta("call-1".to_string(), "\n+world")            .is_none()    );    assert!(        consumer            .push_delta("call-1".to_string(), "\n*** End Patch")            .is_none()    );    let event = consumer        .finish_update_on_complete()        .expect("finish parser")        .expect("progress event");    assert_eq!(        (event.call_id, event.changes),        (            "call-1".to_string(),            HashMap::from([(                PathBuf::from("hello.txt"),                FileChange::Add {                    content: "hello\nworld\n".to_string(),                },            )]),        )    );}#[test]fn diff_consumer_streams_apply_patch_changes_with_environment_header() {    let mut consumer = ApplyPatchArgumentDiffConsumer::default();    assert!(        consumer            .push_delta(                "call-1".to_string(),                "*** Begin Patch\n*** Environment ID: remote\n",            )            .is_none()    );    let event = consumer        .push_delta("call-1".to_string(), "*** Add File: hello.txt\n+hello")        .expect("progress event");    assert_eq!(        event.changes,        HashMap::from([(            PathBuf::from("hello.txt"),            FileChange::Add {                content: String::new(),            },        )])    );}#[test]fn diff_consumer_sends_next_update_after_buffer_interval() {    let mut consumer = ApplyPatchArgumentDiffConsumer::default();    consumer.push_delta("call-1".to_string(), "*** Begin Patch\n");    let first = consumer        .push_delta("call-1".to_string(), "*** Add File: hello.txt\n+hello")        .expect("first progress event");    assert_eq!(        first.changes,        HashMap::from([(            PathBuf::from("hello.txt"),            FileChange::Add {                content: String::new(),            },        )])    );    consumer.last_sent_at =        Some(std::time::Instant::now() - APPLY_PATCH_ARGUMENT_DIFF_BUFFER_INTERVAL);    let second = consumer        .push_delta("call-1".to_string(), "\n+world")        .expect("second progress event");    assert_eq!(        second.changes,        HashMap::from([(            PathBuf::from("hello.txt"),            FileChange::Add {                content: "hello\n".to_string(),            },        )])    );}#[test]fn reconcile_environment_id_requires_selection_when_enabled() {    assert_eq!(        require_environment_id(Some("remote"), /*allow_environment_id*/ false),        Err(FunctionCallError::RespondToModel(            "apply_patch environment selection is unavailable for this turn".to_string(),        ))    );    assert_eq!(        require_environment_id(            /*parsed_environment_id*/ None, /*allow_environment_id*/ true        ),        Ok(None)    );}#[tokio::test]async fn approval_keys_include_move_destination() {    let tmp = TempDir::new().expect("tmp");    let cwd_path = tmp.path();    let cwd = cwd_path.abs();    std::fs::create_dir_all(cwd_path.join("old")).expect("create old dir");    std::fs::create_dir_all(cwd_path.join("renamed/dir")).expect("create dest dir");    std::fs::write(cwd_path.join("old/name.txt"), "old content\n").expect("write old file");    let patch = r#"*** Begin Patch*** Update File: old/name.txt*** Move to: renamed/dir/name.txt@@-old content+new content*** End Patch"#;    let argv = vec!["apply_patch".to_string(), patch.to_string()];    let action = match codex_apply_patch::maybe_parse_apply_patch_verified(        &argv,        &cwd,        LOCAL_FS.as_ref(),        /*sandbox*/ None,    )    .await    {        MaybeApplyPatchVerified::Body(action) => action,        other => panic!("expected patch body, got: {other:?}"),    };    let keys = file_paths_for_action(&action);    assert_eq!(keys.len(), 2);}#[test]fn write_permissions_for_paths_skip_dirs_already_writable_under_workspace_root() {    let tmp = TempDir::new().expect("tmp");    let cwd_path = tmp.path();    let cwd = cwd_path.abs();    let nested = cwd_path.join("nested");    std::fs::create_dir_all(&nested).expect("create nested dir");    let file_path = AbsolutePathBuf::try_from(nested.join("file.txt"))        .expect("nested file path should be absolute");    let sandbox_policy = FileSystemSandboxPolicy::workspace_write(        &[],        /*exclude_tmpdir_env_var*/ true,        /*exclude_slash_tmp*/ false,    );    let permissions = write_permissions_for_paths(&[file_path], &sandbox_policy, &cwd);    assert_eq!(permissions, None);}#[test]fn write_permissions_for_paths_keep_dirs_outside_workspace_root() {    let tmp = TempDir::new().expect("tmp");    let cwd = tmp.path().join("workspace");    let outside = tmp.path().join("outside");    std::fs::create_dir_all(&cwd).expect("create cwd");    std::fs::create_dir_all(&outside).expect("create outside dir");    let file_path = AbsolutePathBuf::try_from(outside.join("file.txt"))        .expect("outside file path should be absolute");    let cwd_abs = cwd.abs();    let sandbox_policy = FileSystemSandboxPolicy::workspace_write(        &[],        /*exclude_tmpdir_env_var*/ true,        /*exclude_slash_tmp*/ true,    );    let permissions = write_permissions_for_paths(&[file_path], &sandbox_policy, &cwd_abs);    let expected_outside =        dunce::simplified(&outside.canonicalize().expect("canonicalize outside dir")).abs();    assert_eq!(        permissions            .and_then(|profile| profile.file_system)            .and_then(|fs| fs.legacy_read_write_roots())            .and_then(|(_read, write)| write),        Some(vec![expected_outside])    );}