use objects::object::{ChangeImportance, ModificationKind, SemanticChange};
use super::*;
fn nested_rust_modules(depth: usize, inner: &str) -> String {
let mut source = String::new();
for level in 0..depth {
source.push_str(&format!("mod layer_{level} {{\n"));
}
source.push_str(inner);
source.push('\n');
for _ in 0..depth {
source.push_str("}\n");
}
source
}
#[test]
fn test_classify_rust_formatting_only() {
let old = "fn foo() {\nbar();\nbaz();\n}\n";
let new = "fn foo() {\n bar();\n baz();\n}\n";
let (kind, imp) = classify_modification(std::path::Path::new("test.rs"), old, new);
assert_eq!(kind, ModificationKind::FormattingOnly);
assert_eq!(imp, ChangeImportance::Noise);
}
#[test]
fn test_classify_javascript_formatting() {
let old = "function greet(name) {\nreturn 'hello ' + name;\n}\n";
let new = "function greet(name) {\n return 'hello ' + name;\n}\n";
let (kind, imp) = classify_modification(std::path::Path::new("test.js"), old, new);
assert_eq!(kind, ModificationKind::FormattingOnly);
assert_eq!(imp, ChangeImportance::Noise);
}
#[test]
fn test_classify_logic_change_rust() {
let old = "fn compute() -> i32 {\n 42\n}\n";
let new = "fn compute() -> i32 {\n 43\n}\n";
let (kind, imp) = classify_modification(std::path::Path::new("test.rs"), old, new);
assert_eq!(kind, ModificationKind::Logic);
assert_eq!(imp, ChangeImportance::High);
}
#[test]
fn test_classify_comments_only_rust() {
let old = "// Computes the answer.\nfn answer() -> i32 { 42 }\n";
let new = "// Returns the final answer.\nfn answer() -> i32 { 42 }\n";
let (kind, imp) = classify_modification(std::path::Path::new("test.rs"), old, new);
assert_eq!(kind, ModificationKind::CommentsOnly);
assert_eq!(imp, ChangeImportance::Low);
}
#[test]
fn test_classify_imports_only_rust() {
let old = "use std::io;\n\nfn work() { do_stuff(); }\n";
let new = "use std::io;\nuse std::fs;\n\nfn work() { do_stuff(); }\n";
let (kind, imp) = classify_modification(std::path::Path::new("test.rs"), old, new);
assert_eq!(kind, ModificationKind::ImportsOnly);
assert_eq!(imp, ChangeImportance::Low);
}
#[test]
fn test_classify_mixed_change() {
let old = "fn foo() {\n let x = 1;\n let y = 2;\n let z = 3;\n let w = 4;\n let v = 5;\n let u = 6;\n let t = 7;\n let s = 8;\n let r = 9;\n x + y\n}\n";
let new = "fn foo() {\n let x = 1;\n let y = 2;\n let z = 3;\n let w = 4;\n let v = 5;\n let u = 6;\n let t = 7;\n let s = 8;\n let r = 9;\n x + y + 1\n}\n";
let (kind, _imp) = classify_modification(std::path::Path::new("test.rs"), old, new);
assert!(
kind == ModificationKind::Mixed || kind == ModificationKind::Logic,
"Expected Mixed or Logic, got {:?}",
kind
);
}
#[test]
fn test_classify_empty_to_content() {
let old = "";
let new = "fn new_stuff() { 42 }\n";
let (kind, imp) = classify_modification(std::path::Path::new("test.rs"), old, new);
assert_eq!(kind, ModificationKind::Logic);
assert_eq!(imp, ChangeImportance::High);
}
#[test]
fn test_classify_python_formatting() {
let old = "def foo():\n return 42\n";
let new = "def foo():\n return 42\n";
let (kind, imp) = classify_modification(std::path::Path::new("test.py"), old, new);
assert_eq!(kind, ModificationKind::FormattingOnly);
assert_eq!(imp, ChangeImportance::Noise);
}
#[test]
fn test_classify_go_logic_change() {
let old = "func main() {\n\tfmt.Println(\"hello\")\n}\n";
let new = "func main() {\n\tfmt.Println(\"goodbye\")\n}\n";
let (kind, imp) = classify_modification(std::path::Path::new("test.go"), old, new);
assert_eq!(kind, ModificationKind::Logic);
assert_eq!(imp, ChangeImportance::High);
}
#[test]
fn test_classify_unknown_language_formatting() {
let old = "key = \"value\"\n";
let new = "key = \"value\"\n";
let (kind, imp) = classify_modification(std::path::Path::new("config.toml"), old, new);
assert_eq!(kind, ModificationKind::FormattingOnly);
assert_eq!(imp, ChangeImportance::Noise);
}
#[test]
fn test_classify_identical_content() {
let content = "fn foo() { 42 }\n";
let (kind, imp) = classify_modification(std::path::Path::new("test.rs"), content, content);
assert_eq!(kind, ModificationKind::WhitespaceOnly);
assert_eq!(imp, ChangeImportance::Noise);
}
#[test]
fn test_detect_function_deletion() {
let old = "fn kept() { 1 }\nfn removed() { 2 }\n";
let new = "fn kept() { 1 }\n";
let changes = detect_function_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
old,
new,
SimilarityMethod::Lines,
);
let has_deleted = changes
.iter()
.any(|c| matches!(c, SemanticChange::FunctionDeleted { name, .. } if name == "removed"));
assert!(
has_deleted,
"Expected FunctionDeleted for 'removed', got: {:?}",
changes
);
}
#[test]
fn test_detect_function_deletion_not_triggered_for_rename() {
let old = r#"fn old_name(input: &str) -> String {
let trimmed = input.trim();
let uppercased = trimmed.to_uppercase();
let result = format!("Processed: {}", uppercased);
println!("Processing: {}", input);
println!("Result: {}", result);
result
}"#;
let new = r#"fn new_name(input: &str) -> String {
let trimmed = input.trim();
let uppercased = trimmed.to_uppercase();
let result = format!("Processed: {}", uppercased);
println!("Processing: {}", input);
println!("Result: {}", result);
result
}"#;
let changes = detect_function_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
old,
new,
SimilarityMethod::Lines,
);
let has_rename = changes
.iter()
.any(|c| matches!(c, SemanticChange::FunctionRenamed { .. }));
let has_deleted = changes
.iter()
.any(|c| matches!(c, SemanticChange::FunctionDeleted { .. }));
assert!(has_rename, "Expected FunctionRenamed: {:?}", changes);
assert!(
!has_deleted,
"Renamed function should not appear as deleted: {:?}",
changes
);
}
#[test]
fn test_detect_function_rename_with_body_update_not_delete_extract() {
let old = r#"#[test]
fn test_snapshot_uses_default_confidence_for_agent() {
let temp_dir = TempDir::new().unwrap();
Repository::init_default(temp_dir.path()).unwrap();
fs::write(temp_dir.path().join("agent.txt"), "content").unwrap();
let state = repo.snapshot(None, None).unwrap();
assert_eq!(
state.confidence,
Some(repo.repo_config().defaults.confidence)
);
}"#;
let new = r#"#[test]
fn test_snapshot_without_confidence_records_none() {
let temp_dir = TempDir::new().unwrap();
Repository::init_default(temp_dir.path()).unwrap();
fs::write(temp_dir.path().join("agent.txt"), "content").unwrap();
let state = repo.snapshot(None, None).unwrap();
assert_eq!(state.confidence, None);
}"#;
let changes = detect_function_changes(
std::path::Path::new("repository_tests.rs"),
std::path::Path::new("repository_tests.rs"),
old,
new,
SimilarityMethod::Lines,
);
assert!(
changes.iter().any(|change| {
matches!(
change,
SemanticChange::FunctionRenamed { old_name, new_name, .. }
if old_name == "test_snapshot_uses_default_confidence_for_agent"
&& new_name == "test_snapshot_without_confidence_records_none"
)
}),
"expected rename for similar test update, got: {changes:?}"
);
assert!(
!changes
.iter()
.any(|change| matches!(change, SemanticChange::FunctionDeleted { .. })),
"rename should not also report deletion: {changes:?}"
);
assert!(
!changes.iter().any(|change| matches!(
change,
SemanticChange::FunctionAdded { .. } | SemanticChange::FunctionExtracted { .. }
)),
"rename should not also report add/extract: {changes:?}"
);
}
#[test]
fn test_detect_signature_change() {
let old = "fn process(x: i32) -> i32 { x + 1 }\n";
let new = "fn process(x: i32, y: i32) -> i32 { x + y }\n";
let changes = detect_function_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
old,
new,
SimilarityMethod::Lines,
);
let has_sig_change = changes
.iter()
.any(|c| matches!(c, SemanticChange::SignatureChanged { name, .. } if name == "process"));
assert!(
has_sig_change,
"Expected SignatureChanged for 'process', got: {:?}",
changes
);
}
#[test]
fn test_detect_multiple_function_deletions() {
let old = "fn a() { 1 }\nfn b() { 2 }\nfn c() { 3 }\n";
let new = "fn a() { 1 }\n";
let changes = detect_function_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
old,
new,
SimilarityMethod::Lines,
);
let deleted_names: Vec<&str> = changes
.iter()
.filter_map(|c| match c {
SemanticChange::FunctionDeleted { name, .. } => Some(name.as_str()),
_ => None,
})
.collect();
assert!(
deleted_names.contains(&"b"),
"Expected 'b' in deleted: {:?}",
deleted_names
);
assert!(
deleted_names.contains(&"c"),
"Expected 'c' in deleted: {:?}",
deleted_names
);
}
#[test]
fn test_detect_signature_unchanged_body_changed() {
let old = "fn compute(x: i32) -> i32 { x + 1 }\n";
let new = "fn compute(x: i32) -> i32 { x * 2 }\n";
let changes = detect_function_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
old,
new,
SimilarityMethod::Lines,
);
let has_sig_change = changes
.iter()
.any(|c| matches!(c, SemanticChange::SignatureChanged { .. }));
assert!(
!has_sig_change,
"Body-only change should not be SignatureChanged: {:?}",
changes
);
assert!(
changes.iter().any(
|c| matches!(c, SemanticChange::FunctionModified { name, .. } if name == "compute")
),
"Body-only change should be FunctionModified: {:?}",
changes
);
}
#[test]
fn test_detect_added_dependency() {
let changes = detect_import_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
"",
"use serde::Deserialize;\n",
);
let has_added = changes
.iter()
.any(|c| matches!(c, SemanticChange::DependencyAdded { name, .. } if name == "serde"));
assert!(
has_added,
"Expected DependencyAdded for serde: {:?}",
changes
);
}
#[test]
fn test_detect_removed_dependency() {
let changes = detect_import_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
"use tokio::runtime;\n",
"",
);
let has_removed = changes
.iter()
.any(|c| matches!(c, SemanticChange::DependencyRemoved { name } if name == "tokio"));
assert!(
has_removed,
"Expected DependencyRemoved for tokio: {:?}",
changes
);
}
#[test]
fn test_detect_grouped_use_edit_does_not_report_dependency_churn() {
let changes = detect_import_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
"use objects::{\n object::{Blob, Tree},\n store::ObjectStore,\n};\n",
"use objects::{\n object::{Blob, SemanticChange, Tree},\n store::ObjectStore,\n};\n",
);
assert!(
changes.is_empty(),
"Editing members of an existing grouped use should not look like dependency add/remove: {changes:?}"
);
}
#[test]
fn test_stdlib_imports_ignored() {
let changes = detect_import_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
"use std::io::Read;\n",
"use std::io::Write;\nuse core::fmt;\n",
);
assert!(
changes.is_empty(),
"Stdlib changes should be ignored: {:?}",
changes
);
}
#[test]
fn test_similarity_empty_strings() {
assert_eq!(compute_similarity("", "", SimilarityMethod::Lines), 1.0);
assert_eq!(compute_similarity("", "", SimilarityMethod::Tokens), 1.0);
}
#[test]
fn test_similarity_one_empty() {
assert_eq!(
compute_similarity("content", "", SimilarityMethod::Lines),
0.0
);
assert_eq!(
compute_similarity("", "content", SimilarityMethod::Tokens),
0.0
);
}
#[test]
fn test_similarity_whitespace_only_difference() {
let a = "foo bar baz";
let b = "foo bar baz";
let token_sim = compute_similarity(a, b, SimilarityMethod::Tokens);
assert_eq!(
token_sim, 1.0,
"Whitespace-only diff should have token similarity 1.0"
);
}
#[test]
fn test_compute_similarity_lines() {
let a = "line1\nline2\nline3";
let b = "line1\nline2\nline3";
assert_eq!(compute_similarity(a, b, SimilarityMethod::Lines), 1.0);
let c = "line1\nline2\nline4";
let sim = compute_similarity(a, c, SimilarityMethod::Lines);
assert!(sim >= 0.5);
let d = "completely different";
let sim = compute_similarity(a, d, SimilarityMethod::Lines);
assert!(sim < 0.5);
}
#[test]
fn test_detect_function_changes_rename() {
let old = r#"fn old_function_name() {
println!("Hello");
let x = 42;
println!("{}", x);
}"#;
let new = r#"fn new_function_name() {
println!("Hello");
let x = 42;
println!("{}", x);
}"#;
let changes = detect_function_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
old,
new,
SimilarityMethod::Lines,
);
assert!(!changes.is_empty());
let has_rename_or_extract = changes.iter().any(|c| {
matches!(c, SemanticChange::FunctionRenamed { .. })
|| matches!(c, SemanticChange::FunctionAdded { .. })
|| matches!(c, SemanticChange::FunctionExtracted { .. })
});
assert!(has_rename_or_extract);
}
#[test]
fn test_detect_function_changes_prefers_stable_best_rename_candidate() {
let old = concat!(
"fn alpha() {\n",
" let value = 1;\n",
" let doubled = value * 2;\n",
" let message = format!(\"value={} doubled={}\", value, doubled);\n",
" println!(\"{}\", message);\n",
" println!(\"{}\", doubled);\n",
"}\n\n",
"fn beta() {\n",
" let value = 1;\n",
" let doubled = value * 2;\n",
" let message = format!(\"value={} doubled={}\", value, doubled);\n",
" println!(\"{}\", message);\n",
" println!(\"{}\", doubled);\n",
"}\n",
);
let new = concat!(
"fn gamma() {\n",
" let value = 1;\n",
" let doubled = value * 2;\n",
" let message = format!(\"value={} doubled={}\", value, doubled);\n",
" println!(\"{}\", message);\n",
" println!(\"{}\", doubled);\n",
"}\n",
);
let changes = detect_function_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
old,
new,
SimilarityMethod::Lines,
);
assert!(changes.iter().any(|change| matches!(
change,
SemanticChange::FunctionRenamed { old_name, new_name, .. }
if old_name == "alpha" && new_name == "gamma"
)));
assert!(changes.iter().any(|change| matches!(
change,
SemanticChange::FunctionDeleted { name, .. } if name == "beta"
)));
}
#[test]
fn test_detect_file_renames() {
let deleted = vec![(
std::path::PathBuf::from("old.rs"),
"fn main() {}".to_string(),
)];
let added = vec![(
std::path::PathBuf::from("new.rs"),
"fn main() {}".to_string(),
)];
let renames = detect_file_renames(&deleted, &added, 0.8, SimilarityMethod::Lines);
assert_eq!(renames.len(), 1);
assert_eq!(renames[0].0, std::path::PathBuf::from("old.rs"));
assert_eq!(renames[0].1, std::path::PathBuf::from("new.rs"));
}
#[test]
fn test_compute_similarity_ast_fallback() {
let a = "fn add(a: i32, b: i32) -> i32 { a + b }";
let b = "fn sum(x: i32, y: i32) -> i32 { x + y }";
let sim = super::analysis_similarity::compute_similarity_with_language(
a,
b,
SimilarityMethod::Ast,
crate::parser::Language::Rust,
);
assert!(sim > 0.6);
}
#[test]
fn test_classify_comments_only_handles_deep_ast_without_recursion() {
let old = nested_rust_modules(512, "// old\nfn deeply_nested() { println!(\"hi\"); }");
let new = nested_rust_modules(512, "// new\nfn deeply_nested() { println!(\"hi\"); }");
let (kind, importance) = classify_modification(std::path::Path::new("test.rs"), &old, &new);
assert_eq!(kind, ModificationKind::CommentsOnly);
assert_eq!(importance, ChangeImportance::Low);
}
#[test]
fn test_ast_similarity_handles_deep_ast_without_recursion() {
let source = nested_rust_modules(512, "fn deeply_nested() { println!(\"hi\"); }");
let similarity = super::analysis_similarity::compute_similarity_with_language(
&source,
&source,
SimilarityMethod::Ast,
crate::parser::Language::Rust,
);
assert_eq!(similarity, 1.0);
}
#[test]
fn test_extract_dependency_from_import() {
assert_eq!(
super::analysis_imports::detect_import_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
"use serde::Deserialize;",
"use std::collections::HashMap;"
)
.len(),
1
);
}
#[test]
fn test_is_stdlib_dependency() {
assert!(
super::analysis_imports::detect_import_changes(
std::path::Path::new("test.rs"),
std::path::Path::new("test.rs"),
"use std::collections::HashMap;",
"use std::io::Read;"
)
.is_empty()
);
}