mod common;
use gitwell::cluster::{build_clusters, Cluster};
use gitwell::config::Config;
use gitwell::report::RepoReport;
use gitwell::scanner::Finding;
fn default_config() -> Config {
Config::default() }
fn report_from_wips(repo_name: &str, wips: Vec<Finding>) -> RepoReport {
RepoReport {
repo_name: repo_name.to_string(),
repo_path: format!("/tmp/{}", repo_name),
sections: vec![("WIP Markers".to_string(), wips)],
}
}
fn wip(sha: &str, ts: u64, message: &str) -> Finding {
Finding::WipCommit {
sha: sha.to_string(),
ts,
message: message.to_string(),
marker: "WIP".to_string(),
}
}
fn cluster_containing_sha<'a>(clusters: &'a [Cluster], sha: &str) -> Option<&'a Cluster> {
clusters.iter().find(|c| {
c.findings.iter().any(|(_, f)| match f {
Finding::WipCommit { sha: s, .. } => s == sha,
_ => false,
})
})
}
#[test]
fn same_repo_within_48h_is_one_session() {
let base = 1_700_000_000;
let a = wip("aaaaaaaaaaaa", base, "WIP first auth thing");
let b = wip("bbbbbbbbbbbb", base + 3_600, "WIP second auth thing");
let reports = vec![report_from_wips("alpha", vec![a, b])];
let clusters = build_clusters(&reports, &default_config());
assert_eq!(
clusters.len(),
1,
"both findings should form a single session; got {:?}",
clusters
);
assert_eq!(clusters[0].findings.len(), 2);
}
#[test]
fn same_repo_months_apart_are_separate_sessions() {
let base = 1_700_000_000;
let jan_a = wip("aaaaaaaaaaaa", base, "WIP jan auth first");
let jan_b = wip("cccccccccccc", base + 3_600, "WIP jan auth second");
let apr_a = wip("bbbbbbbbbbbb", base + 90 * 86_400, "WIP apr auth first");
let apr_b = wip("dddddddddddd", base + 90 * 86_400 + 3_600, "WIP apr auth second");
let reports = vec![report_from_wips(
"alpha",
vec![jan_a, jan_b, apr_a, apr_b],
)];
let clusters = build_clusters(&reports, &default_config());
assert_eq!(
clusters.len(),
2,
"two bursts 90 days apart should form two sessions; got {:?}",
clusters
);
let c0_shas: Vec<String> = clusters[0]
.findings
.iter()
.filter_map(|(_, f)| match f {
Finding::WipCommit { sha, .. } => Some(sha.clone()),
_ => None,
})
.collect();
let c1_shas: Vec<String> = clusters[1]
.findings
.iter()
.filter_map(|(_, f)| match f {
Finding::WipCommit { sha, .. } => Some(sha.clone()),
_ => None,
})
.collect();
for s in &c0_shas {
assert!(
!c1_shas.contains(s),
"sessions must not share findings; {} appears in both",
s
);
}
}
#[test]
fn two_repos_sharing_two_keywords_merge() {
let base = 1_700_000_000;
let alpha_findings = vec![
wip("a111111111a1", base, "WIP auth refactor alpha login"),
wip("a222222222a2", base + 3_600, "WIP auth refactor alpha session"),
];
let beta_findings = vec![
wip("b111111111b1", base, "WIP auth refactor beta login"),
wip("b222222222b2", base + 3_600, "WIP auth refactor beta session"),
];
let reports = vec![
report_from_wips("alpha", alpha_findings),
report_from_wips("beta", beta_findings),
];
let clusters = build_clusters(&reports, &default_config());
assert_eq!(
clusters.len(),
1,
"two keyword-sharing bursts must merge into ONE cluster; got {:?}",
clusters
);
assert_eq!(clusters[0].findings.len(), 4);
assert_eq!(clusters[0].repos.len(), 2);
assert!(clusters[0].repos.contains(&"alpha".to_string()));
assert!(clusters[0].repos.contains(&"beta".to_string()));
}
#[test]
fn two_repos_sharing_only_one_keyword_do_not_merge() {
let base = 1_700_000_000;
let alpha_findings = vec![
wip("a111111111a1", base, "WIP auth alpha login"),
wip("a222222222a2", base + 3_600, "WIP auth alpha session"),
];
let beta_findings = vec![
wip("b111111111b1", base, "WIP auth beta payment"),
wip("b222222222b2", base + 3_600, "WIP auth beta checkout"),
];
let reports = vec![
report_from_wips("alpha", alpha_findings),
report_from_wips("beta", beta_findings),
];
let clusters = build_clusters(&reports, &default_config());
assert_eq!(
clusters.len(),
2,
"single shared keyword must NOT merge bursts; got {:?}",
clusters
);
for c in &clusters {
assert_eq!(
c.repos.len(),
1,
"each cluster should be single-repo; got {:?}",
c.repos
);
}
}
#[test]
fn singleton_findings_are_not_reported_as_sessions() {
let f = wip("cafebabe0000", 1_700_000_000, "WIP only one");
let reports = vec![report_from_wips("solo", vec![f])];
let clusters = build_clusters(&reports, &default_config());
assert!(
clusters.is_empty(),
"singleton clusters must be dropped; got {:?}",
clusters
);
}
#[test]
fn clusters_sorted_by_size_descending() {
let base = 1_700_000_000;
let big = report_from_wips(
"big",
vec![
wip("b1111111b111", base, "WIP one"),
wip("b2222222b222", base + 60, "WIP two"),
wip("b3333333b333", base + 120, "WIP three"),
],
);
let small = report_from_wips(
"small",
vec![
wip("s1111111s111", base, "WIP xyz orange"),
wip("s2222222s222", base + 60, "WIP xyz purple"),
],
);
let clusters = build_clusters(&[big, small], &default_config());
assert_eq!(clusters.len(), 2);
assert!(
clusters[0].findings.len() >= clusters[1].findings.len(),
"bigger cluster should come first; got sizes {} and {}",
clusters[0].findings.len(),
clusters[1].findings.len()
);
let _ = cluster_containing_sha; }