#![allow(clippy::doc_markdown, clippy::items_after_statements)]
use codelore_lib::Options;
use codelore_lib::analyses::knowledge_islands::{owner_activity_for_paths, run_knowledge_islands};
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::GixRepo;
fn build_fixture() -> tempfile::TempDir {
use std::process::Command;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
fn run(path: &std::path::Path, date: &str, args: &[&str]) {
let status = Command::new("git")
.arg("-C")
.arg(path)
.args(args)
.env("GIT_AUTHOR_DATE", date)
.env("GIT_COMMITTER_DATE", date)
.status()
.expect("git");
assert!(status.success());
}
run(
path,
"2024-01-01T00:00:00Z",
&["init", "-b", "main", "--quiet"],
);
run(
path,
"2024-01-01T00:00:00Z",
&["config", "user.email", "init@x"],
);
run(
path,
"2024-01-01T00:00:00Z",
&["config", "user.name", "Init"],
);
std::fs::write(
path.join("alice_owned.txt"),
"a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n",
)
.unwrap();
run(path, "2024-03-01T12:00:00Z", &["add", "alice_owned.txt"]);
run(
path,
"2024-03-01T12:00:00Z",
&[
"commit",
"-m",
"alice",
"--author",
"Alice <alice@old.com>",
"--quiet",
],
);
std::fs::write(path.join("bob_dominates.txt"), "x\n").unwrap();
run(path, "2026-05-25T12:00:00Z", &["add", "bob_dominates.txt"]);
run(
path,
"2026-05-25T12:00:00Z",
&[
"commit",
"-m",
"bob solo",
"--author",
"Bob <bob@active.com>",
"--quiet",
],
);
std::fs::write(
path.join("alice_owned.txt"),
"a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\n",
)
.unwrap();
run(path, "2026-05-26T12:00:00Z", &["add", "alice_owned.txt"]);
run(
path,
"2026-05-26T12:00:00Z",
&[
"commit",
"-m",
"bob 1-liner",
"--author",
"Bob <bob@active.com>",
"--quiet",
],
);
dir
}
#[test]
fn knowledge_islands_surfaces_departed_main_author_solo_owner() {
let fixture = build_fixture();
let repo = GixRepo::open(fixture.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: fixture.path().to_path_buf(),
min_revs: 1,
age_time_now: Some(time::macros::date!(2026 - 06 - 01)),
departed_threshold_days: 30,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_knowledge_islands(&db, &opts).expect("run");
let alice_row = rows
.iter()
.find(|r| r.entity == "alice_owned.txt")
.expect("alice_owned.txt surfaces as knowledge-island");
assert_eq!(alice_row.main_author, "alice@old.com");
assert!(
alice_row.ownership_pct > 80.0,
"Alice owns ~91% (10/11 lines); got {}",
alice_row.ownership_pct,
);
assert!(
alice_row.days_since_main_active > 800,
"should reflect ~822-day gap; got {}",
alice_row.days_since_main_active,
);
assert_eq!(
alice_row.total_loc, 11,
"total_loc must disclose the ownership_pct denominator (10 Alice + 1 Bob), got {}",
alice_row.total_loc,
);
assert!(
!rows.iter().any(|r| r.entity == "bob_dominates.txt"),
"bob_dominates.txt should NOT surface; Bob's last commit is recent",
);
}
#[test]
fn knowledge_islands_threshold_gates_inclusion() {
let fixture = build_fixture();
let repo = GixRepo::open(fixture.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: fixture.path().to_path_buf(),
min_revs: 1,
age_time_now: Some(time::macros::date!(2026 - 06 - 01)),
departed_threshold_days: 365 * 10,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_knowledge_islands(&db, &opts).expect("run");
assert!(
rows.is_empty(),
"10-year departure threshold → no rows; got {} rows",
rows.len(),
);
}
#[test]
fn knowledge_islands_excludes_deleted_files() {
use std::process::Command;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
fn run(path: &std::path::Path, date: &str, args: &[&str]) {
let status = Command::new("git")
.arg("-C")
.arg(path)
.args(args)
.env("GIT_AUTHOR_DATE", date)
.env("GIT_COMMITTER_DATE", date)
.status()
.expect("git");
assert!(status.success());
}
run(
path,
"2024-01-01T00:00:00Z",
&["init", "-b", "main", "--quiet"],
);
run(
path,
"2024-01-01T00:00:00Z",
&["config", "user.email", "t@t"],
);
run(
path,
"2024-01-01T00:00:00Z",
&["config", "user.name", "Tiny"],
);
std::fs::write(path.join("gone.txt"), "x\ny\nz\n").unwrap();
run(path, "2024-03-01T12:00:00Z", &["add", "gone.txt"]);
run(
path,
"2024-03-01T12:00:00Z",
&[
"commit",
"-m",
"add",
"--author",
"Alice <alice@old.com>",
"--quiet",
],
);
std::fs::remove_file(path.join("gone.txt")).unwrap();
run(path, "2024-04-01T12:00:00Z", &["add", "-A"]);
run(
path,
"2024-04-01T12:00:00Z",
&[
"commit",
"-m",
"delete",
"--author",
"Alice <alice@old.com>",
"--quiet",
],
);
let repo = GixRepo::open(path).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: path.to_path_buf(),
min_revs: 1,
age_time_now: Some(time::macros::date!(2026 - 06 - 01)),
departed_threshold_days: 30,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_knowledge_islands(&db, &opts).expect("run");
assert!(
!rows.iter().any(|r| r.entity == "gone.txt"),
"deleted files must not appear in knowledge-islands; got {rows:?}",
);
}
#[test]
fn owner_activity_for_paths_returns_unthresholded_activity_and_omits_unknown() {
let fixture = build_fixture();
let repo = GixRepo::open(fixture.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: fixture.path().to_path_buf(),
min_revs: 1,
age_time_now: Some(time::macros::date!(2026 - 06 - 01)),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let paths = vec![
"alice_owned.txt".to_string(),
"bob_dominates.txt".to_string(),
"no_such_file.txt".to_string(),
];
let map = owner_activity_for_paths(&db, &opts, &paths).expect("helper");
let alice = map.get("alice_owned.txt").expect("alice file present");
assert_eq!(alice.main_author, "alice@old.com");
assert!(
alice.days_since_main_active > 800,
"Alice last committed 2024-03-01; got {}",
alice.days_since_main_active
);
let bob = map.get("bob_dominates.txt").expect("bob file present");
assert!(
bob.days_since_main_active < 30,
"Bob is active; got {}",
bob.days_since_main_active
);
assert!(
!map.contains_key("no_such_file.txt"),
"unknown path = no entry = Inconclusive"
);
assert_eq!(map.len(), 2, "no extra paths leak into the result");
}
#[test]
fn owner_activity_for_paths_empty_paths_returns_empty_map_without_querying() {
let fixture = build_fixture();
let repo = GixRepo::open(fixture.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: fixture.path().to_path_buf(),
age_time_now: Some(time::macros::date!(2026 - 06 - 01)),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let map = owner_activity_for_paths(&db, &opts, &[]).expect("empty paths");
assert!(map.is_empty());
}
fn build_single_commit_fixture() -> tempfile::TempDir {
use std::process::Command;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
fn run(path: &std::path::Path, date: &str, args: &[&str]) {
let status = Command::new("git")
.arg("-C")
.arg(path)
.args(args)
.env("GIT_AUTHOR_DATE", date)
.env("GIT_COMMITTER_DATE", date)
.status()
.expect("git");
assert!(status.success());
}
run(
path,
"2024-01-01T00:00:00Z",
&["init", "-b", "main", "--quiet"],
);
run(
path,
"2024-01-01T00:00:00Z",
&["config", "user.email", "init@x"],
);
run(
path,
"2024-01-01T00:00:00Z",
&["config", "user.name", "Init"],
);
std::fs::write(path.join("solo.txt"), "a\nb\nc\n").unwrap();
run(path, "2024-03-01T12:00:00Z", &["add", "solo.txt"]);
run(
path,
"2024-03-01T12:00:00Z",
&[
"commit",
"-m",
"solo add",
"--author",
"Old <old@dev.com>",
"--quiet",
],
);
dir
}
#[test]
fn knowledge_islands_min_revs_gates_single_commit_files() {
let fixture = build_single_commit_fixture();
let repo = GixRepo::open(fixture.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let ingest_opts = Options {
repo_path: fixture.path().to_path_buf(),
age_time_now: Some(time::macros::date!(2026 - 06 - 01)),
departed_threshold_days: 30,
..Options::default()
};
db.ingest(&repo, &ingest_opts).expect("ingest");
let opts_min_revs_2 = Options {
min_revs: 2,
..ingest_opts.clone()
};
let rows_at_2 = run_knowledge_islands(&db, &opts_min_revs_2).expect("run min_revs=2");
assert!(
!rows_at_2.iter().any(|r| r.entity == "solo.txt"),
"solo.txt has 1 revision; must be excluded under min_revs=2, got {rows_at_2:?}",
);
let opts_min_revs_1 = Options {
min_revs: 1,
..ingest_opts
};
let rows_at_1 = run_knowledge_islands(&db, &opts_min_revs_1).expect("run min_revs=1");
let solo_row = rows_at_1
.iter()
.find(|r| r.entity == "solo.txt")
.expect("solo.txt must surface under min_revs=1");
assert_eq!(solo_row.main_author, "old@dev.com");
assert!(
(solo_row.ownership_pct - 100.0).abs() < f64::EPSILON,
"solo author owns 100%; got {}",
solo_row.ownership_pct,
);
assert_eq!(
solo_row.total_loc, 3,
"solo.txt's single commit adds exactly 3 lines; total_loc must disclose that \
thin denominator behind the 100% ownership figure, got {}",
solo_row.total_loc,
);
}