use std::error::Error;
use agents_tools::{
ApplyDiffError, ApplyDiffMode, apply_diff_to_text, apply_diff_to_text_with_mode,
};
fn update(input: &str, diff: &str) -> String {
apply_diff_to_text(input, diff).expect("diff should apply")
}
fn create(diff: &str) -> String {
apply_diff_to_text_with_mode("ignored", diff, ApplyDiffMode::Create).expect("diff should apply")
}
fn update_error(input: &str, diff: &str) -> ApplyDiffError {
apply_diff_to_text(input, diff).expect_err("diff should fail")
}
#[test]
fn empty_update_is_a_noop() {
for input in ["", "one", "one\n", "one\ntwo\n"] {
assert_eq!(update(input, ""), input);
}
}
#[test]
fn empty_create_produces_an_empty_file() {
assert_eq!(create(""), "");
}
#[test]
fn create_strips_one_prefix_from_every_line() {
assert_eq!(create("+alpha\n++beta\n+gamma"), "alpha\n+beta\ngamma");
}
#[test]
fn create_requires_an_explicit_plus_for_a_trailing_newline() {
assert_eq!(create("+alpha\n"), "alpha");
assert_eq!(create("+alpha\n+"), "alpha\n");
assert_eq!(create("+alpha\n+\n"), "alpha\n");
}
#[test]
fn create_rejects_the_first_unprefixed_line_with_a_stable_error() {
let error =
apply_diff_to_text_with_mode("", "+valid\ninvalid", ApplyDiffMode::Create).unwrap_err();
assert_eq!(error.message(), "Invalid Add File Line: invalid");
assert_eq!(error.to_string(), error.message());
}
#[test]
fn create_stops_at_section_terminators() {
for marker in [
"*** End Patch",
"*** Update File: other.txt",
"*** Delete File: other.txt",
"*** Add File: other.txt",
] {
assert_eq!(create(&format!("+kept\n{marker}\nnot parsed")), "kept");
}
}
#[test]
fn create_does_not_treat_end_of_file_as_a_terminator() {
let error = apply_diff_to_text_with_mode("", "+kept\n*** End of File", ApplyDiffMode::Create)
.unwrap_err();
assert_eq!(error.message(), "Invalid Add File Line: *** End of File");
}
#[test]
fn diff_line_endings_are_normalized() {
assert_eq!(create("+alpha\r\n+beta\r\n+"), "alpha\nbeta\n");
assert_eq!(create("+alpha\r\r\n+beta\r"), "alpha\nbeta");
}
#[test]
fn update_preserves_uniform_crlf_line_endings() {
assert_eq!(
update("first\r\nold\r\nlast\r\n", " first\n-old\n+new\n last"),
"first\r\nnew\r\nlast\r\n"
);
}
#[test]
fn floating_insertion_into_empty_input_retains_the_empty_input_line() {
assert_eq!(update("", "@@\n+alpha\n+beta"), "alpha\nbeta\n");
}
#[test]
fn replacement_without_an_anchor_applies_from_the_start() {
let input = "red\ngreen\nblue";
assert_eq!(update(input, "-red\n+scarlet"), "scarlet\ngreen\nblue");
}
#[test]
fn contextual_replacement_preserves_a_trailing_newline() {
let input = "first\nsecond\nthird\n";
let diff = "@@ first\n-second\n+changed\n third";
assert_eq!(update(input, diff), "first\nchanged\nthird\n");
}
#[test]
fn deletion_preserves_surrounding_content() {
let input = "keep\nremove\nstay\n";
assert_eq!(update(input, "@@ keep\n-remove\n stay"), "keep\nstay\n");
}
#[test]
fn an_unprefixed_empty_diff_line_is_blank_context() {
let input = "use alpha\n\nfn run() {}";
let diff = " use alpha\n+use beta\n\n fn run() {}";
assert_eq!(update(input, diff), "use alpha\nuse beta\n\nfn run() {}");
}
#[test]
fn one_section_can_contain_multiple_edit_runs() {
let input = "a\nb\nc\nd\ne";
let diff = " a\n-b\n+B\n c\n-d\n+D\n e";
assert_eq!(update(input, diff), "a\nB\nc\nD\ne");
}
#[test]
fn multiple_anchored_sections_apply_in_order() {
let input = "fn first() {\n old();\n}\n\nfn second() {\n old();\n}\n";
let diff = concat!(
"@@ fn first() {\n",
"- old();\n",
"+ new();\n",
"@@ fn second() {\n",
"- old();\n",
"+ newer();"
);
assert_eq!(
update(input, diff),
"fn first() {\n new();\n}\n\nfn second() {\n newer();\n}\n"
);
}
#[test]
fn a_bare_anchor_starts_a_floating_section() {
assert_eq!(
update("one\ntwo\n", "@@\n one\n-two\n+second"),
"one\nsecond\n"
);
}
#[test]
fn a_single_missing_anchor_falls_back_to_context_search() {
let input = "before\ntarget\nafter";
let diff = "@@ a label that is not present\n-target\n+changed";
assert_eq!(update(input, diff), "before\nchanged\nafter");
}
#[test]
fn line_number_text_is_treated_as_an_anchor_label() {
let input = "one\ntwo\n";
let diff = "@@ -1,2 +1,2 @@\n one\n-two\n+2";
assert_eq!(update(input, diff), "one\n2\n");
}
#[test]
fn stacked_anchors_narrow_the_search_scope() {
let input = concat!(
"mod first {\n",
" fn target() { old(); }\n",
"}\n",
"mod second {\n",
" fn helper() {}\n",
" fn target() { old(); }\n",
"}\n"
);
let diff = concat!(
"@@ mod second {\n",
"@@ fn target() { old(); }\n",
"-}\n",
"+ fn marker() {}\n",
"+}"
);
assert_eq!(
update(input, diff),
concat!(
"mod first {\n",
" fn target() { old(); }\n",
"}\n",
"mod second {\n",
" fn helper() {}\n",
" fn target() { old(); }\n",
" fn marker() {}\n",
"}\n"
)
);
}
#[test]
fn a_parent_anchor_can_be_reused_by_a_later_section() {
let input = "impl Item {\n fn one() { old(); }\n fn two() { old(); }\n}\n";
let diff = concat!(
"@@ impl Item {\n",
"@@ fn one() { old(); }\n",
"+ // first\n",
"@@ impl Item {\n",
"@@ fn two() { old(); }\n",
"+ // second"
);
assert_eq!(
update(input, diff),
"impl Item {\n fn one() { old(); }\n // first\n fn two() { old(); }\n // second\n}\n"
);
}
#[test]
fn stacked_anchors_require_every_named_anchor_to_match() {
let error = update_error(
"mod actual {\n fn target() {}\n}\n",
"@@ mod missing {\n@@ fn target() {}\n- fn target() {}\n+ fn target() { work(); }",
);
assert_eq!(error.message(), "Invalid Anchor 0:\nmod missing {");
let error = update_error(
"mod actual {\n fn target() {}\n}\n",
"@@ mod actual {\n@@ fn missing() {}\n- fn target() {}\n+ fn target() { work(); }",
);
assert_eq!(error.message(), "Invalid Anchor 1:\n fn missing() {}");
}
#[test]
fn a_missing_named_anchor_before_a_bare_anchor_is_rejected() {
let error = update_error("a\nb\n", "@@ missing\n@@\n-b\n+B");
assert_eq!(error.message(), "Invalid Anchor 0:\nmissing");
}
#[test]
fn a_trailing_bare_anchor_in_a_stack_is_allowed() {
let input = "struct Only;\nimpl Only {\n fn run() {}\n}\n";
let diff = "@@ impl Only {\n@@\n- fn run() {}\n+ fn run() { work(); }";
assert_eq!(
update(input, diff),
"struct Only;\nimpl Only {\n fn run() { work(); }\n}\n"
);
}
#[test]
fn anchors_can_match_after_trimming_both_ends() {
let input = "header\n target \n old\n";
let diff = "@@ target\n- old\n+ new";
assert_eq!(update(input, diff), "header\n target \n new\n");
}
#[test]
fn anchors_use_the_expected_unicode_whitespace_set() {
let input = "parent\n\u{feff}target\u{feff}\nold\n";
let diff = "@@ parent\n@@ target\n-old\n+new";
assert_eq!(update(input, diff), "parent\n\u{feff}target\u{feff}\nnew\n");
let input = "parent\n\u{0085}target\u{0085}\nold\n";
let error = update_error(input, diff);
assert_eq!(error.message(), "Invalid Anchor 1:\ntarget");
}
#[test]
fn context_can_match_after_trimming_trailing_whitespace() {
let input = "alpha \nbeta\n";
let diff = " alpha\n-beta\n+BETA";
assert_eq!(update(input, diff), "alpha \nBETA\n");
}
#[test]
fn context_can_match_after_trimming_both_ends() {
let input = " alpha \n beta \n";
let diff = " alpha\n-beta\n+BETA";
assert_eq!(update(input, diff), " alpha \nBETA\n");
}
#[test]
fn context_uses_the_expected_unicode_whitespace_set() {
assert_eq!(
update("\u{feff}alpha\u{feff}\nbeta", " alpha\n-beta\n+BETA"),
"\u{feff}alpha\u{feff}\nBETA"
);
assert_eq!(
update_error("\u{0085}alpha\u{0085}\nbeta", " alpha\n-beta\n+BETA").message(),
"Invalid Context 0:\nalpha\nbeta"
);
}
#[test]
fn exact_context_is_preferred_over_an_earlier_trimmed_match() {
let input = " target \ntarget\nend";
let diff = "-target\n+changed";
assert_eq!(update(input, diff), " target \nchanged\nend");
}
#[test]
fn trailing_only_fuzz_is_preferred_over_full_trim_fuzz() {
let input = " key \nkey\nend";
let diff = "- key\n+changed";
assert_eq!(update(input, diff), "changed\nkey\nend");
}
#[test]
fn context_mismatch_reports_the_cursor_and_context() {
let error = update_error("one\ntwo\n", " missing\n-two\n+second");
assert_eq!(error.message(), "Invalid Context 0:\nmissing\ntwo");
}
#[test]
fn end_of_file_marker_prefers_the_last_matching_context() {
let input = "same\nold\nmiddle\nsame\nold\n";
let diff = " same\n-old\n+new\n\n*** End of File";
assert_eq!(update(input, diff), "same\nold\nmiddle\nsame\nnew\n");
}
#[test]
fn end_of_file_marker_falls_back_to_an_earlier_match() {
let input = "same\nold\nmiddle\ntail\n";
let diff = " same\n-old\n+new\n*** End of File";
assert_eq!(update(input, diff), "same\nnew\nmiddle\ntail\n");
}
#[test]
fn invalid_end_of_file_context_has_a_distinct_error() {
let error = update_error("one\ntwo\n", " missing\n*** End of File");
assert_eq!(error.message(), "Invalid EOF Context 0:\nmissing");
}
#[test]
fn update_stops_at_file_and_patch_markers() {
for marker in [
"*** End Patch",
"*** Update File: other.txt",
"*** Delete File: other.txt",
"*** Add File: other.txt",
] {
let diff = format!("-old\n+new\n{marker}\nthis is ignored");
assert_eq!(update("old", &diff), "new");
}
}
#[test]
fn unknown_metadata_line_is_rejected() {
let error = update_error("old", "*** Move File: elsewhere\n-old\n+new");
assert_eq!(error.message(), "Invalid Line: *** Move File: elsewhere");
}
#[test]
fn a_bare_metadata_separator_is_an_empty_section() {
let error = update_error("old", "***");
assert_eq!(error.message(), "Nothing in this section - index=0 ***");
}
#[test]
fn an_unprefixed_content_line_is_rejected() {
let error = update_error("old", "old\n+new");
assert_eq!(error.message(), "Invalid Line: old");
}
#[test]
fn every_section_after_the_first_requires_an_anchor() {
let error = update_error("a\nb", "-a\n+A\n@@malformed\n-b\n+B");
assert_eq!(error.message(), "Invalid Line:\n@@malformed");
}
#[test]
fn unicode_content_and_anchors_are_supported() {
let input = "πρόλογος\n🦀 Rust\n終わり\n";
let diff = "@@ 🦀 Rust\n-終わり\n+完了";
assert_eq!(update(input, diff), "πρόλογος\n🦀 Rust\n完了\n");
}
#[test]
fn mode_default_selects_update_behavior() {
assert_eq!(ApplyDiffMode::default(), ApplyDiffMode::Update);
assert_eq!(
apply_diff_to_text_with_mode("old", "-old\n+new", ApplyDiffMode::default()).unwrap(),
"new"
);
}
#[test]
fn error_implements_the_standard_error_contract() {
let error = update_error("old", "invalid");
let standard_error: &dyn Error = &error;
assert_eq!(standard_error.to_string(), "Invalid Line: invalid");
assert!(standard_error.source().is_none());
}
#[test]
fn replacements_insertions_and_deletions_work_at_every_position() {
for line_count in 1..=16 {
let lines: Vec<String> = (0..line_count)
.map(|index| format!("line-{index}"))
.collect();
for target in 0..line_count {
let input = lines.join("\n");
let old = &lines[target];
let replacement = format!("changed-{target}");
let replaced = update(&input, &format!("-{old}\n+{replacement}"));
let mut expected = lines.clone();
expected[target] = replacement.clone();
assert_eq!(replaced, expected.join("\n"));
let deleted = update(&input, &format!("-{old}"));
let mut expected = lines.clone();
expected.remove(target);
assert_eq!(deleted, expected.join("\n"));
let inserted = update(&input, &format!(" {old}\n+inserted-{target}"));
let mut expected = lines.clone();
expected.insert(target + 1, format!("inserted-{target}"));
assert_eq!(inserted, expected.join("\n"));
}
}
}