Ambient system companions over one privacy-preserving signal daemon (aggregate-only, no keystroke content): a git-driven terminal garden and IOKit hardware collectors. ambient daemon macos privacy terminal

crates/signald/tests/git_collector.rs

126 lines · 3750 bytes

  1//! Git collector acceptance test.
  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}