use codelore_lib::Options;
use codelore_lib::analyses::team_composition::run_team_composition;
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::gix_repo::GixRepo;
fn ingest(repo_path: &std::path::Path) -> FactsDb {
let db = FactsDb::new_in_memory().expect("in-memory db");
let repo = GixRepo::open(repo_path).expect("open repo");
let opts = Options {
repo_path: repo_path.to_path_buf(),
min_revs: 1,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
db
}
#[test]
fn tiny_repo_runs_without_error() {
let fixture = codelore_lib::test_support::tiny_repo::build();
let db = ingest(fixture.dir.path());
let opts = Options {
repo_path: fixture.dir.path().to_path_buf(),
window_days: 365,
min_revs: 1,
..Options::default()
};
let rows = run_team_composition(&db, &opts).expect("run team-composition");
assert!(
rows.iter().any(|r| r.author == "__summary__"),
"summary row must be present"
);
}
#[test]
fn tiny_repo_single_author_is_onboarded_bucket() {
let fixture = codelore_lib::test_support::tiny_repo::build();
let db = ingest(fixture.dir.path());
let opts = Options {
repo_path: fixture.dir.path().to_path_buf(),
window_days: 365,
min_revs: 1,
..Options::default()
};
let rows = run_team_composition(&db, &opts).expect("run team-composition");
let author_rows: Vec<_> = rows.iter().filter(|r| r.author != "__summary__").collect();
for row in &author_rows {
assert!(
row.bucket == "onboarded" || row.bucket == "experienced",
"tiny_repo spans days, not years; bucket must be onboarded or experienced, got {:?} for {}",
row.bucket,
row.author,
);
}
}
#[test]
fn delivery_repo_returns_three_author_rows_plus_summary() {
let fixture = codelore_lib::test_support::delivery_repo::build();
let db = ingest(fixture.dir.path());
let opts = Options {
repo_path: fixture.dir.path().to_path_buf(),
window_days: 90,
min_revs: 1,
..Options::default()
};
let rows = run_team_composition(&db, &opts).expect("run team-composition");
let author_rows: Vec<_> = rows.iter().filter(|r| r.author != "__summary__").collect();
assert_eq!(
author_rows.len(),
3,
"delivery_repo has 3 canonical authors; got {} author rows",
author_rows.len(),
);
assert!(
rows.iter().any(|r| r.author == "__summary__"),
"summary row must be present"
);
}
#[test]
fn delivery_repo_alice_is_experienced() {
let fixture = codelore_lib::test_support::delivery_repo::build();
let db = ingest(fixture.dir.path());
let opts = Options {
repo_path: fixture.dir.path().to_path_buf(),
window_days: 90,
min_revs: 1,
..Options::default()
};
let rows = run_team_composition(&db, &opts).expect("run team-composition");
let alice = rows
.iter()
.find(|r| r.author.contains("alice"))
.expect("alice row must be present");
assert_eq!(
alice.bucket, "experienced",
"alice span=110 d → experienced; got {:?}",
alice.bucket,
);
assert!(
alice.tenure_days >= 100 && alice.tenure_days <= 120,
"alice tenure_days must be around 110, got {}",
alice.tenure_days,
);
}
#[test]
fn delivery_repo_no_veterans() {
let fixture = codelore_lib::test_support::delivery_repo::build();
let db = ingest(fixture.dir.path());
let opts = Options {
repo_path: fixture.dir.path().to_path_buf(),
window_days: 90,
min_revs: 1,
..Options::default()
};
let rows = run_team_composition(&db, &opts).expect("run team-composition");
for row in rows.iter().filter(|r| r.author != "__summary__") {
assert_ne!(
row.bucket, "veteran",
"no author spans 365 d in delivery_repo; got veteran for {}",
row.author,
);
}
}
#[test]
fn delivery_repo_all_founders_have_null_onboarding_weeks() {
let fixture = codelore_lib::test_support::delivery_repo::build();
let db = ingest(fixture.dir.path());
let opts = Options {
repo_path: fixture.dir.path().to_path_buf(),
window_days: 90,
min_revs: 1,
..Options::default()
};
let rows = run_team_composition(&db, &opts).expect("run team-composition");
for row in rows.iter().filter(|r| r.author != "__summary__") {
assert_eq!(
row.onboarding_weeks, None,
"all delivery_repo authors are founders; onboarding_weeks must be NULL for {}",
row.author,
);
}
}
#[test]
fn delivery_repo_summary_bucket_string_contains_pcts() {
let fixture = codelore_lib::test_support::delivery_repo::build();
let db = ingest(fixture.dir.path());
let opts = Options {
repo_path: fixture.dir.path().to_path_buf(),
window_days: 90,
min_revs: 1,
..Options::default()
};
let rows = run_team_composition(&db, &opts).expect("run team-composition");
let summary = rows
.iter()
.find(|r| r.author == "__summary__")
.expect("summary row must be present");
assert!(
summary.bucket.contains("onboarded=") && summary.bucket.contains("experienced="),
"summary bucket must contain onboarded and experienced keys; got {:?}",
summary.bucket,
);
}
#[test]
fn differential_repo_excludes_bot_author() {
let fixture = codelore_lib::test_support::differential_repo::build();
let db = ingest(fixture.dir.path());
let opts = Options {
repo_path: fixture.dir.path().to_path_buf(),
window_days: 365,
min_revs: 1,
..Options::default()
};
let rows = run_team_composition(&db, &opts).expect("run team-composition");
let author_rows: Vec<_> = rows.iter().filter(|r| r.author != "__summary__").collect();
assert_eq!(
author_rows.len(),
3,
"differential_repo has 3 human canonical authors; bot must be excluded; got {} rows: {:?}",
author_rows.len(),
author_rows.iter().map(|r| &r.author).collect::<Vec<_>>(),
);
for row in &author_rows {
assert!(
!row.author.to_lowercase().contains("dependabot"),
"bot author must not appear in team-composition rows; got {:?}",
row.author,
);
}
}
#[cfg(feature = "test-support")]
fn git_as_tc(path: &std::path::Path, args: &[&str], author: &str, email: &str, date: &str) {
let status = std::process::Command::new("git")
.arg("-C")
.arg(path)
.args(args)
.env("GIT_AUTHOR_NAME", author)
.env("GIT_AUTHOR_EMAIL", email)
.env("GIT_COMMITTER_NAME", author)
.env("GIT_COMMITTER_EMAIL", email)
.env("GIT_AUTHOR_DATE", date)
.env("GIT_COMMITTER_DATE", date)
.status()
.expect("git");
assert!(status.success(), "git {args:?} failed");
}
#[test]
#[cfg(feature = "test-support")]
#[allow(clippy::too_many_lines)]
fn veteran_capped_to_experienced_when_breadth_below_median() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
let git = |args: &[&str], author: &str, email: &str, date: &str| {
git_as_tc(path, args, author, email, date);
};
git(
&["init", "--quiet"],
"Alice",
"alice@example.com",
"2024-01-01T10:00:00Z",
);
git(
&["config", "gc.auto", "0"],
"Alice",
"alice@example.com",
"2024-01-01T10:00:00Z",
);
std::fs::write(path.join("a.rs"), "pub fn a1() -> u32 { 1 }\n").expect("write");
git(
&["add", "a.rs"],
"Alice",
"alice@example.com",
"2024-01-01T10:00:00Z",
);
git(
&["commit", "-m", "feat: alice init"],
"Alice",
"alice@example.com",
"2024-01-01T10:00:00Z",
);
std::fs::write(path.join("b.rs"), "pub fn b() -> u32 { 1 }\n").expect("write");
git(
&["add", "b.rs"],
"Bob",
"bob@example.com",
"2024-01-02T10:00:00Z",
);
git(
&["commit", "-m", "feat: bob b"],
"Bob",
"bob@example.com",
"2024-01-02T10:00:00Z",
);
std::fs::write(path.join("c.rs"), "pub fn c() -> u32 { 1 }\n").expect("write");
git(
&["add", "c.rs"],
"Bob",
"bob@example.com",
"2024-01-10T10:00:00Z",
);
git(
&["commit", "-m", "feat: bob c"],
"Bob",
"bob@example.com",
"2024-01-10T10:00:00Z",
);
std::fs::write(path.join("d.rs"), "pub fn d() -> u32 { 1 }\n").expect("write");
git(
&["add", "d.rs"],
"Bob",
"bob@example.com",
"2024-01-20T10:00:00Z",
);
git(
&["commit", "-m", "feat: bob d"],
"Bob",
"bob@example.com",
"2024-01-20T10:00:00Z",
);
std::fs::write(path.join("a.rs"), "pub fn a1() -> u32 { 2 }\n").expect("write");
git(
&["add", "a.rs"],
"Alice",
"alice@example.com",
"2025-02-01T10:00:00Z",
);
git(
&["commit", "-m", "chore: alice touch"],
"Alice",
"alice@example.com",
"2025-02-01T10:00:00Z",
);
let db = FactsDb::new_in_memory().expect("in-memory db");
let repo = GixRepo::open(path).expect("open repo");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 1,
window_days: 500,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_team_composition(&db, &opts).expect("run team-composition");
let alice = rows
.iter()
.find(|r| r.author.contains("alice"))
.expect("alice row must be present");
assert!(
alice.tenure_days >= 365,
"alice span must be ≥365 d to be veteran-eligible; got {}",
alice.tenure_days,
);
assert!(
!alice.veteran_breadth_ok,
"alice touches only 1 path; core-set median is 2; breadth gate must be false",
);
assert_eq!(
alice.bucket, "experienced",
"veteran capped to experienced when breadth_ok=false; got {:?}",
alice.bucket,
);
}