use std::vec; use super::*; fn deltas(diff: &AIRequestedCodeDiff) -> &[DiffDelta] { match &diff.diff_type { DiffType::Update { deltas, .. } => deltas, other => panic!("Expected Update diff_type, got {other:?}"), } } const CONTENT: &str = "I'd just like to interject for a moment. What you're refering to as Linux, is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but rather another free component of a fully functioning GNU system made useful by the GNU corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX."; #[test] fn test_simple() { let input_diffs = vec![ SearchAndReplace { search: "2|hey".to_string(), replace: "what".to_string(), }, SearchAndReplace { search: "4|world\n5|of".to_string(), replace: "hey".to_string(), }, ]; let diff = fuzzy_match_diffs("test.rs", &input_diffs, "what\nhey\nthere\nworld\nof\n"); assert_eq!(diff.file_name, "test.rs"); assert_eq!( deltas(&diff), &[ DiffDelta { replacement_line_range: 2..3, insertion: "what".to_string(), }, DiffDelta { replacement_line_range: 4..6, insertion: "hey".to_string(), } ] ); } #[test] fn test_incorrect_line_numbers() { let input_diffs = vec![SearchAndReplace { search: "4|world\n5|of".to_string(), replace: "hey".to_string(), }]; let diff = fuzzy_match_diffs("test.rs", &input_diffs, "what\nthere\nworld\nof"); assert_eq!(diff.file_name, "test.rs"); assert_eq!( deltas(&diff), &[DiffDelta { replacement_line_range: 3..5, insertion: "hey".to_string(), }] ); } #[test] fn test_missing_line_numbers() { let input_diffs = vec![SearchAndReplace { search: "hey\nthere".to_string(), replace: "world".to_string(), }]; let diff = fuzzy_match_diffs("test.rs", &input_diffs, "what\nhey\nthere\nworld\nof\n"); assert_eq!(diff.file_name, "test.rs"); assert_eq!( deltas(&diff), &[DiffDelta { replacement_line_range: 2..4, insertion: "world".to_string(), }] ); let failures = diff.failures.expect("Expected failures to be tracked"); assert_eq!(failures.missing_line_numbers, 1); assert_eq!(failures.fuzzy_match_failures, 0); assert_eq!(failures.noop_deltas, 0); } #[test] fn test_blank_search() { let input_diffs = vec![SearchAndReplace { search: "".to_string(), replace: "hey".to_string(), }]; let diff = fuzzy_match_diffs("test.rs", &input_diffs, "what\nhey\nthere\nworld\nof\n"); assert_eq!(diff.file_name, "test.rs"); assert_eq!( deltas(&diff), &[DiffDelta { replacement_line_range: 0..0, insertion: "hey".to_string(), }] ); } #[test] fn test_closest() { let input_diffs = vec![SearchAndReplace { search: "4|world\n5|of".to_string(), replace: "hey".to_string(), }]; let diff = fuzzy_match_diffs( "test.rs", &input_diffs, "what\nhey\nworld\nof\nthe\nworld\nof\n", ); assert_eq!(diff.file_name, "test.rs"); assert_eq!( deltas(&diff), &[DiffDelta { replacement_line_range: 3..5, insertion: "hey".to_string(), }] ); } #[test] fn test_line_numbers_off_by_one() { let insertion = " Linux, is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but rather another free component of a fully functioning GNU system made useful by the GNU corelibs, hello, world!" .to_string(); let input_diffs = vec![SearchAndReplace { search: "2| Linux, is in fact, GNU/Linux, or as I've\n\ 3| recently taken to calling it, GNU plus\n\ 4| Linux. Linux is not an operating system\n\ 5| unto itself, but rather another free\n\ 6| component of a fully functioning GNU\n\ 7| system made useful by the GNU corelibs," .to_string(), replace: insertion.clone(), }]; let diff = fuzzy_match_diffs("test.rs", &input_diffs, CONTENT); assert_eq!( deltas(&diff), &[DiffDelta { replacement_line_range: 3..9, insertion, }] ); } #[test] fn test_append_to_end_of_file() { let input_diffs = vec![SearchAndReplace { search: "3|".to_string(), replace: "foo".to_string(), }]; let diff = fuzzy_match_diffs("test.rs", &input_diffs, "\n\n\n"); assert_eq!( deltas(&diff), &[DiffDelta { replacement_line_range: 3..4, insertion: "foo".to_string(), }] ) } #[test] fn test_totally_unrelated_search() { let input_diffs = vec![SearchAndReplace { search: "4|foo bar baz".to_string(), replace: "hello, world!".to_string(), }]; let diff = fuzzy_match_diffs("test.rs", &input_diffs, CONTENT); assert!(deltas(&diff).is_empty()); assert!(diff.failures.is_some()); } /// The agent sometimes emits a search whose final line is a prefix of the actual file line. /// Before `PrefixTailMatch`, the Jaro-Winkler scorer landed just under the 0.9 threshold for /// long lines and the diff failed with `Could not apply all diffs to `. With /// `PrefixTailMatch` in the cascade, the rescue succeeds and the existing suffix-preservation /// fixup splices the unmatched tail into the insertion. #[test] fn test_prefix_tail_rescue_with_line_number_hint() { let actual_line = "if the stripping tool encounters any error (nesting, unmatched markers, UTF-8 decode failure), the sync workflow **fails** and does **not** update the watermark. the next run will retry from the same commit. this is correct fail-closed behavior \u{2014} a stripping error might indicate a condition that could cause private code to leak."; let file_content = format!("(preamble)\n\n### error handling\n\n{actual_line}\n\n(trailer)\n"); // Search is a prefix of line 5, with the `5|` line-number hint. let search = "5|if the stripping tool encounters any error (nesting, unmatched markers, UTF-8 decode failure), the sync workflow **fails** and does **not** update the watermark."; let replace = "if the stripping tool encounters any error (nesting, unmatched markers, UTF-8 decode failure, symlinks), the sync workflow **fails** and does **not** update the watermark."; let input_diffs = vec![SearchAndReplace { search: search.to_string(), replace: replace.to_string(), }]; let diff = fuzzy_match_diffs("TECH-DESIGN.md", &input_diffs, &file_content); // The rescue should produce a single delta replacing line 5 with the replacement // plus the unmatched suffix of the original line appended by the existing fixup. let unmatched_suffix = &actual_line[search.strip_prefix("5|").unwrap().len()..]; let expected_insertion = format!("{replace}{unmatched_suffix}"); assert_eq!( deltas(&diff), &[DiffDelta { replacement_line_range: 5..6, insertion: expected_insertion, }] ); // The rescue succeeds cleanly — no failure signals should be surfaced. assert!(diff.failures.is_none()); assert!(!diff.warrants_failure()); } #[test] fn test_parse_line_numbers() { let search = "1|hey\n2|there\n3|world"; let (line_range, line) = parse_line_numbers(search); assert_eq!(line_range, Some(1..4)); assert_eq!(line, "hey\nthere\nworld"); let search = "hey\nthere"; let (line_range, line) = parse_line_numbers(search); assert_eq!(line_range, None); assert_eq!(line, "hey\nthere"); let search = ""; let (line_range, line) = parse_line_numbers(search); assert_eq!(line_range, Some(0..0)); assert_eq!(line, ""); } #[test] fn test_remove_extra_line_num_prefix() { // Test with line numbers. let input = "1|first line\n2|second line\n3|third line".to_string(); assert_eq!( remove_extra_line_num_prefix(input), "first line\nsecond line\nthird line" ); // Test with no line numbers. let input = "first line\nsecond line".to_string(); assert_eq!( remove_extra_line_num_prefix(input), "first line\nsecond line" ); // Test empty string. assert_eq!(remove_extra_line_num_prefix("".to_string()), ""); // Test single line with number. assert_eq!( remove_extra_line_num_prefix("1|only line".to_string()), "only line" ); // Test with line numbers with mixed prefixes. let input = "first line\n2|second line\n3|third line".to_string(); assert_eq!( remove_extra_line_num_prefix(input), "first line\nsecond line\nthird line" ); // Test single line without number. let input = "no number line".to_string(); assert_eq!(remove_extra_line_num_prefix(input.clone()), input); } #[test] fn test_find_similar_sections_out_of_bounds() { let matches = find_similar_sections("hey\nthere\nyou", &[], 0.9); assert!(matches.is_empty()); let matches = find_similar_sections("hey\nthere\nyou", &["hey", "there", "you"], 0.9); assert_eq!( matches, vec![Match { start_line: 1, end_line: 4, similarity: 1.0 }] ); let matches = find_similar_sections("hey\nthere\nyou", &["hey", "there"], 0.9); assert!(matches.is_empty()); let matches = find_similar_sections("", &[], 0.9); assert!(matches.is_empty()); } #[test] fn test_v4a_exact_match() { let hunks = vec![V4AHunk { change_context: vec![], pre_context: "fn main() {".to_string(), old: " println!(\"Hello\");".to_string(), new: " println!(\"Hello, World!\");".to_string(), post_context: "}".to_string(), }]; let file_content = "fn main() {\n println!(\"Hello\");\n}"; let diff = fuzzy_match_v4a_diffs("test.rs", &hunks, None, file_content); assert_eq!(diff.file_name, "test.rs"); assert_eq!(deltas(&diff).len(), 1); assert_eq!( deltas(&diff)[0], DiffDelta { replacement_line_range: 2..3, insertion: " println!(\"Hello, World!\");".to_string(), } ); } #[test] fn test_v4a_with_change_context() { let hunks = vec![V4AHunk { change_context: vec!["impl MyStruct {".to_string()], pre_context: " fn method1() {\n // comment".to_string(), old: " let x = 1;".to_string(), new: " let x = 2;".to_string(), post_context: " }\n}".to_string(), }]; let file_content = "struct MyStruct {}\n\nimpl MyStruct {\n fn method1() {\n // comment\n let x = 1;\n }\n}"; let diff = fuzzy_match_v4a_diffs("test.rs", &hunks, None, file_content); assert_eq!(deltas(&diff).len(), 1); assert_eq!( deltas(&diff)[0], DiffDelta { replacement_line_range: 6..7, insertion: " let x = 2;".to_string(), } ); } #[test] fn test_v4a_indentation_agnostic_match() { // Hunk has different indentation than the actual file let hunks = vec![V4AHunk { change_context: vec![], pre_context: "def hello():".to_string(), old: "print(\"hello\")".to_string(), // No indentation new: " print(\"hello world\")".to_string(), post_context: "".to_string(), }]; let file_content = "def hello():\n print(\"hello\")"; // Has indentation let diff = fuzzy_match_v4a_diffs("test.py", &hunks, None, file_content); assert_eq!(deltas(&diff).len(), 1); assert_eq!( deltas(&diff)[0], DiffDelta { replacement_line_range: 2..3, insertion: " print(\"hello world\")".to_string(), } ); } #[test] fn test_v4a_fuzzy_match() { // Hunk has slightly different content (typo) let hunks = vec![V4AHunk { change_context: vec![], pre_context: "function greet() {".to_string(), old: " console.log(\"helo\");".to_string(), // Typo: "helo" instead of "hello" new: " console.log(\"hello world\");".to_string(), post_context: "}".to_string(), }]; let file_content = "function greet() {\n console.log(\"hello\");\n}"; // Correct spelling let diff = fuzzy_match_v4a_diffs("test.js", &hunks, None, file_content); // Should match due to high similarity (> 0.9) assert_eq!(deltas(&diff).len(), 1); assert_eq!(deltas(&diff)[0].replacement_line_range, 2..3); } #[test] fn test_v4a_no_match() { let hunks = vec![V4AHunk { change_context: vec![], pre_context: "fn does_not_exist() {".to_string(), old: " unrelated_code();".to_string(), new: " new_code();".to_string(), post_context: "}".to_string(), }]; let file_content = "fn main() {\n println!(\"Hello\");\n}"; let diff = fuzzy_match_v4a_diffs("test.rs", &hunks, None, file_content); assert!(deltas(&diff).is_empty()); assert!(diff.failures.is_some()); let failures = diff.failures.unwrap(); assert_eq!(failures.fuzzy_match_failures, 1); } #[test] fn test_v4a_noop_diff() { let hunks = vec![V4AHunk { change_context: vec![], pre_context: "fn main() {".to_string(), old: " println!(\"Hello\");".to_string(), new: " println!(\"Hello\");".to_string(), // Same as old post_context: "}".to_string(), }]; let file_content = "fn main() {\n println!(\"Hello\");\n}"; let diff = fuzzy_match_v4a_diffs("test.rs", &hunks, None, file_content); assert!(deltas(&diff).is_empty()); assert!(diff.failures.is_some()); let failures = diff.failures.unwrap(); assert_eq!(failures.noop_deltas, 1); } #[test] fn test_v4a_empty_context() { // Test with no pre or post context let hunks = vec![V4AHunk { change_context: vec![], pre_context: String::new(), old: "let x = 1;".to_string(), new: "let x = 2;".to_string(), post_context: String::new(), }]; let file_content = "let x = 1;"; let diff = fuzzy_match_v4a_diffs("test.rs", &hunks, None, file_content); assert_eq!(deltas(&diff).len(), 1); assert_eq!( deltas(&diff)[0], DiffDelta { replacement_line_range: 1..2, insertion: "let x = 2;".to_string(), } ); } #[test] fn test_v4a_multiline_old_content() { let hunks = vec![V4AHunk { change_context: vec![], pre_context: "fn calculate() {".to_string(), old: " let a = 1;\n let b = 2;\n let sum = a + b;".to_string(), new: " let sum = 3;".to_string(), post_context: " println!(\"{}\", sum);\n}".to_string(), }]; let file_content = "fn calculate() {\n let a = 1;\n let b = 2;\n let sum = a + b;\n println!(\"{}\", sum);\n}"; let diff = fuzzy_match_v4a_diffs("test.rs", &hunks, None, file_content); assert_eq!(deltas(&diff).len(), 1); assert_eq!( deltas(&diff)[0], DiffDelta { replacement_line_range: 2..5, insertion: " let sum = 3;".to_string(), } ); } #[test] fn test_v4a_multiple_hunks() { let hunks = vec![ V4AHunk { change_context: vec![], pre_context: "fn first() {".to_string(), old: " let x = 1;".to_string(), new: " let x = 10;".to_string(), post_context: "}".to_string(), }, V4AHunk { change_context: vec![], pre_context: "fn second() {".to_string(), old: " let y = 2;".to_string(), new: " let y = 20;".to_string(), post_context: "}".to_string(), }, ]; let file_content = "fn first() {\n let x = 1;\n}\n\nfn second() {\n let y = 2;\n}"; let diff = fuzzy_match_v4a_diffs("test.rs", &hunks, None, file_content); assert_eq!(deltas(&diff).len(), 2); assert_eq!(deltas(&diff)[0].replacement_line_range, 2..3); assert_eq!(deltas(&diff)[0].insertion, " let x = 10;"); assert_eq!(deltas(&diff)[1].replacement_line_range, 6..7); assert_eq!(deltas(&diff)[1].insertion, " let y = 20;"); } #[test] fn test_v4a_add_line_with_change_context_no_old() { // Test adding a new line using only change_context to locate position, without old content or pre-context let hunks = vec![V4AHunk { change_context: vec!["class MyClass {".to_string()], pre_context: "".to_string(), old: "".to_string(), new: " fn new_method() {\n return 2;\n }".to_string(), post_context: " fn existing_method() {".to_string(), }]; let file_content = "class MyClass {\n fn existing_method() {\n return 1;\n }\n}"; let diff = fuzzy_match_v4a_diffs("test.rs", &hunks, None, file_content); assert_eq!(deltas(&diff).len(), 1); // The insertion should happen after the change_context line (line 1) assert_eq!(deltas(&diff)[0].replacement_line_range, 2..2); assert_eq!( deltas(&diff)[0].insertion, " fn new_method() {\n return 2;\n }" ); } #[test] fn test_v4a_add_line_at_start_of_file() { // Test adding a line at the very start of a file let hunks = vec![V4AHunk { change_context: vec![], pre_context: "".to_string(), // No pre-context - start of file old: "".to_string(), // No old content new: "// New header comment".to_string(), post_context: "fn main() {".to_string(), }]; let file_content = "fn main() {\n println!(\"Hello\");\n}"; let diff = fuzzy_match_v4a_diffs("test.rs", &hunks, None, file_content); assert_eq!(deltas(&diff).len(), 1); // Should insert at the beginning (line range 1..1 means before line 1) assert_eq!(deltas(&diff)[0].replacement_line_range, 1..1); assert_eq!(deltas(&diff)[0].insertion, "// New header comment"); } #[test] fn test_v4a_add_line_at_end_of_file() { // Test adding a line at the very end of a file let hunks = vec![V4AHunk { change_context: vec![], pre_context: "fn main() {\n println!(\"Hello\");\n}".to_string(), old: "".to_string(), // No old content new: "\n// Footer comment".to_string(), post_context: "".to_string(), // No post-context - end of file }]; let file_content = "fn main() {\n println!(\"Hello\");\n}"; let diff = fuzzy_match_v4a_diffs("test.rs", &hunks, None, file_content); assert_eq!(deltas(&diff).len(), 1); // Should insert after the last line (line 3), so insertion point is 4..4 assert_eq!(deltas(&diff)[0].replacement_line_range, 4..4); assert_eq!(deltas(&diff)[0].insertion, "\n// Footer comment"); } #[test] fn test_partial_last_line_in_search_preserves_suffix() { // When a search string ends with a partial line (e.g. "let x = 1;\nlet x" where // "let x" is only a prefix of the actual file line "let x = 2;"), the Jaro-Winkler // fuzzy matcher matches via whole-line windows. The unmatched suffix (" = 2;") from // the file's last matched line must be preserved in the insertion. let file_content = "func foo() {\nlet x = 1;\nlet x = 2;\n}"; let diffs = [SearchAndReplace { search: "let x = 1;\nlet x".to_string(), replace: "let y = 1;\nlet x".to_string(), }]; let (deltas, _failures) = fuzzy_match_file_diffs(&diffs, file_content); assert_eq!(deltas.len(), 1, "Expected one matched delta"); assert_eq!(deltas[0].replacement_line_range, 2..4); // The insertion has the unmatched suffix " = 2;" appended to the last line. assert_eq!(deltas[0].insertion, "let y = 1;\nlet x = 2;"); // Verify applying the delta produces correct output (no data loss). let file_lines: Vec<&str> = file_content.lines().collect(); let range = &deltas[0].replacement_line_range; let mut result = String::new(); for line in &file_lines[..range.start - 1] { result.push_str(line); result.push('\n'); } result.push_str(&deltas[0].insertion); result.push('\n'); for line in &file_lines[range.end - 1..] { result.push_str(line); result.push('\n'); } assert_eq!(result, "func foo() {\nlet y = 1;\nlet x = 2;\n}\n"); } #[test] fn test_search_and_replace_accommodates_none() { let parsed_diff = ParsedDiff::StrReplaceEdit { file: None, search: None, replace: None, }; let search_and_replace: Result = parsed_diff.try_into(); assert_eq!(Err(()), search_and_replace); let parsed_diff = ParsedDiff::StrReplaceEdit { file: None, search: Some("search".into()), replace: None, }; assert_eq!( Ok(SearchAndReplace { search: "search".into(), replace: String::new() }), parsed_diff.try_into() ); let parsed_diff = ParsedDiff::StrReplaceEdit { file: None, search: None, replace: Some("replace".into()), }; assert_eq!( Ok(SearchAndReplace { search: String::new(), replace: "replace".into() }), parsed_diff.try_into() ); } /// Test that if a search/replace pair is not a noop, but the overall effect is a noop when applied /// to the file contents, we skip the diff. #[test] fn test_replace_matches_file_content() { let diffs = [SearchAndReplace { search: "1|Hey, there".to_string(), replace: "Hi, there".to_string(), }]; let (deltas, errors) = fuzzy_match_file_diffs(&diffs, "Hi, there\nGoodbye, world"); assert!(deltas.is_empty()); assert_eq!(errors.noop_deltas, 1); } #[test] fn test_search_range_greater_than_file_length() { // This should not panic! let r = match_diff( "hey\nthere", Some(14..15), &["hey", "there"], 1f64, MakeExactMatch, ); assert_eq!(r, Some(1..3)); } #[test] fn test_custom_lines() { assert_eq!(lines("").collect_vec(), vec![""]); assert_eq!(lines("foobar").collect_vec(), vec!["foobar"]); assert_eq!(lines("foo\nbar").collect_vec(), vec!["foo", "bar"]); assert_eq!(lines("foo\nbar\n").collect_vec(), vec!["foo", "bar"]); }