ApplyPatch语法与回归测试
Apply Patch 的测试不能只断言 parser 返回了某个 enum。当前仓库至少分成六层:crate 单元测试、25 个 scenario 最终目录快照、CLI 参数/stdin 契约、tool 的行尾与部分失败矩阵、Unix no-follow/TOCTOU 测试, 以及 Core handler/runtime/SSE 集成。它们分别证明语法与算法、文件状态、公开命令、兼容模式、安全重检 和上层事件/审批装配。
本文承接ApplyPatch安全与原子性、ApplyPatch流式识别和ExecServer并发与集成测试,面向需要修改 patch parser 或新增回归场景的读者。范围是测试组织和覆盖边界,不重复实现算法。读完后,你应能为一个新语法或失败案例选择合适 fixture,并写出输入、断言和未证明范围。
1. Fixture场景
1.1 目录契约
scenario 由可选 input、必需 patch.txt 和 expected 组成;runner 将 input 复制到临时目录,执行 standalone apply_patch,并设置 CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS=1,再比较完整目录快照。当前目录有 25 个 场景,覆盖新增、删除、更新、move、失败后部分成功、Unicode、EOF、CRLF 与 mixed line endings。
源码位置:codex-rs/apply-patch/tests/suite/scenarios.rs :: test_apply_patch_scenarios、run_apply_patch_scenario
let tmp = tempdir()?;
let input_dir = dir.join("input");
if input_dir.is_dir() {
copy_dir_recursive(&input_dir, tmp.path())?;
}
let patch = fs::read_to_string(dir.join("patch.txt"))?;
Command::new(codex_utils_cargo_bin::cargo_bin("apply_patch")?)
.arg(patch)
.env(CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR, "1")
.current_dir(tmp.path())
.output()?;
let expected_snapshot = snapshot_dir(&dir.join("expected"))?;
let actual_snapshot = snapshot_dir(tmp.path())?;
assert_eq!(actual_snapshot, expected_snapshot);runner 故意不把 exit status 作为唯一断言,scenario 的核心契约是最终文件树;因此失败场景也必须准备 expected 状态。
1.2 快照边界
快照记录相对路径、目录和文件 bytes;读取 metadata 会跟随测试环境中 Buck2 materialized symlink,使 Cargo/Buck2 得到相同 fixture 语义。
2. CLI契约
CLI tests 用 assert_cmd 分别通过 argv 和 stdin 提交 patch,检查成功 stdout、exit status 和实际文件内容; 它们覆盖最小公开命令契约,不依赖内部 Hunk 结构。更完整的失败 stderr、行尾和 multi-operation 断言位于 tool suite。
源码位置:codex-rs/apply-patch/tests/suite/cli.rs :: test_apply_patch_cli_add_and_update、test_apply_patch_cli_stdin_add_and_update
apply_patch_command()?
.arg(add_patch)
.current_dir(tmp.path())
.assert()
.success()
.stdout(format!("Success. Updated the following files:\nA {file}\n"));
assert_eq!(fs::read_to_string(&absolute_path)?, "hello\n");CLI 基础测试没有设置 preserve-line-endings 环境变量,因此使用 standalone 默认 LF 模式;不能用这两项 测试证明 CRLF 保留。
3. Tool与stdin
tool tests 通过 argv 调用 standalone binary,并默认设置 preserve-line-endings 环境变量。它们验证多操作 summary、multiple chunks、EOF overlap、CRLF/CR/mixed endings、move overwrite、missing context、目录删除 失败和 partial success。专门的 legacy 测试移除环境变量,断言 CRLF 更新会回到 LF。
源码位置:codex-rs/apply-patch/tests/suite/tool.rs :: run_apply_patch_in_dir、test_apply_patch_cli_applies_multiple_operations
run_apply_patch_in_dir(tmp.path(), patch)?.success().stdout(
"Success. Updated the following files:\nA nested/new.txt\nM modify.txt\nD delete.txt\n",
);
assert_eq!(fs::read_to_string(add_path)?, "created\n");
assert_eq!(fs::read_to_string(&modify_path)?, "line1\nchanged\n");
assert!(!delete_path.exists());4. No-follow回归
Unix-only no_follow suite 不只测试 symlink leaf,还遍历 ancestor link、move source/destination 和缺失父目录 创建;另一个测试先 verify 普通目录,再把祖先目录替换为 symlink,确认 apply 阶段会重新检查。第三个 测试证明普通文件在 no-follow 下仍可成功,而 standalone 默认行为仍会 follow link。
源码位置:codex-rs/apply-patch/tests/suite/no_follow.rs :: no-follow test module
cargo test -p codex-apply-patch --test all suite::no_follow -- --test-threads=15. 失败回归
5.1 语法失败
parser/streaming parser 测试断言错误类型和 line number;它们证明输入无法形成合法 Hunk,不证明文件状态。
5.2 应用失败
CLI tests 对 missing context 断言目标文件保持原内容;lib tests 对 write failure、unreadable destination、symlink delete 和 failed move 断言 ApplyPatchFailure.delta() 的 exactness。
源码位置:codex-rs/apply-patch/src/lib.rs :: test_failed_move_returns_committed_destination_delta
let failure = apply_patch(/* patch, fs */).await.expect_err("source removal should fail");
assert!(failure
.delta()
.changes()
.iter()
.any(|change| change.path == destination));6. Core集成
Core 的 handler/runtime 单测验证 Environment ID、行尾 feature、审批键、sandbox context 和权限路径; apply_patch_custom_tool_streaming_emits_updated_changes 使用 mock Responses SSE,验证流式参数到 PatchApplyUpdated,并在完整调用后断言文件内容。它和 scenario 的边界不同:前者证明 Core 装配和事件, 后者证明 standalone 最终目录。
源码位置:
codex-rs/core/src/tools/handlers/apply_patch_tests.rs:: handler testscodex-rs/core/src/tools/runtimes/apply_patch_tests.rs:: runtime testscodex-rs/core/tests/suite/apply_patch_cli.rs::apply_patch_custom_tool_streaming_emits_updated_changes
let patch = "*** Begin Patch\n*** Add File: streamed.txt\n+hello\n+world\n*** End Patch";
mount_sse_sequence(/* input delta events + final tool call */).await;
submit_without_wait(&harness, "create streamed file").await?;
// collect PatchApplyUpdated events and assert call_id sequence7. 验证
7.1 全场景
scenario runner 当前一次遍历所有 scenario 目录;新增目录即可进入完整回归,但必须同时提供 input、patch.txt 和 expected,且 expected 要表达失败后的真实状态。
源码位置:codex-rs/apply-patch/tests/suite/scenarios.rs :: test_apply_patch_scenarios
cd codex-rs
cargo test -p codex-apply-patch --test all scenarios -- --test-threads=17.2 分层命令
cd codex-rs
cargo test -p codex-apply-patch --lib -- --test-threads=1
cargo test -p codex-apply-patch --test all cli -- --test-threads=1
cargo test -p codex-apply-patch --test all tool -- --test-threads=1
cargo test -p codex-apply-patch --test all suite::no_follow -- --test-threads=1
cargo test -p codex-core --lib tools::handlers::apply_patch::tests -- --test-threads=1
cargo test -p codex-core --lib tools::runtimes::apply_patch::tests -- --test-threads=1
RUST_MIN_STACK=8388608 cargo test -p codex-core --test all apply_patch_custom_tool_streaming_emits_updated_changes -- --test-threads=1这些命令分别证明 fixture state、公开 CLI、行尾/失败矩阵、no-follow、Core approval/runtime 和 streaming event;它们不证明所有平台 sandbox backend,也不证明模型一定生成正确 patch。
8. 源码排查
rg -n "test_apply_patch_scenarios|snapshot_dir|expected_snapshot" codex-rs/apply-patch/tests/suite/scenarios.rs
rg -n "assert\(\)|stdout|stderr|missing_context|failed_move" codex-rs/apply-patch/tests/suite codex-rs/apply-patch/src/lib.rs
rg -n "no_follow|preserves_crlf|mixed_line|legacy_line" codex-rs/apply-patch/tests/suite
rg -n "PatchApplyUpdated|streaming_emits_updated_changes" codex-rs/core/tests/suite/apply_patch_cli.rsApply Patch 测试闭环是:crate unit 验证语法与算法,scenario 验证最终目录,CLI/tool 验证公开命令与行尾 契约,no-follow 证明路径重检,Core 集成证明 environment/approval/runtime 与流式事件;失败场景必须明确 是 unchanged、partial commit 还是 inexact delta。下一篇CodeMode架构总览进入 Code Mode 执行子系列。
