use std::collections::HashMap;
use super::{
DroppedFile, FileDisposition, FilteredDiff, FilteredFile, FilteredHunk, HunkDropReason,
};
pub(super) fn make_kept_file(name: &str, hunk_content: &str) -> FilteredFile {
FilteredFile {
filename: name.to_string(),
status: "modified".to_string(),
disposition: FileDisposition::Kept,
hunks: vec![FilteredHunk {
header: "@@ -1,3 +1,3 @@".to_string(),
lines: vec![hunk_content.to_string()],
substantive_confidence: 1.0,
reason_kept: "deterministic-pass".to_string(),
}],
dropped_hunks: vec![],
summary_line: None,
}
}
#[test]
fn filtered_hunk_render_roundtrip() {
let h = FilteredHunk {
header: "@@ -1,2 +1,2 @@".to_string(),
lines: vec!["-old line".to_string(), "+new line".to_string()],
substantive_confidence: 1.0,
reason_kept: "test".to_string(),
};
let rendered = h.render();
assert!(rendered.contains("@@ -1,2 +1,2 @@"));
assert!(rendered.contains("-old line"));
assert!(rendered.contains("+new line"));
}
#[test]
fn hunk_drop_reason_label() {
assert_eq!(HunkDropReason::WhitespaceOnly.label(), "whitespace-only");
assert_eq!(HunkDropReason::ImportOnly.label(), "import-only");
assert_eq!(HunkDropReason::CommentOnly.label(), "comment-only");
assert_eq!(
HunkDropReason::MechanicalHaiku.label(),
"mechanical (Haiku)"
);
}
#[test]
fn filtered_diff_render_for_prompt_contains_surviving_content() {
let diff = FilteredDiff {
files: vec![make_kept_file("src/auth.rs", "+pub fn authenticate() {}")],
dropped_files: vec![],
drop_hunk_counts: HashMap::new(),
original_byte_size: 500,
filtered_byte_size: 100,
};
let rendered = diff.render_for_prompt(10_000);
assert!(rendered.contains("src/auth.rs"), "file path must appear");
assert!(
rendered.contains("authenticate"),
"hunk content must appear"
);
}
#[test]
fn filtered_diff_render_respects_max_chars() {
let files: Vec<FilteredFile> = (0..100)
.map(|i| make_kept_file(&format!("src/file{i}.rs"), &"+fn foo() {}".repeat(50)))
.collect();
let diff = FilteredDiff {
files,
dropped_files: vec![],
drop_hunk_counts: HashMap::new(),
original_byte_size: 100_000,
filtered_byte_size: 50_000,
};
let rendered = diff.render_for_prompt(2_000);
assert!(
rendered.len() <= 2_000 + 400,
"rendered output must not greatly exceed max_chars: len={}",
rendered.len()
);
}
#[test]
fn filtered_diff_drop_summary_emitted() {
let mut drop_counts = HashMap::new();
drop_counts.insert(HunkDropReason::ImportOnly, 3u32);
drop_counts.insert(HunkDropReason::WhitespaceOnly, 1u32);
let diff = FilteredDiff {
files: vec![make_kept_file("src/main.rs", "+fn main() {}")],
dropped_files: vec![DroppedFile {
path: "Cargo.lock".to_string(),
reason: "lockfile".to_string(),
}],
drop_hunk_counts: drop_counts,
original_byte_size: 5_000,
filtered_byte_size: 200,
};
let rendered = diff.render_for_prompt(100_000);
assert!(
rendered.contains("DiffAnalyzer filtered"),
"noise summary must appear: {rendered}"
);
assert!(
rendered.contains("file(s) omitted"),
"file drop count must appear: {rendered}"
);
assert!(
rendered.contains("hunk(s) omitted"),
"hunk drop count must appear: {rendered}"
);
}
#[test]
fn no_summary_when_nothing_dropped() {
let diff = FilteredDiff {
files: vec![make_kept_file("src/lib.rs", "+pub fn new() {}")],
dropped_files: vec![],
drop_hunk_counts: HashMap::new(),
original_byte_size: 100,
filtered_byte_size: 100,
};
let summary = diff.build_noise_summary();
assert!(summary.is_empty(), "empty summary when nothing was dropped");
}
#[test]
fn render_for_prompt_mid_file_hunk_overflow_loud_not_silent() {
let large_hunk_content = "+".to_string() + &"x".repeat(900);
let file1 = FilteredFile {
filename: "src/first.rs".to_string(),
status: "modified".to_string(),
disposition: FileDisposition::Kept,
hunks: vec![
FilteredHunk {
header: "@@ -1,1 +1,1 @@".to_string(),
lines: vec!["+fn first() {}".to_string()],
substantive_confidence: 1.0,
reason_kept: "test".to_string(),
},
FilteredHunk {
header: "@@ -10,1 +10,1 @@".to_string(),
lines: vec![large_hunk_content],
substantive_confidence: 1.0,
reason_kept: "test".to_string(),
},
],
dropped_hunks: vec![],
summary_line: None,
};
let file2 = make_kept_file("src/second.rs", "+fn second() {}");
let diff = FilteredDiff {
files: vec![file1, file2],
dropped_files: vec![],
drop_hunk_counts: HashMap::new(),
original_byte_size: 2_000,
filtered_byte_size: 1_000,
};
let rendered = diff.render_for_prompt(200);
assert!(
rendered.contains("RENDER TRUNCATED"),
"truncation marker must appear when budget is hit: {rendered}"
);
assert!(
!rendered.contains("src/second.rs"),
"second file must not appear after mid-file budget overflow: {rendered}"
);
assert!(
rendered.len() <= 200 + 400, "output must not greatly exceed max_chars: len={}",
rendered.len()
);
}
#[test]
fn render_for_prompt_no_truncation_marker_when_fits() {
let diff = FilteredDiff {
files: vec![make_kept_file("src/lib.rs", "+pub fn new() {}")],
dropped_files: vec![],
drop_hunk_counts: HashMap::new(),
original_byte_size: 100,
filtered_byte_size: 100,
};
let rendered = diff.render_for_prompt(100_000);
assert!(
!rendered.contains("RENDER TRUNCATED"),
"no truncation marker when content fits: {rendered}"
);
}
#[test]
fn render_for_prompt_marker_does_not_cause_large_overflow() {
let files: Vec<FilteredFile> = (0..50)
.map(|i| make_kept_file(&format!("src/file{i}.rs"), &"+fn foo() {}".repeat(20)))
.collect();
let diff = FilteredDiff {
files,
dropped_files: vec![],
drop_hunk_counts: HashMap::new(),
original_byte_size: 50_000,
filtered_byte_size: 25_000,
};
let max_chars: usize = 500;
let rendered = diff.render_for_prompt(max_chars);
assert!(
rendered.len() <= max_chars + 400,
"rendered len {} must not greatly exceed max_chars {max_chars}",
rendered.len()
);
assert!(
rendered.contains("RENDER TRUNCATED"),
"truncation marker must appear: {rendered}"
);
}