use std::fs;
use std::path::{Path, PathBuf};
use serde_json::{json, Value};
use super::helpers::AftProcess;
fn configured_aft(project: &Path) -> AftProcess {
let mut aft = AftProcess::spawn();
let configure = aft.send(
&json!({
"id": "cfg-apply-patch",
"command": "configure",
"harness": "opencode",
"project_root": project,
})
.to_string(),
);
assert_eq!(
configure["success"], true,
"configure failed: {configure:?}"
);
aft
}
fn write_file(root: &Path, relative: &str, content: &str) -> PathBuf {
let path = root.join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&path, content).unwrap();
path
}
fn apply(aft: &mut AftProcess, id: &str, patch_text: &str) -> Value {
aft.send(
&json!({
"id": id,
"command": "apply_patch",
"params": { "patch_text": patch_text },
})
.to_string(),
)
}
fn preview(aft: &mut AftProcess, id: &str, patch_text: &str) -> Value {
aft.send(
&json!({
"id": id,
"command": "apply_patch",
"params": { "patch_text": patch_text, "preview": true },
})
.to_string(),
)
}
#[test]
fn apply_patch_full_multifile_and_single_undo_reverts_all() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_file(root, "update.txt", "alpha\nold\nomega\n");
write_file(root, "delete.txt", "remove me\n");
let mut aft = configured_aft(root);
let patch = r#"*** Begin Patch
*** Add File: add.txt
+created
*** Update File: update.txt
@@
alpha
-old
+new
omega
*** Delete File: delete.txt
*** End Patch"#;
let resp = apply(&mut aft, "apply-full", patch);
assert_eq!(resp["success"], true, "patch failed: {resp:?}");
assert_eq!(resp["complete"], true);
assert_eq!(resp["partial"], false);
assert_eq!(resp["metadata"]["files"].as_array().unwrap().len(), 3);
assert_eq!(
fs::read_to_string(root.join("add.txt")).unwrap(),
"created\n"
);
assert_eq!(
fs::read_to_string(root.join("update.txt")).unwrap(),
"alpha\nnew\nomega\n"
);
assert!(!root.join("delete.txt").exists());
let undo = aft.send(&json!({ "id": "undo-full", "command": "undo" }).to_string());
assert_eq!(undo["success"], true, "undo failed: {undo:?}");
assert_eq!(undo["operation"], true);
assert_eq!(
undo["restored_count"], 2,
"content backups restored: {undo:?}"
);
assert!(!root.join("add.txt").exists());
assert_eq!(
fs::read_to_string(root.join("update.txt")).unwrap(),
"alpha\nold\nomega\n"
);
assert_eq!(
fs::read_to_string(root.join("delete.txt")).unwrap(),
"remove me\n"
);
assert!(aft.shutdown().success());
}
#[test]
fn apply_patch_update_uses_reflow_fuzzy_hunk() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_file(
root,
"reflow.txt",
"const message = first + second + third;\n",
);
let mut aft = configured_aft(root);
let patch = r#"*** Begin Patch
*** Update File: reflow.txt
@@
-const message = first +
- second +
- third;
+const message = first + second + fourth;
*** End Patch"#;
let resp = apply(&mut aft, "apply-reflow", patch);
assert_eq!(resp["success"], true, "reflow patch failed: {resp:?}");
assert_eq!(
fs::read_to_string(root.join("reflow.txt")).unwrap(),
"const message = first + second + fourth;\n"
);
assert!(aft.shutdown().success());
}
#[test]
fn apply_patch_move_happy_path_and_undo_restores_source() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_file(root, "source.txt", "before\nkeep\n");
let mut aft = configured_aft(root);
let patch = r#"*** Begin Patch
*** Update File: source.txt
*** Move to: nested/dest.txt
@@
-before
+after
keep
*** End Patch"#;
let resp = apply(&mut aft, "apply-move", patch);
assert_eq!(resp["success"], true, "move failed: {resp:?}");
let file = &resp["metadata"]["files"][0];
assert_eq!(file["type"], "move");
assert_eq!(file["relativePath"], "nested/dest.txt");
assert!(file["movePath"]
.as_str()
.unwrap()
.ends_with("nested/dest.txt"));
assert!(!root.join("source.txt").exists());
assert_eq!(
fs::read_to_string(root.join("nested/dest.txt")).unwrap(),
"after\nkeep\n"
);
let undo = aft.send(&json!({ "id": "undo-move", "command": "undo" }).to_string());
assert_eq!(undo["success"], true, "undo failed: {undo:?}");
assert_eq!(
fs::read_to_string(root.join("source.txt")).unwrap(),
"before\nkeep\n"
);
assert!(!root.join("nested/dest.txt").exists());
assert!(aft.shutdown().success());
}
#[test]
fn apply_patch_partial_keeps_successful_hunks_and_reports_failure() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_file(root, "one.txt", "old\n");
write_file(root, "two.txt", "actual\n");
let mut aft = configured_aft(root);
let patch = r#"*** Begin Patch
*** Update File: one.txt
@@
-old
+new
*** Update File: two.txt
@@
-missing
+replacement
*** End Patch"#;
let resp = apply(&mut aft, "apply-partial", patch);
assert_eq!(
resp["success"], true,
"partial should be success envelope: {resp:?}"
);
assert_eq!(resp["complete"], false);
assert_eq!(resp["partial"], true);
assert_eq!(resp["all_failed"], false);
assert_eq!(fs::read_to_string(root.join("one.txt")).unwrap(), "new\n");
assert_eq!(
fs::read_to_string(root.join("two.txt")).unwrap(),
"actual\n"
);
assert!(resp["failures"][0]["error"]
.as_str()
.unwrap()
.contains("Failed to find expected lines"));
assert!(aft.shutdown().success());
}
#[test]
fn apply_patch_total_failure_returns_error_envelope_without_writes() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_file(root, "target.txt", "actual\n");
let mut aft = configured_aft(root);
let patch = r#"*** Begin Patch
*** Update File: target.txt
@@
-missing
+replacement
*** End Patch"#;
let resp = apply(&mut aft, "apply-total-failure", patch);
assert_eq!(
resp["success"], false,
"total failure should be error: {resp:?}"
);
assert_eq!(resp["code"], "apply_patch_failed");
assert_eq!(resp["complete"], false);
assert_eq!(resp["all_failed"], true);
assert_eq!(resp["partial"], false);
assert_eq!(resp["metadata"]["files"].as_array().unwrap().len(), 0);
assert_eq!(
fs::read_to_string(root.join("target.txt")).unwrap(),
"actual\n"
);
assert!(aft.shutdown().success());
}
#[test]
fn apply_patch_preview_reports_diff_and_leaves_disk_unchanged() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_file(root, "existing.txt", "old\n");
let mut aft = configured_aft(root);
let patch = r#"*** Begin Patch
*** Add File: preview-new.txt
+created
*** Update File: existing.txt
@@
-old
+new
*** End Patch"#;
let resp = preview(&mut aft, "preview-valid", patch);
assert_eq!(resp["success"], true, "preview failed: {resp:?}");
assert_eq!(resp["preview"], true);
assert!(resp["preview_diff"]
.as_str()
.unwrap()
.contains("preview-new.txt"));
assert_eq!(resp["affected_rel_paths"].as_array().unwrap().len(), 2);
assert!(!root.join("preview-new.txt").exists());
assert_eq!(
fs::read_to_string(root.join("existing.txt")).unwrap(),
"old\n"
);
let invalid = r#"*** Begin Patch
*** Update File: existing.txt
@@
-missing
+new
*** End Patch"#;
let err = preview(&mut aft, "preview-invalid", invalid);
assert_eq!(
err["success"], false,
"invalid preview should fail: {err:?}"
);
assert_eq!(
fs::read_to_string(root.join("existing.txt")).unwrap(),
"old\n"
);
assert!(aft.shutdown().success());
}
#[test]
fn apply_patch_add_existing_fails_and_syntax_rollback_does_not_poison_undo() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
write_file(root, "exists.txt", "already\n");
write_file(root, "good.ts", "const good = 1;\n");
write_file(root, "bad.ts", "const value = 1;\n");
let mut aft = configured_aft(root);
let add_existing = r#"*** Begin Patch
*** Add File: exists.txt
+replacement
*** End Patch"#;
let add_resp = apply(&mut aft, "add-existing", add_existing);
assert_eq!(
add_resp["success"], false,
"add existing should fail: {add_resp:?}"
);
assert!(add_resp["failures"][0]["error"]
.as_str()
.unwrap()
.contains("file already exists"));
assert_eq!(
fs::read_to_string(root.join("exists.txt")).unwrap(),
"already\n"
);
let mixed = r#"*** Begin Patch
*** Update File: good.ts
@@
-const good = 1;
+const good = 2;
*** Update File: bad.ts
@@
-const value = 1;
+const value = {;
*** End Patch"#;
let resp = apply(&mut aft, "syntax-rollback", mixed);
assert_eq!(
resp["success"], true,
"mixed patch should be partial: {resp:?}"
);
assert_eq!(resp["partial"], true);
assert_eq!(
fs::read_to_string(root.join("good.ts")).unwrap(),
"const good = 2;\n"
);
assert_eq!(
fs::read_to_string(root.join("bad.ts")).unwrap(),
"const value = 1;\n"
);
let undo = aft.send(&json!({ "id": "undo-syntax", "command": "undo" }).to_string());
assert_eq!(
undo["success"], true,
"undo should restore only successful hunk: {undo:?}"
);
assert_eq!(
fs::read_to_string(root.join("good.ts")).unwrap(),
"const good = 1;\n"
);
assert_eq!(
fs::read_to_string(root.join("bad.ts")).unwrap(),
"const value = 1;\n"
);
assert!(aft.shutdown().success());
}