use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use rac_engine::gitinfo::{
last_committed, last_committed_for_paths, parse_iso8601_epoch, repository_root, staleness,
DEFAULT_STALE_AFTER_DAYS,
};
const REFERENCE_EPOCH: i64 = 1_798_761_600;
fn git(dir: &Path, args: &[&str], date: Option<&str>) {
let mut cmd = Command::new("git");
cmd.arg("-C").arg(dir).args(args);
cmd.env("GIT_AUTHOR_NAME", "Vector Pin")
.env("GIT_AUTHOR_EMAIL", "vector@example.invalid")
.env("GIT_COMMITTER_NAME", "Vector Pin")
.env("GIT_COMMITTER_EMAIL", "vector@example.invalid");
if let Some(d) = date {
cmd.env("GIT_AUTHOR_DATE", d).env("GIT_COMMITTER_DATE", d);
}
let out = cmd.output().expect("run git");
assert!(
out.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&out.stderr)
);
}
fn scratch_repo() -> PathBuf {
let base = std::env::var("CARGO_TARGET_TMPDIR").unwrap_or_else(|_| "/tmp".into());
let dir = Path::new(&base).join(format!("gitinfo_scratch_{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
git(&dir, &["init", "-q"], None);
fs::write(dir.join("committed.md"), "# One\n").unwrap();
git(&dir, &["add", "committed.md"], None);
git(
&dir,
&["commit", "-q", "-m", "first"],
Some("2026-06-01T09:00:00+01:00"),
);
fs::write(dir.join("committed.md"), "# One v2\n").unwrap();
git(
&dir,
&["commit", "-q", "-am", "second"],
Some("2026-06-30T23:30:00+01:00"),
);
fs::write(dir.join("negative-offset.md"), "# West\n").unwrap();
git(&dir, &["add", "negative-offset.md"], None);
git(
&dir,
&["commit", "-q", "-m", "west"],
Some("2026-12-31T20:00:00-07:00"),
);
fs::write(dir.join("future.md"), "# Future\n").unwrap();
git(&dir, &["add", "future.md"], None);
git(
&dir,
&["commit", "-q", "-m", "future"],
Some("2027-01-03T06:00:00+02:00"),
);
fs::write(dir.join("untracked.md"), "# Loose\n").unwrap();
dir
}
#[test]
fn recency_matches_pinned_semantics() {
let repo = scratch_repo();
let root = repository_root(&repo).expect("scratch repo has a root");
let last = last_committed(&root, &repo.join("committed.md"));
assert_eq!(last.as_deref(), Some("2026-06-30T23:30:00+01:00"));
let st = staleness(last.as_deref(), DEFAULT_STALE_AFTER_DAYS, REFERENCE_EPOCH);
assert_eq!(st.age_days, Some(184));
assert_eq!(st.stale, Some(true), "184 > default threshold 180");
assert_eq!(st.last_committed.as_deref(), last.as_deref());
assert_eq!(staleness(last.as_deref(), 184, REFERENCE_EPOCH).stale, Some(false));
assert_eq!(staleness(last.as_deref(), 183, REFERENCE_EPOCH).stale, Some(true));
let west = last_committed(&root, &repo.join("negative-offset.md"));
assert_eq!(west.as_deref(), Some("2026-12-31T20:00:00-07:00"));
let stw = staleness(west.as_deref(), DEFAULT_STALE_AFTER_DAYS, REFERENCE_EPOCH);
assert_eq!(stw.age_days, Some(-1));
assert_eq!(stw.stale, Some(false));
let fut = last_committed(&root, &repo.join("future.md"));
assert_eq!(fut.as_deref(), Some("2027-01-03T06:00:00+02:00"));
let stf = staleness(fut.as_deref(), DEFAULT_STALE_AFTER_DAYS, REFERENCE_EPOCH);
assert_eq!(stf.age_days, Some(-3));
assert_eq!(stf.stale, Some(false));
let none = last_committed(&root, &repo.join("untracked.md"));
assert_eq!(none, None);
let stn = staleness(None, DEFAULT_STALE_AFTER_DAYS, REFERENCE_EPOCH);
assert_eq!(stn.last_committed, None);
assert_eq!(stn.age_days, None);
assert_eq!(stn.stale, None);
let batch_paths = vec![
repo.join("future.md"),
repo.join("committed.md"),
repo.join("untracked.md"),
repo.join("committed.md"),
];
let batch = last_committed_for_paths(&repo, &batch_paths);
assert_eq!(batch[0].1.as_deref(), fut.as_deref());
assert_eq!(batch[1].1.as_deref(), last.as_deref());
assert_eq!(batch[2].1, None);
assert_eq!(batch[3].1.as_deref(), last.as_deref());
fs::remove_dir_all(&repo).ok();
}
#[test]
fn non_repo_directory_degrades_to_none() {
let base = std::env::var("CARGO_TARGET_TMPDIR").unwrap_or_else(|_| "/tmp".into());
let dir = Path::new(&base).join(format!("gitinfo_norepo_{}", std::process::id()));
fs::create_dir_all(&dir).unwrap();
if repository_root(&dir).is_none() {
let st = staleness(None, DEFAULT_STALE_AFTER_DAYS, 0);
assert_eq!(st.stale, None);
}
fs::remove_dir_all(&dir).ok();
}
#[test]
fn iso8601_parse_offsets() {
assert_eq!(parse_iso8601_epoch("1970-01-01T00:00:00+00:00"), Some(0));
assert_eq!(parse_iso8601_epoch("1970-01-01T00:00:00Z"), Some(0));
assert_eq!(
parse_iso8601_epoch("1970-01-01T01:00:00+01:00"),
Some(0),
"offset must be subtracted to reach UTC"
);
assert_eq!(
parse_iso8601_epoch("1970-01-01T00:00:00-01:00"),
Some(3600)
);
assert_eq!(parse_iso8601_epoch("1970-01-01T00:00:00+0000"), Some(0));
assert_eq!(
parse_iso8601_epoch("1970-01-01T00:00:00.500000+00:00"),
Some(0)
);
assert_eq!(parse_iso8601_epoch("not-a-date"), None);
assert_eq!(parse_iso8601_epoch("1970-01-01T00:00:00"), None);
}