use codelore_lib::Options;
use codelore_lib::analyses::delivery_friction::run_delivery_friction;
use codelore_lib::facts::FactsDb;
use codelore_lib::repo::GixRepo;
#[test]
fn delivery_friction_runs_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(),
min_revs: 1,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_delivery_friction(&db, &opts).expect("run delivery-friction");
assert!(
!rows.is_empty(),
"tiny_repo must surface at least one file with revisions >= min_revs=1"
);
for row in &rows {
assert!(!row.path.is_empty(), "path populated");
assert!(row.revisions >= 1, "revisions >= min_revs filter");
assert!(
row.median_lead_time_days >= 0.0,
"lead-time delta cannot be negative; got {} for {}",
row.median_lead_time_days,
row.path
);
assert!(
row.p95_lead_time_days >= row.median_lead_time_days,
"p95 >= median by construction"
);
assert!(
row.wip_age_days >= 0.0,
"wip_age_days cannot be negative; got {} for {}",
row.wip_age_days,
row.path
);
assert!(
(0.0..=100.0).contains(&row.friction_score),
"friction_score must be in [0,100]; got {} for {}",
row.friction_score,
row.path
);
}
}
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,
"2023-01-01T00:00:00Z",
&["init", "-b", "main", "--quiet"],
);
run(
path,
"2023-01-01T00:00:00Z",
&["config", "user.email", "t@t"],
);
run(
path,
"2023-01-01T00:00:00Z",
&["config", "user.name", "Tiny"],
);
std::fs::write(path.join("app.txt"), "one\n").unwrap();
run(path, "2023-06-01T12:00:00Z", &["add", "app.txt"]);
run(
path,
"2023-06-01T12:00:00Z",
&["commit", "-m", "c1", "--quiet"],
);
std::fs::write(path.join("app.txt"), "one\ntwo\n").unwrap();
run(
path,
"2023-07-01T12:00:00Z",
&["commit", "-am", "c2", "--quiet"],
);
dir
}
#[test]
fn delivery_friction_wip_age_anchored_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(),
min_revs: 1,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let first = run_delivery_friction(&db, &opts).expect("run delivery-friction");
let second = run_delivery_friction(&db, &opts).expect("run again");
let project = |rows: &[codelore_lib::analyses::delivery_friction::DeliveryFrictionRow]| {
rows.iter()
.map(|r| (r.path.clone(), r.wip_age_days, r.friction_score))
.collect::<Vec<_>>()
};
assert_eq!(
project(&first),
project(&second),
"delivery-friction output must be deterministic across runs"
);
let app = first
.iter()
.find(|r| r.path == "app.txt")
.expect("app.txt must surface");
assert!(
app.wip_age_days < 1.0,
"wip_age_days for the newest-touched file must be ~0 under the \
max-commit-date anchor; got {}",
app.wip_age_days,
);
}
fn build_clock_skew_repo() -> tempfile::TempDir {
use std::process::Command;
fn run(path: &std::path::Path, author_date: &str, committer_date: &str, args: &[&str]) {
let status = Command::new("git")
.arg("-C")
.arg(path)
.args(args)
.env("GIT_AUTHOR_DATE", author_date)
.env("GIT_COMMITTER_DATE", committer_date)
.status()
.expect("git");
assert!(status.success());
}
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path();
let init_date = "2024-01-01T00:00:00Z";
run(
path,
init_date,
init_date,
&["init", "-b", "main", "--quiet"],
);
run(path, init_date, init_date, &["config", "user.email", "t@t"]);
run(path, init_date, init_date, &["config", "user.name", "Tiny"]);
std::fs::write(path.join("skewed.txt"), "one\n").unwrap();
run(path, init_date, init_date, &["add", "skewed.txt"]);
run(
path,
"2024-03-02T12:00:00Z",
"2024-03-01T12:00:00Z",
&["commit", "-m", "skewed commit", "--quiet"],
);
dir
}
#[test]
fn delivery_friction_excludes_negative_lead_times_from_median() {
let skewed = build_clock_skew_repo();
let repo = GixRepo::open(skewed.path()).expect("open");
let db = FactsDb::new_in_memory().expect("db");
let opts = Options {
repo_path: skewed.path().to_path_buf(),
min_revs: 1,
..Options::default()
};
db.ingest(&repo, &opts).expect("ingest");
let rows = run_delivery_friction(&db, &opts).expect("run delivery-friction");
let row = rows
.iter()
.find(|r| r.path == "skewed.txt")
.expect("skewed.txt must surface (1 revision >= min_revs=1)");
assert_eq!(
row.revisions, 1,
"the clock-skew commit must still count toward revisions — only \
the lead-time statistic excludes it, not the per-file commit count"
);
assert!(
row.median_lead_time_days >= 0.0,
"clock-skew commit (committer_date < date) must not produce a \
negative median; got {}",
row.median_lead_time_days,
);
assert!(
row.median_lead_time_days.abs() < f64::EPSILON,
"the only commit's lead_secs is excluded (non-positive) so the \
median falls back to the documented zero, exactly like the \
rebase-only-workflow case; got {}",
row.median_lead_time_days,
);
assert!(
row.p95_lead_time_days.abs() < f64::EPSILON,
"same exclusion applies to the p95 aggregate; got {}",
row.p95_lead_time_days,
);
}