use codelore_lib::Options;
use codelore_lib::analyses::stale_code::run_stale_code;
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::GixRepo;
#[test]
fn stale_code_executes_cleanly_on_tiny_repo() {
let tiny = codelore_lib::test_support::tiny_repo::build();
let repo = GixRepo::open(tiny.dir.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: tiny.dir.path().to_path_buf(),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_stale_code(&db, &opts).expect("run stale-code");
assert!(
rows.is_empty(),
"tiny_repo has only fresh commits — expected empty stale-code result, got {} rows",
rows.len(),
);
}
fn build_aged_repo() -> tempfile::TempDir {
use std::process::Command;
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());
}
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
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("legacy.txt"), "old\n").unwrap();
run(path, "2022-01-15T12:00:00Z", &["add", "legacy.txt"]);
run(
path,
"2022-01-15T12:00:00Z",
&["commit", "-m", "legacy", "--quiet"],
);
std::fs::write(path.join("recent.txt"), "new\n").unwrap();
run(path, "2024-01-15T12:00:00Z", &["add", "recent.txt"]);
run(
path,
"2024-01-15T12:00:00Z",
&["commit", "-m", "recent", "--quiet"],
);
dir
}
#[test]
fn stale_code_anchor_defaults_to_newest_commit_deterministically() {
let aged = build_aged_repo();
let repo = GixRepo::open(aged.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: aged.path().to_path_buf(),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let first = run_stale_code(&db, &opts).expect("run stale-code");
let second = run_stale_code(&db, &opts).expect("run stale-code again");
let project = |rows: &[codelore_lib::analyses::stale_code::StaleCodeRow]| {
rows.iter()
.map(|r| {
(
r.path.clone(),
r.last_touched.clone(),
r.months_since_touched,
)
})
.collect::<Vec<_>>()
};
assert_eq!(
project(&first),
project(&second),
"stale-code output must be deterministic across runs"
);
let legacy = first
.iter()
.find(|r| r.path == "legacy.txt")
.expect("legacy.txt must surface as stale");
assert!(
legacy.months_since_touched >= 12,
"legacy.txt should be >=12 months stale relative to the newest commit; got {}",
legacy.months_since_touched,
);
assert!(
!first.iter().any(|r| r.path == "recent.txt"),
"recent.txt (the anchor commit) is not stale and must not surface",
);
}
fn build_deleted_then_readded_repo() -> tempfile::TempDir {
use std::process::Command;
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());
}
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
run(
path,
"2022-01-15T12:00:00Z",
&["init", "-b", "main", "--quiet"],
);
run(
path,
"2022-01-15T12:00:00Z",
&["config", "user.email", "t@t"],
);
run(
path,
"2022-01-15T12:00:00Z",
&["config", "user.name", "Tiny"],
);
std::fs::write(path.join("zombie.txt"), "first life\n").unwrap();
run(path, "2022-01-15T12:00:00Z", &["add", "zombie.txt"]);
run(
path,
"2022-01-15T12:00:00Z",
&["commit", "-m", "add zombie", "--quiet"],
);
run(
path,
"2022-02-15T12:00:00Z",
&["rm", "--quiet", "zombie.txt"],
);
run(
path,
"2022-02-15T12:00:00Z",
&["commit", "-m", "remove zombie", "--quiet"],
);
std::fs::write(path.join("zombie.txt"), "second life\n").unwrap();
run(path, "2022-03-15T12:00:00Z", &["add", "zombie.txt"]);
run(
path,
"2022-03-15T12:00:00Z",
&["commit", "-m", "re-add zombie", "--quiet"],
);
std::fs::write(path.join("recent.txt"), "new\n").unwrap();
run(path, "2024-01-15T12:00:00Z", &["add", "recent.txt"]);
run(
path,
"2024-01-15T12:00:00Z",
&["commit", "-m", "recent", "--quiet"],
);
dir
}
#[test]
fn stale_code_surfaces_deleted_then_readded_file() {
let repo_dir = build_deleted_then_readded_repo();
let repo = GixRepo::open(repo_dir.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: repo_dir.path().to_path_buf(),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_stale_code(&db, &opts).expect("run stale-code");
let zombie = rows
.iter()
.find(|r| r.path == "zombie.txt")
.expect("zombie.txt (deleted then re-added, live at HEAD) must surface as stale");
assert!(
zombie.months_since_touched >= 12,
"zombie.txt should read >=12 months stale relative to the 2024 anchor; got {}",
zombie.months_since_touched,
);
assert_eq!(
zombie.last_touched, "2022-03-15",
"last_touched must be the re-add date, not the intervening deletion",
);
}
fn build_renamed_repo() -> tempfile::TempDir {
use std::process::Command;
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());
}
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
run(
path,
"2022-01-15T12:00:00Z",
&["init", "-b", "main", "--quiet"],
);
run(
path,
"2022-01-15T12:00:00Z",
&["config", "user.email", "t@t"],
);
run(
path,
"2022-01-15T12:00:00Z",
&["config", "user.name", "Tiny"],
);
std::fs::write(path.join("legacy_old.txt"), "alpha\nbeta\ngamma\ndelta\n").unwrap();
run(path, "2022-01-15T12:00:00Z", &["add", "legacy_old.txt"]);
run(
path,
"2022-01-15T12:00:00Z",
&["commit", "-m", "add legacy", "--quiet"],
);
run(
path,
"2022-02-15T12:00:00Z",
&["mv", "legacy_old.txt", "legacy_new.txt"],
);
run(
path,
"2022-02-15T12:00:00Z",
&["commit", "-m", "rename legacy", "--quiet"],
);
std::fs::write(path.join("recent.txt"), "new\n").unwrap();
run(path, "2024-01-15T12:00:00Z", &["add", "recent.txt"]);
run(
path,
"2024-01-15T12:00:00Z",
&["commit", "-m", "recent", "--quiet"],
);
dir
}
#[test]
fn stale_code_no_ghost_for_renamed_file_under_lineage() {
let repo_dir = build_renamed_repo();
let repo = GixRepo::open(repo_dir.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts_off = Options {
repo_path: repo_dir.path().to_path_buf(),
use_canonical_lineage: false,
..Options::default()
};
db.ingest(&repo, &opts_off).expect("ingest");
let rows_off = run_stale_code(&db, &opts_off).expect("run stale-code (lineage off)");
assert!(
rows_off.iter().any(|r| r.path == "legacy_old.txt"),
"fixture integrity: legacy_old.txt must ghost as stale under raw changes \
(proves git mv ingested as a rename); got: {rows_off:?}"
);
let opts_on = Options {
repo_path: repo_dir.path().to_path_buf(),
use_canonical_lineage: true,
..Options::default()
};
let rows_on = run_stale_code(&db, &opts_on).expect("run stale-code (lineage on)");
assert!(
!rows_on.iter().any(|r| r.path == "legacy_old.txt"),
"legacy_old.txt must NOT ghost when canonical lineage is on; got: {rows_on:?}"
);
assert!(
rows_on.iter().any(|r| r.path == "legacy_new.txt"),
"the live canonical path legacy_new.txt should surface as stale; got: {rows_on:?}"
);
}