mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Make turn diff tracking operation backed (#21180)
## Summary - replace filesystem-based turn diff tracking with an operation-backed accumulator - preserve enough verified apply_patch state to render move-overwrite cases correctly - keep the turn/diff/updated contract intact while removing remote-only turn-diff test skips This takes the assumption that no 3P services rely on the output format of `apply_patch` ## Why For the CCA file system isolation push --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -12,6 +12,7 @@ use std::sync::atomic::AtomicI32;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_exec_server::CreateDirectoryOptions;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
@@ -377,10 +378,6 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff(
|
||||
model_output: ApplyPatchModelOutput,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
let test = harness.test();
|
||||
@@ -1050,10 +1047,6 @@ async fn apply_patch_custom_tool_streaming_emits_updated_changes() -> Result<()>
|
||||
#[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(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?;
|
||||
let test = harness.test();
|
||||
@@ -1113,13 +1106,88 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<(
|
||||
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 {
|
||||
fs.create_directory(
|
||||
&cwd,
|
||||
CreateDirectoryOptions { recursive: true },
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
let repo_root = cwd.parent().expect("nested cwd should have parent");
|
||||
fs.write_file(
|
||||
&repo_root.join(".git"),
|
||||
b"gitdir: /tmp/fake-worktree\n".to_vec(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
fs.write_file(
|
||||
&repo_root.join("repo.txt"),
|
||||
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",
|
||||
ApplyPatchModelOutput::Function,
|
||||
)
|
||||
.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(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness_with(|builder| builder.with_model("gpt-5.4")).await?;
|
||||
let test = harness.test();
|
||||
@@ -1265,10 +1333,6 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff(
|
||||
model_output: ApplyPatchModelOutput,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
let test = harness.test();
|
||||
@@ -1300,64 +1364,9 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[test_case(ApplyPatchModelOutput::Freeform)]
|
||||
#[test_case(ApplyPatchModelOutput::Function)]
|
||||
#[test_case(ApplyPatchModelOutput::Shell)]
|
||||
#[test_case(ApplyPatchModelOutput::ShellViaHeredoc)]
|
||||
#[test_case(ApplyPatchModelOutput::ShellCommandViaHeredoc)]
|
||||
async fn apply_patch_turn_diff_for_rename_with_content_change(
|
||||
model_output: ApplyPatchModelOutput,
|
||||
) -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
let test = harness.test();
|
||||
let codex = test.codex.clone();
|
||||
|
||||
// Seed original file
|
||||
harness.write_file("old.txt", "old\n").await?;
|
||||
|
||||
// Patch: update + move
|
||||
let call_id = "apply-rename-change";
|
||||
let patch = "*** Begin Patch\n*** Update File: old.txt\n*** Move to: new.txt\n@@\n-old\n+new\n*** End Patch";
|
||||
mount_apply_patch(&harness, call_id, patch, "ok", model_output).await;
|
||||
|
||||
submit_without_wait(&harness, "rename with change").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 rename");
|
||||
// Basic checks: shows old -> new, and the content delta
|
||||
assert!(diff.contains("old.txt"), "diff missing old path: {diff:?}");
|
||||
assert!(diff.contains("new.txt"), "diff missing new path: {diff:?}");
|
||||
assert!(diff.contains("--- a/"), "missing old header");
|
||||
assert!(diff.contains("+++ b/"), "missing new header");
|
||||
assert!(diff.contains("-old\n"), "missing removal line");
|
||||
assert!(diff.contains("+new\n"), "missing addition line");
|
||||
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(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
let test = harness.test();
|
||||
@@ -1408,10 +1417,6 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()>
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_remote!(
|
||||
Ok(()),
|
||||
"TurnDiffTracker currently reads the test-runner filesystem, not the remote executor filesystem",
|
||||
);
|
||||
|
||||
let harness = apply_patch_harness().await?;
|
||||
let test = harness.test();
|
||||
@@ -1482,6 +1487,75 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result
|
||||
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 {
|
||||
fs.write_file(
|
||||
&cwd.join("binary.dat"),
|
||||
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_function_call(call_success, patch_success),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
sse(vec![
|
||||
ev_response_created("resp-2"),
|
||||
ev_apply_patch_function_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)]
|
||||
#[test_case(ApplyPatchModelOutput::Freeform)]
|
||||
#[test_case(ApplyPatchModelOutput::Function)]
|
||||
|
||||
Reference in New Issue
Block a user