Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
370
crates/codegen/xai-fsnotify/tests/integration.rs
Normal file
370
crates/codegen/xai-fsnotify/tests/integration.rs
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
//! Integration tests using the public API only. Each test exercises the
|
||||
//! real OS watcher against a `tempfile`-rooted fake git repo.
|
||||
//!
|
||||
//! These can be flaky on some CI runners where FS events aren't reliably
|
||||
//! delivered (matches the existing pattern in `watcher.rs` integration
|
||||
//! tests). Marked `#[ignore]` for now; run locally with
|
||||
//! `cargo test --test integration -- --ignored`.
|
||||
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::time::timeout;
|
||||
use xai_fsnotify::{FsConfig, FsEvent, FsEventKind, FsEventSource};
|
||||
|
||||
fn fake_git_repo() -> TempDir {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let git_dir = temp.path().join(".git");
|
||||
fs::create_dir(&git_dir).unwrap();
|
||||
fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").unwrap();
|
||||
fs::create_dir_all(git_dir.join("objects")).unwrap();
|
||||
fs::create_dir_all(git_dir.join("refs")).unwrap();
|
||||
temp
|
||||
}
|
||||
|
||||
fn fake_sl_repo() -> TempDir {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let sl_dir = temp.path().join(".sl");
|
||||
fs::create_dir(&sl_dir).unwrap();
|
||||
fs::write(sl_dir.join("dirstate"), sl_dirstate(0x11)).unwrap();
|
||||
temp
|
||||
}
|
||||
|
||||
/// `.sl/dirstate` = p1(20) ‖ p2(NULL_ID, 20) ‖ "\ntreestate\n"; only the
|
||||
/// leading p1 (working-copy parent) is read by the source.
|
||||
fn sl_dirstate(p1_byte: u8) -> Vec<u8> {
|
||||
let mut v = vec![p1_byte; 20];
|
||||
v.extend_from_slice(&[0u8; 20]);
|
||||
v.extend_from_slice(b"\ntreestate\n");
|
||||
v
|
||||
}
|
||||
|
||||
async fn recv_until(
|
||||
rx: &mut broadcast::Receiver<FsEvent>,
|
||||
pred: impl Fn(&FsEvent) -> bool,
|
||||
) -> FsEvent {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Ok(e) if pred(&e) => return e,
|
||||
Ok(_) => continue,
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
Err(broadcast::error::RecvError::Closed) => panic!("channel closed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[ignore = "flaky in CI; FS events not reliably delivered"]
|
||||
async fn source_emits_files_changed() {
|
||||
let temp = fake_git_repo();
|
||||
let source = FsEventSource::start(temp.path().to_path_buf(), {
|
||||
let mut c = FsConfig::default();
|
||||
c.debounce_ms = 50;
|
||||
c
|
||||
})
|
||||
.unwrap();
|
||||
let mut rx = source.subscribe();
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
fs::write(temp.path().join("hello.txt"), "world").unwrap();
|
||||
|
||||
let event = timeout(
|
||||
Duration::from_secs(2),
|
||||
recv_until(&mut rx, |e| matches!(e, FsEvent::FilesChanged { .. })),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match event {
|
||||
FsEvent::FilesChanged { kind, paths } => {
|
||||
assert!(matches!(kind, FsEventKind::Created | FsEventKind::Modified));
|
||||
assert!(paths.iter().any(|p| p.ends_with("hello.txt")));
|
||||
}
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[ignore = "flaky in CI; FS events not reliably delivered"]
|
||||
async fn source_emits_git_op_started_and_completed_no_head_change() {
|
||||
let temp = fake_git_repo();
|
||||
let source = FsEventSource::start(temp.path().to_path_buf(), {
|
||||
let mut c = FsConfig::default();
|
||||
c.debounce_ms = 50;
|
||||
c
|
||||
})
|
||||
.unwrap();
|
||||
let mut rx = source.subscribe();
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
let lock = temp.path().join(".git/index.lock");
|
||||
fs::write(&lock, "").unwrap();
|
||||
|
||||
let _ = timeout(
|
||||
Duration::from_secs(2),
|
||||
recv_until(&mut rx, |e| matches!(e, FsEvent::GitOperationStarted)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
fs::remove_file(&lock).unwrap();
|
||||
|
||||
let completed = timeout(
|
||||
Duration::from_secs(2),
|
||||
recv_until(&mut rx, |e| {
|
||||
matches!(e, FsEvent::GitOperationCompleted { .. })
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match completed {
|
||||
FsEvent::GitOperationCompleted { head_changed } => assert!(!head_changed),
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[ignore = "flaky in CI; FS events not reliably delivered"]
|
||||
async fn source_emits_completed_with_head_change_on_branch_switch() {
|
||||
let temp = fake_git_repo();
|
||||
let source = FsEventSource::start(temp.path().to_path_buf(), {
|
||||
let mut c = FsConfig::default();
|
||||
c.debounce_ms = 50;
|
||||
c
|
||||
})
|
||||
.unwrap();
|
||||
let mut rx = source.subscribe();
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
let lock = temp.path().join(".git/index.lock");
|
||||
fs::write(&lock, "").unwrap();
|
||||
let _ = timeout(
|
||||
Duration::from_secs(2),
|
||||
recv_until(&mut rx, |e| matches!(e, FsEvent::GitOperationStarted)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
fs::write(temp.path().join(".git/HEAD"), "ref: refs/heads/feature\n").unwrap();
|
||||
fs::remove_file(&lock).unwrap();
|
||||
|
||||
let event = timeout(
|
||||
Duration::from_secs(2),
|
||||
recv_until(&mut rx, |e| {
|
||||
matches!(e, FsEvent::GitOperationCompleted { .. })
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match event {
|
||||
FsEvent::GitOperationCompleted { head_changed } => assert!(head_changed),
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[ignore = "flaky in CI; FS events not reliably delivered"]
|
||||
async fn source_emits_sl_op_started_and_completed_no_head_change() {
|
||||
let temp = fake_sl_repo();
|
||||
let source = FsEventSource::start(temp.path().to_path_buf(), {
|
||||
let mut c = FsConfig::default();
|
||||
c.debounce_ms = 50;
|
||||
c
|
||||
})
|
||||
.unwrap();
|
||||
let mut rx = source.subscribe();
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
let wlock = temp.path().join(".sl/wlock");
|
||||
fs::write(&wlock, "").unwrap();
|
||||
let _ = timeout(
|
||||
Duration::from_secs(2),
|
||||
recv_until(&mut rx, |e| matches!(e, FsEvent::GitOperationStarted)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Release without moving p1 (e.g. a dirty-treestate `sl status`).
|
||||
fs::remove_file(&wlock).unwrap();
|
||||
let completed = timeout(
|
||||
Duration::from_secs(2),
|
||||
recv_until(&mut rx, |e| {
|
||||
matches!(e, FsEvent::GitOperationCompleted { .. })
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match completed {
|
||||
FsEvent::GitOperationCompleted { head_changed } => assert!(!head_changed),
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[ignore = "flaky in CI; FS events not reliably delivered"]
|
||||
async fn source_emits_completed_with_head_change_on_sl_goto() {
|
||||
let temp = fake_sl_repo();
|
||||
let source = FsEventSource::start(temp.path().to_path_buf(), {
|
||||
let mut c = FsConfig::default();
|
||||
c.debounce_ms = 50;
|
||||
c
|
||||
})
|
||||
.unwrap();
|
||||
let mut rx = source.subscribe();
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
let wlock = temp.path().join(".sl/wlock");
|
||||
fs::write(&wlock, "").unwrap();
|
||||
let _ = timeout(
|
||||
Duration::from_secs(2),
|
||||
recv_until(&mut rx, |e| matches!(e, FsEvent::GitOperationStarted)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Move the working-copy parent (p1) before releasing wlock, then release.
|
||||
// `read_head` reads the new p1 on demand when the wlock-removal is processed.
|
||||
fs::write(temp.path().join(".sl/dirstate"), sl_dirstate(0x22)).unwrap();
|
||||
fs::remove_file(&wlock).unwrap();
|
||||
|
||||
let event = timeout(
|
||||
Duration::from_secs(2),
|
||||
recv_until(&mut rx, |e| {
|
||||
matches!(e, FsEvent::GitOperationCompleted { .. })
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
match event {
|
||||
FsEvent::GitOperationCompleted { head_changed } => assert!(head_changed),
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[serial]
|
||||
async fn shared_dedupes_by_directory() {
|
||||
use std::sync::Arc;
|
||||
|
||||
let temp = TempDir::new().unwrap();
|
||||
let path = temp.path().to_path_buf();
|
||||
|
||||
// First call creates the watcher; subsequent calls for the same canonical
|
||||
// directory hand back clones of the *same* source rather than opening a
|
||||
// new OS watch. Skip gracefully where the OS denies watches (CI limits).
|
||||
let Ok(a) = xai_fsnotify::shared(path.clone(), FsConfig::default()) else {
|
||||
eprintln!("skipping: OS watcher unavailable (resource limit?)");
|
||||
return;
|
||||
};
|
||||
let before = xai_fsnotify::stats();
|
||||
let b = xai_fsnotify::shared(path.clone(), FsConfig::default()).unwrap();
|
||||
assert!(Arc::ptr_eq(&a, &b), "same dir must share one watcher");
|
||||
assert_eq!(
|
||||
Arc::strong_count(&a),
|
||||
2,
|
||||
"second shared() must clone the existing source, not create a new one"
|
||||
);
|
||||
|
||||
// The reuse must be counted as a cache hit (no new OS watcher created).
|
||||
let after = xai_fsnotify::stats();
|
||||
assert_eq!(
|
||||
after.reused_total - before.reused_total,
|
||||
1,
|
||||
"reuse must increment reused_total"
|
||||
);
|
||||
assert_eq!(
|
||||
after.created_total, before.created_total,
|
||||
"reuse must not create a new watcher"
|
||||
);
|
||||
assert!(after.live_watchers >= 1, "the shared watcher must be live");
|
||||
|
||||
// A different directory gets its own independent watcher (a real miss).
|
||||
let other = TempDir::new().unwrap();
|
||||
let c = xai_fsnotify::shared(other.path().to_path_buf(), FsConfig::default()).unwrap();
|
||||
assert!(!Arc::ptr_eq(&a, &c), "different dirs must not share");
|
||||
assert_eq!(
|
||||
xai_fsnotify::stats().created_total - after.created_total,
|
||||
1,
|
||||
"a new directory must create a new watcher"
|
||||
);
|
||||
|
||||
// Once the last sharer drops, the registry entry is reclaimed and a later
|
||||
// request rebuilds a fresh source (exercises the recreate-after-drop path).
|
||||
drop(a);
|
||||
drop(b);
|
||||
let d = xai_fsnotify::shared(path, FsConfig::default()).unwrap();
|
||||
assert_eq!(
|
||||
Arc::strong_count(&d),
|
||||
1,
|
||||
"after all sharers drop, shared() must build a new source"
|
||||
);
|
||||
}
|
||||
|
||||
/// Runnable measurement: simulates many sessions/subagents all watching the
|
||||
/// same working directory and prints how many OS watchers were saved.
|
||||
///
|
||||
/// ```bash
|
||||
/// cargo test -p xai-fsnotify --test integration \
|
||||
/// shared_watcher_scaling_demo -- --nocapture
|
||||
/// ```
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[serial]
|
||||
async fn shared_watcher_scaling_demo() {
|
||||
const SESSIONS: usize = 50;
|
||||
|
||||
let temp = TempDir::new().unwrap();
|
||||
let path = temp.path().to_path_buf();
|
||||
|
||||
let before = xai_fsnotify::stats();
|
||||
// Hold every handle alive, mirroring N concurrent sessions on one cwd.
|
||||
let mut handles = Vec::with_capacity(SESSIONS);
|
||||
for _ in 0..SESSIONS {
|
||||
let Ok(src) = xai_fsnotify::shared(path.clone(), FsConfig::default()) else {
|
||||
eprintln!("skipping: OS watcher unavailable (resource limit?)");
|
||||
return;
|
||||
};
|
||||
handles.push(src);
|
||||
}
|
||||
let after = xai_fsnotify::stats();
|
||||
|
||||
let created = after.created_total - before.created_total;
|
||||
let reused = after.reused_total - before.reused_total;
|
||||
println!(
|
||||
"shared watcher scaling: {SESSIONS} sessions on one cwd -> \
|
||||
created={created}, reused(saved)={reused}, live_watchers={}",
|
||||
after.live_watchers
|
||||
);
|
||||
println!(
|
||||
" before sharing this needed {SESSIONS} OS watchers; after sharing it needs {created}."
|
||||
);
|
||||
|
||||
// One real OS watch for the whole fleet; the rest are cache hits.
|
||||
assert_eq!(created, 1, "all sessions on one cwd share a single watcher");
|
||||
assert_eq!(reused, (SESSIONS - 1) as u64);
|
||||
assert!(after.live_watchers >= 1);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
#[ignore = "flaky in CI; FS events not reliably delivered"]
|
||||
async fn source_works_in_non_git_workspace() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let source = FsEventSource::start(temp.path().to_path_buf(), {
|
||||
let mut c = FsConfig::default();
|
||||
c.debounce_ms = 50;
|
||||
c
|
||||
})
|
||||
.unwrap();
|
||||
let mut rx = source.subscribe();
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
fs::write(temp.path().join("hi.txt"), "x").unwrap();
|
||||
let event = timeout(
|
||||
Duration::from_secs(2),
|
||||
recv_until(&mut rx, |e| matches!(e, FsEvent::FilesChanged { .. })),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(matches!(event, FsEvent::FilesChanged { .. }));
|
||||
}
|
||||
Loading…
Reference in a new issue