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

135 lines · 4328 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;
 10use std::sync::atomic::{AtomicU64, Ordering};
 11
 12use signal_schema::{Signal, SignalName};
 13use signald::collectors::git;
 14
 15#[test]
 16fn fresh_repo_reports_today_commits_and_no_staleness() {
 17    let repo = TempRepo::init();
 18    repo.commit_now("first");
 19    repo.commit_now("second");
 20    repo.commit_now("third");
 21
 22    let signals = git::collect(repo.path());
 23
 24    assert_eq!(value(&signals, SignalName::CommitsToday), 3.0);
 25    assert_eq!(value(&signals, SignalName::CommitsWindow), 3.0);
 26    assert_eq!(value(&signals, SignalName::BranchCount), 1.0);
 27    assert_eq!(value(&signals, SignalName::DaysSinceLastCommit), 0.0);
 28}
 29
 30#[test]
 31fn stale_repo_reports_zero_recent_commits_and_high_staleness() {
 32    let repo = TempRepo::init();
 33    repo.commit_days_ago("ancient", 40);
 34
 35    let signals = git::collect(repo.path());
 36
 37    assert_eq!(value(&signals, SignalName::CommitsToday), 0.0);
 38    assert_eq!(value(&signals, SignalName::CommitsWindow), 0.0);
 39    let stale = value(&signals, SignalName::DaysSinceLastCommit);
 40    assert!(stale >= 39.0, "expected ~40 days stale, got {stale}");
 41}
 42
 43#[test]
 44fn non_git_directory_yields_no_signals() {
 45    let dir = unique_dir("ambient-nongit");
 46    std::fs::create_dir_all(&dir).unwrap();
 47    let signals = git::collect(&dir);
 48    assert!(signals.is_empty());
 49    let _ = std::fs::remove_dir_all(&dir);
 50}
 51
 52// --- helpers ---
 53
 54fn value(signals: &[Signal], name: SignalName) -> f64 {
 55    signals
 56        .iter()
 57        .find(|s| s.name == name)
 58        .unwrap_or_else(|| panic!("signal {name:?} missing"))
 59        .value
 60        .0
 61}
 62
 63struct TempRepo {
 64    dir: PathBuf,
 65}
 66
 67impl TempRepo {
 68    fn init() -> TempRepo {
 69        let dir = unique_dir("ambient-gitcollector");
 70        std::fs::create_dir_all(&dir).unwrap();
 71        run(&dir, &["init", "-q"], &[]);
 72        // Local identity so commits work without global git config.
 73        run(&dir, &["config", "user.email", "test@example.com"], &[]);
 74        run(&dir, &["config", "user.name", "Test"], &[]);
 75        TempRepo { dir }
 76    }
 77
 78    fn path(&self) -> &Path {
 79        &self.dir
 80    }
 81
 82    fn commit_now(&self, msg: &str) {
 83        run(&self.dir, &["commit", "--allow-empty", "-q", "-m", msg], &[]);
 84    }
 85
 86    fn commit_days_ago(&self, msg: &str, days: u64) {
 87        let secs = std::time::SystemTime::now()
 88            .duration_since(std::time::UNIX_EPOCH)
 89            .unwrap()
 90            .as_secs()
 91            - days * 86_400;
 92        let date = format!("{secs} +0000");
 93        run(
 94            &self.dir,
 95            &["commit", "--allow-empty", "-q", "-m", msg],
 96            &[("GIT_AUTHOR_DATE", &date), ("GIT_COMMITTER_DATE", &date)],
 97        );
 98    }
 99}
100
101impl Drop for TempRepo {
102    fn drop(&mut self) {
103        let _ = std::fs::remove_dir_all(&self.dir);
104    }
105}
106
107fn run(repo: &Path, args: &[&str], envs: &[(&str, &str)]) {
108    let mut cmd = Command::new("git");
109    cmd.arg("-C").arg(repo).args(args);
110    for (k, v) in envs {
111        cmd.env(k, v);
112    }
113    let out = cmd.output().expect("run git");
114    assert!(
115        out.status.success(),
116        "git {args:?} failed: {}",
117        String::from_utf8_lossy(&out.stderr)
118    );
119}
120
121fn unique_dir(prefix: &str) -> PathBuf {
122    // pid and a timestamp are not enough on their own: tests run in parallel
123    // threads of one process, so the pid is shared, and the clock is coarser
124    // than a nanosecond, so two tests starting together can read the same
125    // value. Two `git init`s into one directory then fail with "File exists".
126    // The counter makes the name unique within the process; pid keeps it
127    // unique across concurrent cargo runs.
128    static SEQ: AtomicU64 = AtomicU64::new(0);
129    let nanos = std::time::SystemTime::now()
130        .duration_since(std::time::UNIX_EPOCH)
131        .unwrap()
132        .as_nanos();
133    let seq = SEQ.fetch_add(1, Ordering::Relaxed);
134    std::env::temp_dir().join(format!("{prefix}-{}-{nanos}-{seq}", std::process::id()))
135}