Codex Handbook
core/src/turn_diff_tracker_tests.rs 495 lines
use super::*;use codex_apply_patch::AppliedPatchDelta;use codex_apply_patch::MaybeApplyPatchVerified;use codex_exec_server::LOCAL_FS;use codex_git_utils::ApplyGitRequest;use codex_git_utils::apply_git_patch;use codex_utils_absolute_path::AbsolutePathBuf;use pretty_assertions::assert_eq;use std::fs;use std::path::Path;use std::process::Command;use std::time::Duration;use std::time::Instant;use tempfile::tempdir;fn git_blob_sha1_hex(data: &str) -> String {    format!("{:x}", git_blob_sha1_hex_bytes(data.as_bytes()))}async fn apply_verified_patch(root: &Path, patch: &str) -> AppliedPatchDelta {    let cwd = AbsolutePathBuf::from_absolute_path(root).expect("absolute tempdir path");    let argv = vec!["apply_patch".to_string(), patch.to_string()];    match codex_apply_patch::maybe_parse_apply_patch_verified(        &argv,        &cwd,        LOCAL_FS.as_ref(),        /*sandbox*/ None,    )    .await    {        MaybeApplyPatchVerified::Body(_) => {}        other => panic!("expected verified patch action, got {other:?}"),    }    let mut stdout = Vec::new();    let mut stderr = Vec::new();    codex_apply_patch::apply_patch(        patch,        &cwd,        &mut stdout,        &mut stderr,        LOCAL_FS.as_ref(),        /*sandbox*/ None,    )    .await    .expect("patch should apply")}fn tracker_with_root(root: &Path) -> TurnDiffTracker {    TurnDiffTracker::with_environment_display_roots([("".to_string(), root.to_path_buf())])}#[tokio::test]async fn accumulates_add_then_update_as_single_add() {    let dir = tempdir().expect("tempdir");    let mut tracker = tracker_with_root(dir.path());    let add = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Add File: a.txt\n+foo\n*** End Patch",    )    .await;    tracker.track_delta("", &add);    let update = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Update File: a.txt\n@@\n foo\n+bar\n*** End Patch",    )    .await;    tracker.track_delta("", &update);    let right_oid = git_blob_sha1_hex("foo\nbar\n");    let expected = format!(        r#"diff --git a/a.txt b/a.txtnew file mode {REGULAR_FILE_MODE}index {ZERO_OID}..{right_oid}--- {DEV_NULL}+++ b/a.txt@@ -0,0 +1,2 @@+foo+bar"#,    );    assert_eq!(tracker.get_unified_diff(), Some(expected));}#[tokio::test]async fn invalidated_tracker_suppresses_existing_diff() {    let dir = tempdir().expect("tempdir");    let mut tracker = tracker_with_root(dir.path());    let add = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Add File: a.txt\n+foo\n*** End Patch",    )    .await;    tracker.track_delta("", &add);    tracker.invalidate();    assert_eq!(tracker.get_unified_diff(), None);}#[tokio::test]async fn tracks_same_absolute_path_across_multiple_environments() {    let dir = tempdir().expect("tempdir");    let add = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Add File: shared.txt\n+content\n*** End Patch",    )    .await;    let mut tracker = TurnDiffTracker::with_environment_display_roots([        ("local".to_string(), dir.path().to_path_buf()),        ("remote".to_string(), dir.path().to_path_buf()),    ]);    tracker.track_delta("remote", &add);    tracker.track_delta("local", &add);    let right_oid = git_blob_sha1_hex("content\n");    let expected = format!(        r#"diff --git a/local/shared.txt b/local/shared.txtnew file mode {REGULAR_FILE_MODE}index {ZERO_OID}..{right_oid}--- {DEV_NULL}+++ b/local/shared.txt@@ -0,0 +1 @@+contentdiff --git a/remote/shared.txt b/remote/shared.txtnew file mode {REGULAR_FILE_MODE}index {ZERO_OID}..{right_oid}--- {DEV_NULL}+++ b/remote/shared.txt@@ -0,0 +1 @@+content"#,    );    assert_eq!(tracker.get_unified_diff(), Some(expected));}#[tokio::test]async fn accumulates_delete() {    let dir = tempdir().expect("tempdir");    fs::write(dir.path().join("b.txt"), "x\n").expect("seed file");    let mut tracker = tracker_with_root(dir.path());    let delete = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Delete File: b.txt\n*** End Patch",    )    .await;    tracker.track_delta("", &delete);    let left_oid = git_blob_sha1_hex("x\n");    let expected = format!(        r#"diff --git a/b.txt b/b.txtdeleted file mode {REGULAR_FILE_MODE}index {left_oid}..{ZERO_OID}--- a/b.txt+++ {DEV_NULL}@@ -1 +0,0 @@-x"#,    );    assert_eq!(tracker.get_unified_diff(), Some(expected));}#[tokio::test]async fn accumulates_move_and_update() {    let dir = tempdir().expect("tempdir");    fs::write(dir.path().join("src.txt"), "line\n").expect("seed file");    let mut tracker = tracker_with_root(dir.path());    let update = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Update File: src.txt\n*** Move to: dst.txt\n@@\n-line\n+line2\n*** End Patch",    )    .await;    tracker.track_delta("", &update);    let left_oid = git_blob_sha1_hex("line\n");    let right_oid = git_blob_sha1_hex("line2\n");    let expected = format!(        r#"diff --git a/src.txt b/dst.txtindex {left_oid}..{right_oid}--- a/src.txt+++ b/dst.txt@@ -1 +1 @@-line+line2"#,    );    assert_eq!(tracker.get_unified_diff(), Some(expected));}#[tokio::test]async fn pure_rename_yields_no_diff() {    let dir = tempdir().expect("tempdir");    fs::write(dir.path().join("old.txt"), "same\n").expect("seed file");    let mut tracker = tracker_with_root(dir.path());    let rename = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Update File: old.txt\n*** Move to: new.txt\n@@\n same\n*** End Patch",    )    .await;    tracker.track_delta("", &rename);    assert_eq!(tracker.get_unified_diff(), None);}#[tokio::test]async fn add_over_existing_file_becomes_update() {    let dir = tempdir().expect("tempdir");    fs::write(dir.path().join("dup.txt"), "before\n").expect("seed file");    let mut tracker = tracker_with_root(dir.path());    let add = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Add File: dup.txt\n+after\n*** End Patch",    )    .await;    tracker.track_delta("", &add);    let left_oid = git_blob_sha1_hex("before\n");    let right_oid = git_blob_sha1_hex("after\n");    let expected = format!(        r#"diff --git a/dup.txt b/dup.txtindex {left_oid}..{right_oid}--- a/dup.txt+++ b/dup.txt@@ -1 +1 @@-before+after"#,    );    assert_eq!(tracker.get_unified_diff(), Some(expected));}#[tokio::test]async fn delete_then_readd_same_path_becomes_update() {    let dir = tempdir().expect("tempdir");    fs::write(dir.path().join("cycle.txt"), "before\n").expect("seed file");    let mut tracker = tracker_with_root(dir.path());    let delete = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Delete File: cycle.txt\n*** End Patch",    )    .await;    tracker.track_delta("", &delete);    let add = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Add File: cycle.txt\n+after\n*** End Patch",    )    .await;    tracker.track_delta("", &add);    let left_oid = git_blob_sha1_hex("before\n");    let right_oid = git_blob_sha1_hex("after\n");    let expected = format!(        r#"diff --git a/cycle.txt b/cycle.txtindex {left_oid}..{right_oid}--- a/cycle.txt+++ b/cycle.txt@@ -1 +1 @@-before+after"#,    );    assert_eq!(tracker.get_unified_diff(), Some(expected));}#[tokio::test]async fn move_over_existing_destination_without_content_change_deletes_source_only() {    let dir = tempdir().expect("tempdir");    fs::write(dir.path().join("a.txt"), "same\n").expect("seed source");    fs::write(dir.path().join("b.txt"), "same\n").expect("seed destination");    let mut tracker = tracker_with_root(dir.path());    let move_overwrite = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Update File: a.txt\n*** Move to: b.txt\n@@\n same\n*** End Patch",    )    .await;    tracker.track_delta("", &move_overwrite);    let left_oid = git_blob_sha1_hex("same\n");    let expected = format!(        r#"diff --git a/a.txt b/a.txtdeleted file mode {REGULAR_FILE_MODE}index {left_oid}..{ZERO_OID}--- a/a.txt+++ {DEV_NULL}@@ -1 +0,0 @@-same"#,    );    assert_eq!(tracker.get_unified_diff(), Some(expected));}#[tokio::test]async fn move_over_existing_destination_with_content_change_deletes_source_and_updates_destination(){    let dir = tempdir().expect("tempdir");    fs::write(dir.path().join("a.txt"), "from\n").expect("seed source");    fs::write(dir.path().join("b.txt"), "existing\n").expect("seed destination");    let mut tracker = tracker_with_root(dir.path());    let move_overwrite = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Update File: a.txt\n*** Move to: b.txt\n@@\n-from\n+new\n*** End Patch",    )    .await;    tracker.track_delta("", &move_overwrite);    let left_oid_a = git_blob_sha1_hex("from\n");    let left_oid_b = git_blob_sha1_hex("existing\n");    let right_oid_b = git_blob_sha1_hex("new\n");    let expected = format!(        r#"diff --git a/a.txt b/a.txtdeleted file mode {REGULAR_FILE_MODE}index {left_oid_a}..{ZERO_OID}--- a/a.txt+++ {DEV_NULL}@@ -1 +0,0 @@-fromdiff --git a/b.txt b/b.txtindex {left_oid_b}..{right_oid_b}--- a/b.txt+++ b/b.txt@@ -1 +1 @@-existing+new"#,    );    assert_eq!(tracker.get_unified_diff(), Some(expected));}#[tokio::test]async fn preserves_committed_change_order_with_delete_then_move_overwrite() {    let dir = tempdir().expect("tempdir");    fs::write(dir.path().join("a.txt"), "from\n").expect("seed source");    fs::write(dir.path().join("b.txt"), "existing\n").expect("seed destination");    let mut tracker = tracker_with_root(dir.path());    let ordered_patch = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Delete File: b.txt\n*** Update File: a.txt\n*** Move to: b.txt\n@@\n-from\n+new\n*** End Patch",    )    .await;    tracker.track_delta("", &ordered_patch);    let left_oid_a = git_blob_sha1_hex("from\n");    let left_oid_b = git_blob_sha1_hex("existing\n");    let right_oid_b = git_blob_sha1_hex("new\n");    let expected = format!(        r#"diff --git a/a.txt b/a.txtdeleted file mode {REGULAR_FILE_MODE}index {left_oid_a}..{ZERO_OID}--- a/a.txt+++ {DEV_NULL}@@ -1 +0,0 @@-fromdiff --git a/b.txt b/b.txtindex {left_oid_b}..{right_oid_b}--- a/b.txt+++ b/b.txt@@ -1 +1 @@-existing+new"#,    );    assert_eq!(tracker.get_unified_diff(), Some(expected));}#[tokio::test]async fn reuses_rendered_diffs_for_unchanged_paths() {    let dir = tempdir().expect("tempdir");    let mut tracker = tracker_with_root(dir.path());    let add_a = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Add File: a.txt\n+one\n*** End Patch",    )    .await;    tracker.track_delta("", &add_a);    assert_eq!(tracker.rendered_diff_count(), 1);    let add_b = apply_verified_patch(        dir.path(),        "*** Begin Patch\n*** Add File: b.txt\n+two\n*** End Patch",    )    .await;    tracker.track_delta("", &add_b);    assert_eq!(tracker.rendered_diff_count(), 2);    assert_eq!(        tracker.get_unified_diff(),        tracker.get_unified_diff(),        "reading the cached aggregate must not render file diffs",    );    assert_eq!(tracker.rendered_diff_count(), 2);}#[tokio::test]async fn repeated_updates_only_rerender_the_touched_path() {    let dir = tempdir().expect("tempdir");    let mut tracker = tracker_with_root(dir.path());    for patch in [        "*** Begin Patch\n*** Add File: stable.txt\n+stable\n*** End Patch".to_string(),        "*** Begin Patch\n*** Add File: hot.txt\n+value 0\n*** End Patch".to_string(),    ] {        tracker.track_delta("", &apply_verified_patch(dir.path(), &patch).await);    }    for value in 1..=40 {        let patch = format!(            "*** Begin Patch\n*** Update File: hot.txt\n@@\n-value {}\n+value {value}\n*** End Patch",            value - 1,        );        tracker.track_delta("", &apply_verified_patch(dir.path(), &patch).await);    }    assert_eq!(tracker.rendered_diff_count(), 42);}#[test]fn large_rewrite_returns_promptly_and_preserves_exact_content() {    let dir = tempdir().expect("tempdir");    assert!(        Command::new("git")            .args(["init", "--quiet"])            .current_dir(dir.path())            .status()            .expect("run git init")            .success()    );    assert!(        Command::new("git")            .args(["config", "core.autocrlf", "false"])            .current_dir(dir.path())            .status()            .expect("disable line ending conversion")            .success()    );    let old_content = (0..48_000)        .map(|line| format!("old line {line:05}\n"))        .collect::<String>();    let new_content = (0..48_000)        .map(|line| format!("new line {line:05}\n"))        .collect::<String>();    let path = dir.path().join("large.txt");    fs::write(&path, &old_content).expect("seed large file");    assert!(        Command::new("git")            .args(["add", "large.txt"])            .current_dir(dir.path())            .status()            .expect("run git add")            .success()    );    let tracker = tracker_with_root(dir.path());    let tracked_path = TrackedPath::new("", &path);    let started = Instant::now();    let diff = tracker        .render_diff(            &tracked_path,            Some(&old_content),            &tracked_path,            Some(&new_content),        )        .expect("complete rewrite should produce a diff");    assert!(        started.elapsed() < Duration::from_secs(2),        "large rewrite took {:?}",        started.elapsed(),    );    let result = apply_git_patch(&ApplyGitRequest {        cwd: dir.path().to_path_buf(),        diff,        revert: false,        preflight: false,    })    .expect("apply generated diff");    assert_eq!(result.exit_code, 0, "{}", result.stderr);    assert_eq!(        fs::read_to_string(path).expect("read large file"),        new_content    );}