Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
64 lines
2.5 KiB
Rust
64 lines
2.5 KiB
Rust
//! Pure tests for `project_root_of` + `Debouncer`. No fs, no notify, no
|
|
//! tokio — these helpers are sync by construction.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use kei_projects_watcher::{project_root_of, Debouncer};
|
|
|
|
#[test]
|
|
fn project_root_of_returns_immediate_child() {
|
|
let root = Path::new("/Users/x/Projects");
|
|
let touched = Path::new("/Users/x/Projects/MyApp/src/main.rs");
|
|
let got = project_root_of(touched, root).expect("path is under root");
|
|
assert_eq!(got, PathBuf::from("/Users/x/Projects/MyApp"));
|
|
}
|
|
|
|
#[test]
|
|
fn project_root_of_handles_top_level_file_in_project() {
|
|
let root = Path::new("/Users/x/Projects");
|
|
let touched = Path::new("/Users/x/Projects/MyApp/Cargo.toml");
|
|
let got = project_root_of(touched, root).expect("path is under root");
|
|
assert_eq!(got, PathBuf::from("/Users/x/Projects/MyApp"));
|
|
}
|
|
|
|
#[test]
|
|
fn project_root_of_returns_none_when_outside_root() {
|
|
let root = Path::new("/Users/x/Projects");
|
|
assert!(project_root_of(Path::new("/Users/x/Other/file.txt"), root).is_none());
|
|
assert!(project_root_of(Path::new("/Users/x/Projects"), root).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn debouncer_collapses_multiple_events_to_one() {
|
|
let window = Duration::from_secs(2);
|
|
let mut deb = Debouncer::new(window);
|
|
let project = PathBuf::from("/Users/x/Projects/MyApp");
|
|
let t0 = Instant::now();
|
|
deb.push(project.clone(), t0);
|
|
deb.push(project.clone(), t0 + Duration::from_millis(500));
|
|
deb.push(project.clone(), t0 + Duration::from_millis(1000));
|
|
let probe_early = t0 + Duration::from_millis(1500);
|
|
assert!(deb.drain_ready(probe_early).is_empty(), "still inside quiet window");
|
|
assert_eq!(deb.pending_len(), 1, "all 3 pushes coalesced into one project entry");
|
|
let probe_late = t0 + Duration::from_millis(3500);
|
|
let ready = deb.drain_ready(probe_late);
|
|
assert_eq!(ready, vec![project], "exactly one emission for the project");
|
|
assert_eq!(deb.pending_len(), 0);
|
|
assert!(deb.drain_ready(probe_late).is_empty(), "drain consumes the entry");
|
|
}
|
|
|
|
#[test]
|
|
fn debouncer_isolates_distinct_projects() {
|
|
let mut deb = Debouncer::new(Duration::from_millis(100));
|
|
let a = PathBuf::from("/r/A");
|
|
let b = PathBuf::from("/r/B");
|
|
let t0 = Instant::now();
|
|
deb.push(a.clone(), t0);
|
|
deb.push(b.clone(), t0 + Duration::from_millis(80));
|
|
let mut ready = deb.drain_ready(t0 + Duration::from_millis(150));
|
|
ready.sort();
|
|
assert_eq!(ready, vec![a.clone()]);
|
|
let ready2 = deb.drain_ready(t0 + Duration::from_millis(200));
|
|
assert_eq!(ready2, vec![b]);
|
|
}
|