use std::fs;
use std::path::Path;
use std::process::Command;
use projd_core::{
HealthSignalKind, HealthSignalStatus, ScanOptions, VcsKind, scan_path, scan_path_with,
};
use tempfile::tempdir;
fn git_available() -> bool {
Command::new("git")
.arg("--version")
.output()
.is_ok_and(|output| output.status.success())
}
fn run_git(cwd: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
.current_dir(cwd)
.env("GIT_AUTHOR_NAME", "Test User")
.env("GIT_AUTHOR_EMAIL", "test@example.com")
.env("GIT_COMMITTER_NAME", "Test User")
.env("GIT_COMMITTER_EMAIL", "test@example.com")
.output()
.expect("run git");
assert!(
output.status.success(),
"git {:?} failed:\nstdout: {}\nstderr: {}",
args,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
fn make_repo_with_one_commit(dir: &Path) {
run_git(dir, &["init", "-q", "--initial-branch=main"]);
run_git(dir, &["config", "user.email", "test@example.com"]);
run_git(dir, &["config", "user.name", "Test User"]);
fs::write(dir.join("file.txt"), "hello\n").unwrap();
run_git(dir, &["add", "file.txt"]);
run_git(dir, &["commit", "-q", "-m", "initial commit"]);
}
#[test]
fn activity_filled_for_real_git_repo() {
if !git_available() {
return;
}
let tmp = tempdir().unwrap();
make_repo_with_one_commit(tmp.path());
let scan = scan_path(tmp.path()).expect("scan succeeds");
assert_eq!(scan.vcs.kind, VcsKind::Git);
assert!(
scan.vcs.activity.days_since_last_commit.is_some(),
"days_since_last_commit should be filled"
);
assert_eq!(
scan.vcs.activity.days_since_last_commit,
Some(0),
"fresh commit should be 0 days old"
);
assert!(
scan.vcs.activity.commits_last_90d.unwrap_or(0) >= 1,
"should see at least 1 commit in 90d window"
);
assert_eq!(
scan.vcs.activity.contributors_count,
Some(1),
"single author should be counted"
);
assert!(
scan.vcs.activity.first_commit_date.is_some(),
"first_commit_date should be set"
);
}
#[test]
fn activity_empty_for_non_git_dir() {
let tmp = tempdir().unwrap();
let scan = scan_path(tmp.path()).expect("scan succeeds");
assert_eq!(scan.vcs.kind, VcsKind::None);
assert!(scan.vcs.activity.days_since_last_commit.is_none());
assert!(scan.vcs.activity.commits_last_90d.is_none());
assert!(scan.vcs.activity.contributors_count.is_none());
assert!(scan.vcs.activity.first_commit_date.is_none());
assert!(scan.vcs.activity.contributors.is_empty());
}
#[test]
fn detailed_contributors_off_by_default() {
if !git_available() {
return;
}
let tmp = tempdir().unwrap();
make_repo_with_one_commit(tmp.path());
let scan = scan_path(tmp.path()).expect("scan succeeds");
assert!(
scan.vcs.activity.contributors.is_empty(),
"contributors should be empty by default"
);
}
#[test]
fn detailed_contributors_on_yields_at_least_one() {
if !git_available() {
return;
}
let tmp = tempdir().unwrap();
make_repo_with_one_commit(tmp.path());
let opts = ScanOptions {
detailed_contributors: true,
};
let scan = scan_path_with(tmp.path(), &opts).expect("scan succeeds");
assert!(
!scan.vcs.activity.contributors.is_empty(),
"contributors should be populated when opt-in"
);
let contrib = &scan.vcs.activity.contributors[0];
assert!(!contrib.name.is_empty(), "name should be filled");
assert!(
contrib.email.contains('@'),
"email should look like an address"
);
assert!(contrib.commit_count >= 1);
}
#[test]
fn health_signals_include_activity_pass_for_fresh_repo() {
if !git_available() {
return;
}
let tmp = tempdir().unwrap();
make_repo_with_one_commit(tmp.path());
let scan = scan_path(tmp.path()).expect("scan succeeds");
let activity_signal = scan
.health
.signals
.iter()
.find(|signal| signal.kind == HealthSignalKind::Activity)
.expect("Activity signal present");
assert_eq!(activity_signal.status, HealthSignalStatus::Pass);
assert!(activity_signal.evidence.contains("active"));
}
#[test]
fn health_signals_include_activity_info_for_no_vcs() {
let tmp = tempdir().unwrap();
let scan = scan_path(tmp.path()).expect("scan succeeds");
let activity_signal = scan
.health
.signals
.iter()
.find(|signal| signal.kind == HealthSignalKind::Activity)
.expect("Activity signal present");
assert_eq!(activity_signal.status, HealthSignalStatus::Info);
assert!(activity_signal.evidence.contains("unknown"));
}