use super::*;
use serde_json::json;
#[test]
fn grep_content_mode_paths_come_from_content_not_filenames() {
let response = json!({
"mode": "content",
"numFiles": 0,
"filenames": [],
"content": "src/search/hybrid.rs:107: Self::combine_with_recency(\nsrc/search/hybrid.rs:125: Self::combine_with_recency(a, b)\ndocs/plans/ppr-ranking-signal.md:52:| RRF fusion |",
"numLines": 3,
"totalLines": 3
});
let files = extract_files_from_tool_response("Grep", &response);
assert_eq!(
files,
vec!["src/search/hybrid.rs", "src/search/hybrid.rs"],
"content-mode paths must be parsed out of `content`"
);
}
#[test]
fn grep_files_with_matches_mode_uses_filenames() {
let response = json!({
"mode": "files_with_matches",
"numFiles": 2,
"filenames": ["src/cli/hook.rs", "src/search/hybrid.rs"],
"content": "",
});
assert_eq!(
extract_files_from_tool_response("Grep", &response),
vec!["src/cli/hook.rs", "src/search/hybrid.rs"]
);
}
#[test]
fn bash_stdout_yields_both_match_lines_and_bare_paths() {
let response = json!({
"stdout": "src/cli/hook.rs:4731: use hook_tool_response;\nsrc/breadcrumb.rs\n./src/config.rs\n",
"stderr": "",
"interrupted": false,
"isImage": false,
"noOutputExpected": false
});
assert_eq!(
extract_files_from_tool_response("Bash", &response),
vec!["src/cli/hook.rs", "src/breadcrumb.rs", "./src/config.rs"]
);
}
#[test]
fn bash_prose_is_not_mistaken_for_paths() {
let response = json!({
"stdout": "note: the function is defined here\nwarning: 3 warnings emitted\nCompiling bobbin-ai v0.14.0\nerror: could not compile\n",
"stderr": ""
});
assert!(
extract_files_from_tool_response("Bash", &response).is_empty(),
"prose with colons must not parse as `path:line:text`"
);
}
#[test]
fn a_colon_line_without_a_line_number_is_not_a_match_line() {
assert_eq!(
match_line_path("src/main.rs:42: let x = 1;"),
Some("src/main.rs".to_string())
);
assert_eq!(
match_line_path("src/main.rs:42"),
Some("src/main.rs".to_string())
);
assert_eq!(match_line_path("note: src/main.rs is stale"), None);
assert_eq!(match_line_path("src/main.rs:notanumber:x"), None);
assert_eq!(match_line_path("no colon here"), None);
}
#[test]
fn missing_tool_response_yields_nothing() {
assert!(extract_files_from_tool_response("Bash", &serde_json::Value::Null).is_empty());
assert!(extract_files_from_tool_response("Grep", &json!({})).is_empty());
assert!(extract_files_from_tool_response("Read", &json!({"filePath": "src/a.rs"})).is_empty());
}
#[test]
fn glob_shape_is_unmeasured_so_both_documented_forms_are_accepted() {
assert_eq!(
extract_files_from_tool_response("Glob", &json!({"filenames": ["src/a.rs"]})),
vec!["src/a.rs"]
);
assert_eq!(
extract_files_from_tool_response("Glob", &json!(["src/a.rs", "README.md"])),
vec!["src/a.rs"]
);
assert_eq!(
extract_files_from_tool_response("Glob", &json!("src/a.rs\nsrc/b.rs\n")),
vec!["src/a.rs", "src/b.rs"]
);
}
#[test]
fn stdout_scanning_is_bounded_and_utf8_safe() {
let s = "aé";
assert_eq!(truncate_on_char_boundary(s, 2), "a");
assert_eq!(truncate_on_char_boundary(s, 3), "aé");
assert_eq!(truncate_on_char_boundary("abc", 99), "abc");
let huge = format!("{}\nsrc/late.rs\n", "x".repeat(BASH_STDOUT_SCAN_LIMIT));
let files = extract_files_from_tool_response("Bash", &json!({"stdout": huge}));
assert!(
files.is_empty(),
"paths past the scan limit are not scanned"
);
}
#[test]
fn only_files_that_exist_inside_the_repo_survive() {
let repo = tempfile::tempdir().unwrap();
let root = repo.path();
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/real.rs"), "fn main() {}").unwrap();
let outside = tempfile::tempdir().unwrap();
std::fs::write(outside.path().join("elsewhere.rs"), "fn main() {}").unwrap();
let resolved = resolve_discovered_files(
vec![
"src/real.rs".to_string(), "./src/real.rs".to_string(), "src/imaginary.rs".to_string(), outside
.path()
.join("elsewhere.rs")
.to_string_lossy()
.to_string(), ],
root,
root,
);
assert_eq!(resolved, vec!["src/real.rs".to_string()]);
}
#[test]
fn discovered_files_are_capped() {
let repo = tempfile::tempdir().unwrap();
let root = repo.path();
std::fs::create_dir_all(root.join("src")).unwrap();
let mut raw = Vec::new();
for i in 0..(MAX_DISCOVERED_FILES + 3) {
let name = format!("src/f{}.rs", i);
std::fs::write(root.join(&name), "fn main() {}").unwrap();
raw.push(name);
}
assert_eq!(
resolve_discovered_files(raw, root, root).len(),
MAX_DISCOVERED_FILES
);
}
fn coupling(a: &str, b: &str, score: f32) -> crate::types::FileCoupling {
crate::types::FileCoupling {
file_a: a.to_string(),
file_b: b.to_string(),
score,
co_changes: 1,
last_co_change: 0,
}
}
#[test]
fn a_file_reached_from_two_seeds_appears_once_at_its_best_score() {
let seeds = vec!["src/a.rs".to_string(), "src/b.rs".to_string()];
let merged = merge_coupling_for_seeds(&seeds, |seed| match seed {
"src/a.rs" => vec![coupling("src/a.rs", "src/shared.rs", 0.4)],
"src/b.rs" => vec![coupling("src/b.rs", "src/shared.rs", 0.9)],
_ => vec![],
});
assert_eq!(merged, vec![("src/shared.rs".to_string(), 0.9)]);
}
#[test]
fn seeds_and_weak_couplings_are_dropped() {
let seeds = vec!["src/a.rs".to_string(), "src/b.rs".to_string()];
let merged = merge_coupling_for_seeds(&seeds, |seed| {
if seed == "src/a.rs" {
vec![
coupling("src/a.rs", "src/b.rs", 0.95),
coupling("src/a.rs", "src/weak.rs", 0.05),
coupling("src/a.rs", "src/good.rs", 0.5),
]
} else {
vec![]
}
});
assert_eq!(merged, vec![("src/good.rs".to_string(), 0.5)]);
}
#[test]
fn merged_coupling_is_ranked_and_capped() {
let seeds = vec!["src/a.rs".to_string()];
let merged = merge_coupling_for_seeds(&seeds, |_| {
(0..MAX_COUPLED_FILES + 4)
.map(|i| {
coupling(
"src/a.rs",
&format!("src/c{}.rs", i),
0.1 + i as f32 / 100.0,
)
})
.collect()
});
assert_eq!(merged.len(), MAX_COUPLED_FILES);
assert!(
merged.windows(2).all(|w| w[0].1 >= w[1].1),
"highest coupling first"
);
}
#[test]
fn rendered_section_reports_the_lines_it_used() {
let (text, used) = render_discovered_coupling(
&["src/found.rs".to_string()],
&[("src/coupled.rs".to_string(), 0.42)],
100,
false,
);
assert!(text.contains("## Related to What You Found"));
assert!(
text.contains("`src/found.rs`"),
"names what the search found"
);
assert!(text.contains("- `src/coupled.rs` (coupling: 0.42)"));
assert_eq!(
used,
text.lines().count(),
"line accounting must match the text"
);
}
#[test]
fn a_budget_that_fits_only_the_header_renders_nothing() {
let (text, used) = render_discovered_coupling(
&["src/found.rs".to_string()],
&[("src/coupled.rs".to_string(), 0.42)],
3,
false,
);
assert!(text.is_empty());
assert_eq!(used, 0);
}
#[test]
fn nothing_coupled_renders_nothing() {
let (text, used) = render_discovered_coupling(&["src/found.rs".to_string()], &[], 100, true);
assert!(text.is_empty());
assert_eq!(used, 0);
}
#[test]
fn read_of_a_source_file_dispatches_refs_only() {
let mode = classify_dispatch("Read", &json!({"file_path": "src/main.rs"}));
assert!(matches!(mode, DispatchMode::RefsOnly { .. }));
}
#[test]
fn read_of_a_non_source_file_dispatches_reactions_only() {
let mode = classify_dispatch("Read", &json!({"file_path": "README.md"}));
assert!(matches!(mode, DispatchMode::ReactionsOnly));
}
#[test]
fn edit_dispatches_edit_related() {
let mode = classify_dispatch("Edit", &json!({"file_path": "src/main.rs"}));
assert!(matches!(mode, DispatchMode::EditRelated { .. }));
}
#[test]
fn test_is_source_code_file() {
assert!(is_source_code_file("src/main.rs"));
assert!(is_source_code_file("/home/user/project/handler.go"));
assert!(is_source_code_file("app.py"));
assert!(is_source_code_file("components/Button.tsx"));
assert!(!is_source_code_file("README.md"));
assert!(!is_source_code_file("config.toml"));
assert!(!is_source_code_file("package.json"));
assert!(!is_source_code_file("styles.css"));
assert!(!is_source_code_file("Makefile"));
assert!(!is_source_code_file("data.yaml"));
}