use codelore_lib::Options;
use codelore_lib::analyses::code_age::run_code_age;
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::GixRepo;
#[test]
fn code_age_for_tiny_repo() {
use time::macros::date;
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(),
min_revs: 1,
age_time_now: Some(date!(2026 - 06 - 09)),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_code_age(&db, &opts).expect("run");
assert!(!rows.is_empty(), "should produce at least 1 row");
for row in &rows {
assert!(
row.age_months >= 0,
"age must be >= 0, got {} for {}",
row.age_months,
row.path
);
}
let main_row = rows
.iter()
.find(|r| r.path == "src/main.rs")
.expect("main.rs");
assert_eq!(
main_row.age_months, 0,
"main.rs last changed 2026-06-05, anchor 2026-06-09 → 0 months, got {}",
main_row.age_months
);
}
#[test]
fn code_age_back_test_excludes_post_anchor_commits() {
use time::macros::date;
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(),
min_revs: 1,
age_time_now: Some(date!(2020 - 01 - 01)),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_code_age(&db, &opts).expect("run");
assert!(
rows.is_empty(),
"all commits post-anchor → no rows; got {} rows",
rows.len()
);
}
#[test]
fn code_age_uses_interval_month_semantics_not_boundary_crossing() {
use std::process::Command;
use time::macros::date;
fn build_repo_with_commit_at(date_str: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
let init = Command::new("git")
.arg("-C")
.arg(path)
.args(["init", "-b", "main", "--quiet"])
.status()
.expect("git init");
assert!(init.success());
for (k, v) in [("user.email", "tiny@example.com"), ("user.name", "Tiny")] {
Command::new("git")
.arg("-C")
.arg(path)
.args(["config", k, v])
.status()
.expect("git config");
}
std::fs::write(path.join("a.txt"), "x\n").expect("write");
Command::new("git")
.arg("-C")
.arg(path)
.args(["add", "a.txt"])
.status()
.expect("git add");
let status = Command::new("git")
.arg("-C")
.arg(path)
.args(["commit", "-m", "init", "--quiet"])
.env("GIT_AUTHOR_DATE", date_str)
.env("GIT_COMMITTER_DATE", date_str)
.status()
.expect("git commit");
assert!(status.success());
dir
}
let cases: &[(&str, time::Date, i32)] = &[
("2026-03-15T12:00:00Z", date!(2026 - 04 - 01), 0),
("2026-03-15T12:00:00Z", date!(2026 - 04 - 16), 1),
("2026-03-31T12:00:00Z", date!(2026 - 04 - 30), 0),
("2026-03-15T12:00:00Z", date!(2026 - 03 - 15), 0),
("2025-04-15T12:00:00Z", date!(2026 - 04 - 15), 12),
];
for (commit_iso, anchor, expected) in cases {
let repo_dir = build_repo_with_commit_at(commit_iso);
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(),
min_revs: 1,
age_time_now: Some(*anchor),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_code_age(&db, &opts).expect("run");
let row = rows
.iter()
.find(|r| r.path == "a.txt")
.unwrap_or_else(|| panic!("missing a.txt row for case {commit_iso} → {anchor}"));
assert_eq!(
row.age_months, *expected,
"interval-month semantics for case {commit_iso} → {anchor}: expected {expected}, got {}",
row.age_months,
);
}
}
#[test]
fn code_age_never_returns_negative_age() {
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 anchor = (time::OffsetDateTime::now_utc() - time::Duration::days(30)).date();
let opts = Options {
repo_path: tiny.dir.path().to_path_buf(),
min_revs: 1,
age_time_now: Some(anchor),
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_code_age(&db, &opts).expect("run");
for row in &rows {
assert!(
row.age_months >= 0 && row.age_days >= 0,
"negative age leaked through filter for {}: months={} days={}",
row.path,
row.age_months,
row.age_days
);
}
}
#[test]
#[allow(clippy::items_after_statements)]
fn code_age_excludes_deleted_files() {
use std::process::Command;
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
fn run_git_at(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_git_at(
path,
"2026-01-01T00:00:00Z",
&["init", "-b", "main", "--quiet"],
);
run_git_at(
path,
"2026-01-01T00:00:00Z",
&["config", "user.email", "t@t"],
);
run_git_at(
path,
"2026-01-01T00:00:00Z",
&["config", "user.name", "Tiny"],
);
std::fs::write(path.join("alive.rs"), "x").unwrap();
std::fs::write(path.join("gone.rs"), "y").unwrap();
run_git_at(
path,
"2026-01-01T12:00:00Z",
&["add", "alive.rs", "gone.rs"],
);
run_git_at(
path,
"2026-01-01T12:00:00Z",
&["commit", "-m", "add both", "--quiet"],
);
std::fs::remove_file(path.join("gone.rs")).unwrap();
run_git_at(path, "2026-01-02T12:00:00Z", &["add", "-A"]);
run_git_at(
path,
"2026-01-02T12:00:00Z",
&["commit", "-m", "del", "--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,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_code_age(&db, &opts).expect("run");
let paths: Vec<&str> = rows.iter().map(|r| r.path.as_str()).collect();
assert!(
paths.contains(&"alive.rs"),
"alive.rs must surface; got paths={paths:?}",
);
assert!(
!paths.contains(&"gone.rs"),
"gone.rs was deleted — must NOT surface; got paths={paths:?}",
);
}