pub mod diff;
pub mod inventory;
pub mod snapshot;
pub mod unknown;
use crate::Paths;
use crate::error::Result;
use crate::platform::PlatformProbe;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ScanReport {
pub scanned_at: chrono::DateTime<chrono::Utc>,
pub paths: Vec<inventory::PathEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanOutcome {
pub report: ScanReport,
pub snapshot_path: PathBuf,
pub diff: Option<diff::DiffReport>,
}
pub fn run(paths: &Paths, probes: &[&dyn PlatformProbe]) -> Result<ScanOutcome> {
let prev = snapshot::load_latest(paths)?;
let report = ScanReport {
scanned_at: chrono::Utc::now(),
paths: inventory::collect(paths, probes)?,
};
let snapshot_path = snapshot::save(paths, &report)?;
let diff = prev.as_ref().map(|p| diff::compute(p, &report));
Ok(ScanOutcome {
report,
snapshot_path,
diff,
})
}
pub fn diff_against_latest(
paths: &Paths,
probes: &[&dyn PlatformProbe],
) -> Result<Option<diff::DiffReport>> {
let Some(prev) = snapshot::load_latest(paths)? else {
return Ok(None);
};
let curr = ScanReport {
scanned_at: chrono::Utc::now(),
paths: inventory::collect(paths, probes)?,
};
Ok(Some(diff::compute(&prev, &curr)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::platform::McpServerEntry;
use std::path::{Path, PathBuf};
fn paths_for(tmp: &tempfile::TempDir) -> Paths {
Paths {
home: tmp.path().to_path_buf(),
user_home: tmp.path().to_path_buf(),
}
}
struct TestNoopProbe;
impl PlatformProbe for TestNoopProbe {
fn id(&self) -> &'static str {
"noop"
}
fn target_roots(&self, _paths: &Paths) -> Vec<(String, PathBuf)> {
Vec::new()
}
fn mcp_config_paths(&self, _paths: &Paths) -> Vec<PathBuf> {
Vec::new()
}
fn extract_mcp_servers(&self, _content: &str, _path: &Path) -> Result<Vec<McpServerEntry>> {
Ok(Vec::new())
}
}
fn noop_only(probe: &TestNoopProbe) -> [&dyn PlatformProbe; 1] {
[probe as &dyn PlatformProbe]
}
#[test]
fn diff_against_latest_returns_none_for_baseline() {
let tmp = tempfile::tempdir().unwrap();
let paths = paths_for(&tmp);
let probe = TestNoopProbe;
assert!(
diff_against_latest(&paths, &noop_only(&probe))
.unwrap()
.is_none()
);
}
#[test]
fn diff_against_latest_does_not_create_new_snapshot() {
let tmp = tempfile::tempdir().unwrap();
let paths = paths_for(&tmp);
let probe = TestNoopProbe;
run(&paths, &noop_only(&probe)).unwrap();
let snapshot_dir = paths.snapshots();
let before = std::fs::read_dir(&snapshot_dir).unwrap().count();
let diff = diff_against_latest(&paths, &noop_only(&probe)).unwrap();
let after = std::fs::read_dir(&snapshot_dir).unwrap().count();
assert_eq!(before, after, "diff_against_latest must not persist");
let d = diff.expect("diff exists when baseline exists");
assert!(d.is_empty(), "no-op diff must be empty");
}
}