crates/signald/tests/git_collector.rs
126 lines · 3769 bytes
1//! Git collector acceptance test (spec §5 Phase 1).
2//!
3//! Builds a real temp git repo, drives the collector against it, and asserts the
4//! aggregate scalars — commits-in-window, commits-today, branch count, and
5//! days-since-last-commit — match the repo's true state. Also covers the stale
6//! repo (an old commit) that the garden renders as wilt.
7
8use std::path::{Path, PathBuf};
9use std::process::Command;
10
11use signal_schema::{Signal, SignalName};
12use signald::collectors::git;
13
14#[test]
15fn fresh_repo_reports_today_commits_and_no_staleness() {
16 let repo = TempRepo::init();
17 repo.commit_now("first");
18 repo.commit_now("second");
19 repo.commit_now("third");
20
21 let signals = git::collect(repo.path());
22
23 assert_eq!(value(&signals, SignalName::CommitsToday), 3.0);
24 assert_eq!(value(&signals, SignalName::CommitsWindow), 3.0);
25 assert_eq!(value(&signals, SignalName::BranchCount), 1.0);
26 assert_eq!(value(&signals, SignalName::DaysSinceLastCommit), 0.0);
27}
28
29#[test]
30fn stale_repo_reports_zero_recent_commits_and_high_staleness() {
31 let repo = TempRepo::init();
32 repo.commit_days_ago("ancient", 40);
33
34 let signals = git::collect(repo.path());
35
36 assert_eq!(value(&signals, SignalName::CommitsToday), 0.0);
37 assert_eq!(value(&signals, SignalName::CommitsWindow), 0.0);
38 let stale = value(&signals, SignalName::DaysSinceLastCommit);
39 assert!(stale >= 39.0, "expected ~40 days stale, got {stale}");
40}
41
42#[test]
43fn non_git_directory_yields_no_signals() {
44 let dir = unique_dir("ambient-nongit");
45 std::fs::create_dir_all(&dir).unwrap();
46 let signals = git::collect(&dir);
47 assert!(signals.is_empty());
48 let _ = std::fs::remove_dir_all(&dir);
49}
50
51// --- helpers ---
52
53fn value(signals: &[Signal], name: SignalName) -> f64 {
54 signals
55 .iter()
56 .find(|s| s.name == name)
57 .unwrap_or_else(|| panic!("signal {name:?} missing"))
58 .value
59 .0
60}
61
62struct TempRepo {
63 dir: PathBuf,
64}
65
66impl TempRepo {
67 fn init() -> TempRepo {
68 let dir = unique_dir("ambient-gitcollector");
69 std::fs::create_dir_all(&dir).unwrap();
70 run(&dir, &["init", "-q"], &[]);
71 // Local identity so commits work without global git config.
72 run(&dir, &["config", "user.email", "test@example.com"], &[]);
73 run(&dir, &["config", "user.name", "Test"], &[]);
74 TempRepo { dir }
75 }
76
77 fn path(&self) -> &Path {
78 &self.dir
79 }
80
81 fn commit_now(&self, msg: &str) {
82 run(&self.dir, &["commit", "--allow-empty", "-q", "-m", msg], &[]);
83 }
84
85 fn commit_days_ago(&self, msg: &str, days: u64) {
86 let secs = std::time::SystemTime::now()
87 .duration_since(std::time::UNIX_EPOCH)
88 .unwrap()
89 .as_secs()
90 - days * 86_400;
91 let date = format!("{secs} +0000");
92 run(
93 &self.dir,
94 &["commit", "--allow-empty", "-q", "-m", msg],
95 &[("GIT_AUTHOR_DATE", &date), ("GIT_COMMITTER_DATE", &date)],
96 );
97 }
98}
99
100impl Drop for TempRepo {
101 fn drop(&mut self) {
102 let _ = std::fs::remove_dir_all(&self.dir);
103 }
104}
105
106fn run(repo: &Path, args: &[&str], envs: &[(&str, &str)]) {
107 let mut cmd = Command::new("git");
108 cmd.arg("-C").arg(repo).args(args);
109 for (k, v) in envs {
110 cmd.env(k, v);
111 }
112 let out = cmd.output().expect("run git");
113 assert!(
114 out.status.success(),
115 "git {args:?} failed: {}",
116 String::from_utf8_lossy(&out.stderr)
117 );
118}
119
120fn unique_dir(prefix: &str) -> PathBuf {
121 let nanos = std::time::SystemTime::now()
122 .duration_since(std::time::UNIX_EPOCH)
123 .unwrap()
124 .as_nanos();
125 std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()))
126}