Skip to content

Commit 622b7d9

Browse files
authored
chore: testing on apply_path (openai#5557)
1 parent af3882d commit 622b7d9

File tree

9 files changed

+1469
-2
lines changed

9 files changed

+1469
-2
lines changed

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
mod cli;
2+
#[cfg(not(target_os = "windows"))]
3+
mod tool;
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
use assert_cmd::Command;
2+
use pretty_assertions::assert_eq;
3+
use std::fs;
4+
use std::path::Path;
5+
use tempfile::tempdir;
6+
7+
fn run_apply_patch_in_dir(dir: &Path, patch: &str) -> anyhow::Result<assert_cmd::assert::Assert> {
8+
let mut cmd = Command::cargo_bin("apply_patch")?;
9+
cmd.current_dir(dir);
10+
Ok(cmd.arg(patch).assert())
11+
}
12+
13+
fn apply_patch_command(dir: &Path) -> anyhow::Result<Command> {
14+
let mut cmd = Command::cargo_bin("apply_patch")?;
15+
cmd.current_dir(dir);
16+
Ok(cmd)
17+
}
18+
19+
#[test]
20+
fn test_apply_patch_cli_applies_multiple_operations() -> anyhow::Result<()> {
21+
let tmp = tempdir()?;
22+
let modify_path = tmp.path().join("modify.txt");
23+
let delete_path = tmp.path().join("delete.txt");
24+
25+
fs::write(&modify_path, "line1\nline2\n")?;
26+
fs::write(&delete_path, "obsolete\n")?;
27+
28+
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";
29+
30+
run_apply_patch_in_dir(tmp.path(), patch)?.success().stdout(
31+
"Success. Updated the following files:\nA nested/new.txt\nM modify.txt\nD delete.txt\n",
32+
);
33+
34+
assert_eq!(
35+
fs::read_to_string(tmp.path().join("nested/new.txt"))?,
36+
"created\n"
37+
);
38+
assert_eq!(fs::read_to_string(&modify_path)?, "line1\nchanged\n");
39+
assert!(!delete_path.exists());
40+
41+
Ok(())
42+
}
43+
44+
#[test]
45+
fn test_apply_patch_cli_applies_multiple_chunks() -> anyhow::Result<()> {
46+
let tmp = tempdir()?;
47+
let target_path = tmp.path().join("multi.txt");
48+
fs::write(&target_path, "line1\nline2\nline3\nline4\n")?;
49+
50+
let patch = "*** Begin Patch\n*** Update File: multi.txt\n@@\n-line2\n+changed2\n@@\n-line4\n+changed4\n*** End Patch";
51+
52+
run_apply_patch_in_dir(tmp.path(), patch)?
53+
.success()
54+
.stdout("Success. Updated the following files:\nM multi.txt\n");
55+
56+
assert_eq!(
57+
fs::read_to_string(&target_path)?,
58+
"line1\nchanged2\nline3\nchanged4\n"
59+
);
60+
61+
Ok(())
62+
}
63+
64+
#[test]
65+
fn test_apply_patch_cli_moves_file_to_new_directory() -> anyhow::Result<()> {
66+
let tmp = tempdir()?;
67+
let original_path = tmp.path().join("old/name.txt");
68+
let new_path = tmp.path().join("renamed/dir/name.txt");
69+
fs::create_dir_all(original_path.parent().expect("parent should exist"))?;
70+
fs::write(&original_path, "old content\n")?;
71+
72+
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";
73+
74+
run_apply_patch_in_dir(tmp.path(), patch)?
75+
.success()
76+
.stdout("Success. Updated the following files:\nM renamed/dir/name.txt\n");
77+
78+
assert!(!original_path.exists());
79+
assert_eq!(fs::read_to_string(&new_path)?, "new content\n");
80+
81+
Ok(())
82+
}
83+
84+
#[test]
85+
fn test_apply_patch_cli_rejects_empty_patch() -> anyhow::Result<()> {
86+
let tmp = tempdir()?;
87+
88+
apply_patch_command(tmp.path())?
89+
.arg("*** Begin Patch\n*** End Patch")
90+
.assert()
91+
.failure()
92+
.stderr("No files were modified.\n");
93+
94+
Ok(())
95+
}
96+
97+
#[test]
98+
fn test_apply_patch_cli_reports_missing_context() -> anyhow::Result<()> {
99+
let tmp = tempdir()?;
100+
let target_path = tmp.path().join("modify.txt");
101+
fs::write(&target_path, "line1\nline2\n")?;
102+
103+
apply_patch_command(tmp.path())?
104+
.arg("*** Begin Patch\n*** Update File: modify.txt\n@@\n-missing\n+changed\n*** End Patch")
105+
.assert()
106+
.failure()
107+
.stderr("Failed to find expected lines in modify.txt:\nmissing\n");
108+
assert_eq!(fs::read_to_string(&target_path)?, "line1\nline2\n");
109+
110+
Ok(())
111+
}
112+
113+
#[test]
114+
fn test_apply_patch_cli_rejects_missing_file_delete() -> anyhow::Result<()> {
115+
let tmp = tempdir()?;
116+
117+
apply_patch_command(tmp.path())?
118+
.arg("*** Begin Patch\n*** Delete File: missing.txt\n*** End Patch")
119+
.assert()
120+
.failure()
121+
.stderr("Failed to delete file missing.txt\n");
122+
123+
Ok(())
124+
}
125+
126+
#[test]
127+
fn test_apply_patch_cli_rejects_empty_update_hunk() -> anyhow::Result<()> {
128+
let tmp = tempdir()?;
129+
130+
apply_patch_command(tmp.path())?
131+
.arg("*** Begin Patch\n*** Update File: foo.txt\n*** End Patch")
132+
.assert()
133+
.failure()
134+
.stderr("Invalid patch hunk on line 2: Update file hunk for path 'foo.txt' is empty\n");
135+
136+
Ok(())
137+
}
138+
139+
#[test]
140+
fn test_apply_patch_cli_requires_existing_file_for_update() -> anyhow::Result<()> {
141+
let tmp = tempdir()?;
142+
143+
apply_patch_command(tmp.path())?
144+
.arg("*** Begin Patch\n*** Update File: missing.txt\n@@\n-old\n+new\n*** End Patch")
145+
.assert()
146+
.failure()
147+
.stderr(
148+
"Failed to read file to update missing.txt: No such file or directory (os error 2)\n",
149+
);
150+
151+
Ok(())
152+
}
153+
154+
#[test]
155+
fn test_apply_patch_cli_move_overwrites_existing_destination() -> anyhow::Result<()> {
156+
let tmp = tempdir()?;
157+
let original_path = tmp.path().join("old/name.txt");
158+
let destination = tmp.path().join("renamed/dir/name.txt");
159+
fs::create_dir_all(original_path.parent().expect("parent should exist"))?;
160+
fs::create_dir_all(destination.parent().expect("parent should exist"))?;
161+
fs::write(&original_path, "from\n")?;
162+
fs::write(&destination, "existing\n")?;
163+
164+
run_apply_patch_in_dir(
165+
tmp.path(),
166+
"*** Begin Patch\n*** Update File: old/name.txt\n*** Move to: renamed/dir/name.txt\n@@\n-from\n+new\n*** End Patch",
167+
)?
168+
.success()
169+
.stdout("Success. Updated the following files:\nM renamed/dir/name.txt\n");
170+
171+
assert!(!original_path.exists());
172+
assert_eq!(fs::read_to_string(&destination)?, "new\n");
173+
174+
Ok(())
175+
}
176+
177+
#[test]
178+
fn test_apply_patch_cli_add_overwrites_existing_file() -> anyhow::Result<()> {
179+
let tmp = tempdir()?;
180+
let path = tmp.path().join("duplicate.txt");
181+
fs::write(&path, "old content\n")?;
182+
183+
run_apply_patch_in_dir(
184+
tmp.path(),
185+
"*** Begin Patch\n*** Add File: duplicate.txt\n+new content\n*** End Patch",
186+
)?
187+
.success()
188+
.stdout("Success. Updated the following files:\nA duplicate.txt\n");
189+
190+
assert_eq!(fs::read_to_string(&path)?, "new content\n");
191+
192+
Ok(())
193+
}
194+
195+
#[test]
196+
fn test_apply_patch_cli_delete_directory_fails() -> anyhow::Result<()> {
197+
let tmp = tempdir()?;
198+
fs::create_dir(tmp.path().join("dir"))?;
199+
200+
apply_patch_command(tmp.path())?
201+
.arg("*** Begin Patch\n*** Delete File: dir\n*** End Patch")
202+
.assert()
203+
.failure()
204+
.stderr("Failed to delete file dir\n");
205+
206+
Ok(())
207+
}
208+
209+
#[test]
210+
fn test_apply_patch_cli_rejects_invalid_hunk_header() -> anyhow::Result<()> {
211+
let tmp = tempdir()?;
212+
213+
apply_patch_command(tmp.path())?
214+
.arg("*** Begin Patch\n*** Frobnicate File: foo\n*** End Patch")
215+
.assert()
216+
.failure()
217+
.stderr("Invalid patch hunk on line 2: '*** Frobnicate File: foo' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'\n");
218+
219+
Ok(())
220+
}
221+
222+
#[test]
223+
fn test_apply_patch_cli_updates_file_appends_trailing_newline() -> anyhow::Result<()> {
224+
let tmp = tempdir()?;
225+
let target_path = tmp.path().join("no_newline.txt");
226+
fs::write(&target_path, "no newline at end")?;
227+
228+
run_apply_patch_in_dir(
229+
tmp.path(),
230+
"*** Begin Patch\n*** Update File: no_newline.txt\n@@\n-no newline at end\n+first line\n+second line\n*** End Patch",
231+
)?
232+
.success()
233+
.stdout("Success. Updated the following files:\nM no_newline.txt\n");
234+
235+
let contents = fs::read_to_string(&target_path)?;
236+
assert!(contents.ends_with('\n'));
237+
assert_eq!(contents, "first line\nsecond line\n");
238+
239+
Ok(())
240+
}
241+
242+
#[test]
243+
fn test_apply_patch_cli_failure_after_partial_success_leaves_changes() -> anyhow::Result<()> {
244+
let tmp = tempdir()?;
245+
let new_file = tmp.path().join("created.txt");
246+
247+
apply_patch_command(tmp.path())?
248+
.arg("*** Begin Patch\n*** Add File: created.txt\n+hello\n*** Update File: missing.txt\n@@\n-old\n+new\n*** End Patch")
249+
.assert()
250+
.failure()
251+
.stdout("")
252+
.stderr("Failed to read file to update missing.txt: No such file or directory (os error 2)\n");
253+
254+
assert_eq!(fs::read_to_string(&new_file)?, "hello\n");
255+
256+
Ok(())
257+
}

codex-rs/core/src/tools/handlers/apply_patch.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ impl ToolHandler for ApplyPatchHandler {
100100

101101
let req = ApplyPatchRequest {
102102
patch: apply.action.patch.clone(),
103-
cwd,
103+
cwd: apply.action.cwd.clone(),
104104
timeout_ms: None,
105105
user_explicitly_approved: apply.user_explicitly_approved_this_action,
106106
codex_exe: turn.codex_linux_sandbox_exe.clone(),

codex-rs/core/src/tools/handlers/shell.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ impl ShellHandler {
154154

155155
let req = ApplyPatchRequest {
156156
patch: apply.action.patch.clone(),
157-
cwd: exec_params.cwd.clone(),
157+
cwd: apply.action.cwd.clone(),
158158
timeout_ms: exec_params.timeout_ms,
159159
user_explicitly_approved: apply.user_explicitly_approved_this_action,
160160
codex_exe: turn.codex_linux_sandbox_exe.clone(),

codex-rs/core/tests/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ path = "lib.rs"
1010
anyhow = { workspace = true }
1111
assert_cmd = { workspace = true }
1212
codex-core = { workspace = true }
13+
codex-protocol = { workspace = true }
1314
notify = { workspace = true }
1415
regex-lite = { workspace = true }
1516
serde_json = { workspace = true }

0 commit comments

Comments
 (0)