mod support;
use std::io::Read;
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use support::git_repo::GitRepo;
use support::inspect::capture;
use support::shore;
struct Watcher {
child: Child,
stdout: Arc<Mutex<String>>,
_drain: thread::JoinHandle<()>,
}
impl Watcher {
fn spawn(repo: &Path, poll_ms: u64) -> Self {
let mut child = Command::new(env!("CARGO_BIN_EXE_shore"))
.args([
"history",
"--repo",
repo.to_str().unwrap(),
"--watch",
"--poll-ms",
&poll_ms.to_string(),
])
.env_remove("SHORE_LOG")
.env_remove("RUST_LOG")
.env_remove("SHORE_FORMAT")
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn shore review history --watch");
let stdout = Arc::new(Mutex::new(String::new()));
let mut child_stdout = child.stdout.take().expect("watcher stdout");
let sink = Arc::clone(&stdout);
let drain = thread::spawn(move || {
let mut buf = [0u8; 4096];
loop {
match child_stdout.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
if let Ok(mut guard) = sink.lock() {
guard.push_str(&String::from_utf8_lossy(&buf[..n]));
}
}
}
}
});
Self {
child,
stdout,
_drain: drain,
}
}
fn render_count(&self) -> usize {
self.stdout
.lock()
.map(|guard| guard.lines().filter(|line| !line.trim().is_empty()).count())
.unwrap_or(0)
}
fn wait_for_renders(&self, target: usize, timeout: Duration) -> usize {
let deadline = Instant::now() + timeout;
loop {
let count = self.render_count();
if count >= target || Instant::now() >= deadline {
return count;
}
thread::sleep(Duration::from_millis(25));
}
}
}
impl Drop for Watcher {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
#[test]
fn watch_reprints_only_when_event_set_hash_changes() {
let repo = GitRepo::new();
repo.write("src/lib.rs", "pub fn value() -> u32 { 1 }\n");
repo.commit_all("base");
repo.write("src/lib.rs", "pub fn value() -> u32 { 2 }\n");
capture(repo.path());
let watcher = Watcher::spawn(repo.path(), 50);
let after_initial = watcher.wait_for_renders(1, Duration::from_secs(10));
assert_eq!(after_initial, 1, "watch prints once on startup");
thread::sleep(Duration::from_millis(500));
assert_eq!(
watcher.render_count(),
1,
"watch must not reprint on a bare tick"
);
let output = shore([
"observation",
"add",
"--repo",
repo.path().to_str().unwrap(),
"--track",
"agent:codex",
"--title",
"watched change",
"--body",
"this moves the event set hash",
]);
assert!(
output.status.success(),
"observation add failed:\n{}",
String::from_utf8_lossy(&output.stderr)
);
let after_change = watcher.wait_for_renders(2, Duration::from_secs(10));
assert_eq!(
after_change, 2,
"watch reprints once when the event set changes"
);
thread::sleep(Duration::from_millis(500));
assert_eq!(
watcher.render_count(),
2,
"watch must not reprint again once the change is rendered"
);
}