Skip to main content

borg/
live.rs

1//! Delivery modes: when the conscience judges the work (docs/delivery.md).
2//!
3//! - **hook**: early delivery and queued lessons are free word matching; one graded model
4//!   call runs at the turn end (main.rs `stop`), and only a grade 3 blocks the stop.
5//! - **live** (the default, chosen by the live-vs-hook eval): the same graded call also runs *during* the turn. After a state-changing tool call
6//!   (prefilter.rs `changes`), the PostToolUse hook spawns a detached `borg check` and returns at once;
7//!   the check judges the work so far and leaves its verdict in the `.queued` file, which the next
8//!   prompt or tool call delivers (main.rs `queued`). The stop still runs as the backstop, and skips its
9//!   own model call when a live check this turn already covered every change (`covered`).
10//!
11//! The mode is chosen per collective (`"mode"` in collectives.json, collective.rs) and `BORG_MODE`
12//! overrides it; unset or anything else is `DEFAULT`. Every harness runs it: Claude Code and omp take the
13//! next tool call's context as PostToolUse `additionalContext`, the opencode adapter adds it to the tool
14//! output (1.x) or steers it into the session (2.x). Without a Claude transcript the work so far is the
15//! turn's tool calls (main.rs `turn_file`).
16//!
17//! Three small files per session beside its state file, no daemon:
18//! - `<S>.live.n`: one line per state-changing tool call (the change counter);
19//! - `<S>.live.mark`: `<covered> <started_ms> <turn>`, the counter value the last completed check covered,
20//!   when the last check started (the debounce runs from there) and the turn it started in (main.rs
21//!   `turn_id`: a check of an earlier turn never covers this one);
22//! - `<S>.live.lock`: `<pid> <started_ms>` while a check (or a live stop) runs, linked into place whole
23//!   so two quick tool calls never run two checks; stale when its process is gone or it is older than
24//!   `stale_ms`.
25
26use std::io::Write;
27use std::path::{Path, PathBuf};
28use std::time::Duration;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Mode {
32    Hook,
33    Live,
34}
35
36impl Mode {
37    pub fn name(self) -> &'static str {
38        match self {
39            Mode::Hook => "hook",
40            Mode::Live => "live",
41        }
42    }
43}
44
45/// The mode in force and where it came from (`BORG_MODE`, `config` or `default`), with a warning
46/// when a value was not a mode (`borg doctor` shows it; the mode is then `DEFAULT`).
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct Resolved {
49    pub mode: Mode,
50    pub source: &'static str,
51    pub warning: Option<String>,
52}
53
54/// The mode when neither BORG_MODE nor the collective sets one: live, which passed the eval's fixed
55/// rule against hook (docs/delivery.md, "Why live is the default"). `"mode": "hook"` or `BORG_MODE=hook` switches back.
56pub const DEFAULT: Mode = Mode::Live;
57
58/// `env` (BORG_MODE) wins over `configured` (the collective's `mode`), else `DEFAULT`. An unknown value
59/// is `DEFAULT` plus a warning: a typo must never break the hooks.
60pub fn resolve(env: Option<&str>, configured: Option<&str>) -> Resolved {
61    let pick = |v: &str, source: &'static str| match v.trim().to_ascii_lowercase().as_str() {
62        "hook" => Resolved { mode: Mode::Hook, source, warning: None },
63        "live" => Resolved { mode: Mode::Live, source, warning: None },
64        _ => Resolved { mode: DEFAULT, source, warning: Some(format!("{source}: mode {v:?} is neither \"hook\" nor \"live\"; using {}", DEFAULT.name())) },
65    };
66    match (env.filter(|v| !v.trim().is_empty()), configured.filter(|v| !v.trim().is_empty())) {
67        (Some(v), _) => pick(v, "BORG_MODE"),
68        (None, Some(v)) => pick(v, "config"),
69        (None, None) => Resolved { mode: DEFAULT, source: "default", warning: None },
70    }
71}
72
73/// The mode of collective `name` in `cfg` for this process (BORG_MODE first).
74pub fn of(cfg: Option<&crate::collective::Config>, name: &str) -> Resolved {
75    let configured = cfg.and_then(|c| c.collectives.iter().find(|e| e.name == name)).and_then(|e| e.mode.as_deref());
76    resolve(std::env::var("BORG_MODE").ok().as_deref(), configured)
77}
78
79/// The mode of this hook process: the hook exports the resolved mode as `BORG_MODE` (main.rs), so the
80/// stop and the spawned check read the same one.
81pub fn current() -> Mode {
82    resolve(std::env::var("BORG_MODE").ok().as_deref(), None).mode
83}
84
85/// Why live mode cannot run here, if it cannot: it needs the consequence model, which word matching
86/// alone (`BORG_MATCH=bm25`) and the eval modes `BORG_ONLY` and `BORG_CRITIC` replace.
87pub fn unsupported() -> Option<&'static str> {
88    if std::env::var("BORG_MATCH").as_deref() == Ok("bm25") {
89        Some("BORG_MATCH=bm25")
90    } else if std::env::var_os("BORG_ONLY").is_some() {
91        Some("BORG_ONLY")
92    } else if std::env::var_os("BORG_CRITIC").is_some() {
93        Some("BORG_CRITIC")
94    } else {
95        None
96    }
97}
98
99/// Baseline: the least time between the starts of two checks of a session when `BORG_LIVE_DEBOUNCE_S`
100/// is unset, the value live mode passed the live-vs-hook eval with (docs/delivery.md, "Why live is the
101/// default"). It also guards against a check per tool call.
102pub const DEBOUNCE_S: i64 = 45;
103
104/// The least time between the starts of two checks of a session: `BORG_LIVE_DEBOUNCE_S`, else
105/// [`DEBOUNCE_S`].
106pub fn debounce_ms() -> i64 {
107    std::env::var("BORG_LIVE_DEBOUNCE_S").ok().and_then(|v| v.trim().parse::<i64>().ok()).unwrap_or(DEBOUNCE_S).max(0) * 1000
108}
109
110/// Guard: what a lock may outlive its check's longest model call by (the check's own reading and
111/// writing), before it is taken for a crashed check's.
112const STALE_SLACK_MS: i64 = 30_000;
113
114/// A lock older than this belongs to a check that cannot still be waiting for its model (or to a pid
115/// that was reused): derived, twice the predict timeout plus [`STALE_SLACK_MS`].
116pub fn stale_ms() -> i64 {
117    i64::try_from(crate::predict::timeout().as_millis()).unwrap_or(i64::MAX).saturating_mul(2).saturating_add(STALE_SLACK_MS)
118}
119
120fn beside(ctx: &Path, ext: &str) -> PathBuf {
121    ctx.with_extension(ext)
122}
123
124pub fn counter_file(ctx: &Path) -> PathBuf {
125    beside(ctx, "live.n")
126}
127
128pub fn mark_file(ctx: &Path) -> PathBuf {
129    beside(ctx, "live.mark")
130}
131
132pub fn lock_file(ctx: &Path) -> PathBuf {
133    beside(ctx, "live.lock")
134}
135
136/// State-changing tool calls of the session so far.
137pub fn count(ctx: &Path) -> u64 {
138    std::fs::read_to_string(counter_file(ctx)).map_or(0, |t| t.lines().count() as u64)
139}
140
141/// Counts one state-changing tool call.
142pub fn bump(ctx: &Path, tool: &str) {
143    let _ = std::fs::OpenOptions::new().create(true).append(true).open(counter_file(ctx)).and_then(|mut f| writeln!(f, "{tool}"));
144}
145
146/// What the last check covered, when it started and in which turn (None: a mark without one).
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148pub struct Mark {
149    pub covered: u64,
150    pub started_ms: i64,
151    pub turn: Option<u64>,
152}
153
154pub fn read_mark(ctx: &Path) -> Option<Mark> {
155    let t = std::fs::read_to_string(mark_file(ctx)).ok()?;
156    let mut it = t.split_whitespace();
157    Some(Mark { covered: it.next()?.parse().ok()?, started_ms: it.next()?.parse().ok()?, turn: it.next().and_then(|v| v.parse().ok()) })
158}
159
160pub fn write_mark(ctx: &Path, m: Mark) {
161    let turn = m.turn.map_or(String::new(), |t| format!(" {t}"));
162    let _ = std::fs::write(mark_file(ctx), format!("{} {}{turn}\n", m.covered, m.started_ms));
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum Lock {
167    Free,
168    Held,
169    /// The file is there, but its check is gone (or far too old): it may be taken over.
170    Stale,
171}
172
173/// Whether process `pid` still exists (signal 0 checks without sending).
174pub fn alive(pid: i32) -> bool {
175    // SAFETY: kill with signal 0 only checks that the process exists and may be signalled.
176    pid > 0 && (unsafe { libc::kill(pid, 0) } == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM))
177}
178
179/// The state of a lock whose file reads `text` at `now`. A lock is written whole (`acquire`), so one
180/// that does not parse is someone's half-written file: held, until its file is older than `stale_after`.
181fn state_of(text: &str, file_ms: i64, now: i64, stale_after: i64, alive: impl Fn(i32) -> bool) -> Lock {
182    let mut it = text.split_whitespace();
183    match (it.next().and_then(|p| p.parse::<i32>().ok()), it.next().and_then(|m| m.parse::<i64>().ok())) {
184        (Some(pid), Some(ms)) if alive(pid) && now - ms <= stale_after => Lock::Held,
185        (Some(_), Some(_)) => Lock::Stale,
186        _ if now - file_ms > stale_after => Lock::Stale,
187        _ => Lock::Held,
188    }
189}
190
191fn modified_ms(path: &Path) -> i64 {
192    std::fs::metadata(path).and_then(|m| m.modified()).ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map_or(0, |d| d.as_millis() as i64)
193}
194
195/// The lock's state at `now`; `alive` tells whether a pid still runs.
196pub fn lock_state(ctx: &Path, now: i64, stale_after: i64, alive: impl Fn(i32) -> bool) -> Lock {
197    let path = lock_file(ctx);
198    let Ok(t) = std::fs::read_to_string(&path) else { return Lock::Free };
199    state_of(&t, modified_ms(&path), now, stale_after, alive)
200}
201
202/// A held lock; dropping it removes the file, if it is still this one's.
203#[derive(Debug)]
204pub struct Guard {
205    path: PathBuf,
206    text: String,
207}
208
209impl Drop for Guard {
210    fn drop(&mut self) {
211        if std::fs::read_to_string(&self.path).is_ok_and(|t| t == self.text) {
212            let _ = std::fs::remove_file(&self.path);
213        }
214    }
215}
216
217/// Takes the session's lock for this process, or None when another check holds it. The lock is written
218/// to a file of its own first and hard-linked into place, which fails when a lock is there: no one ever
219/// reads a half-written lock. A stale lock is moved aside (a rename: only one process gets it) and taken
220/// once more; when what was moved aside is no longer the stale lock seen, someone took it over first,
221/// and it goes back.
222pub fn acquire(ctx: &Path, now: i64) -> Option<Guard> {
223    let path = lock_file(ctx);
224    let pid = std::process::id();
225    let text = format!("{pid} {now}\n");
226    let tmp = path.with_extension(format!("lock.{pid}"));
227    std::fs::write(&tmp, &text).ok()?;
228    let mut got = None;
229    for _ in 0..2 {
230        if std::fs::hard_link(&tmp, &path).is_ok() {
231            got = Some(Guard { path: path.clone(), text: text.clone() });
232            break;
233        }
234        let Ok(seen) = std::fs::read_to_string(&path) else { continue };
235        if state_of(&seen, modified_ms(&path), now, stale_ms(), alive) != Lock::Stale {
236            break;
237        }
238        let aside = path.with_extension(format!("lock.stale.{pid}"));
239        if std::fs::rename(&path, &aside).is_err() {
240            continue;
241        }
242        let moved = std::fs::read_to_string(&aside).unwrap_or_default();
243        if moved != seen {
244            let _ = std::fs::hard_link(&aside, &path);
245            let _ = std::fs::remove_file(&aside);
246            break;
247        }
248        let _ = std::fs::remove_file(&aside);
249    }
250    let _ = std::fs::remove_file(&tmp);
251    got
252}
253
254/// Runs `f` while holding an exclusive lock on `<file>.lock` (kept, never removed), so appends to `file`
255/// and taking it whole (a rename) never interleave: an append never lands in a file already taken.
256/// When the lock cannot be taken `f` runs unlocked: for a plain append no worse than without it.
257pub fn locked<T>(file: &Path, f: impl FnOnce() -> T) -> T {
258    let held = lock_of(file).ok();
259    let out = f();
260    if let Some(l) = held {
261        let _ = l.unlock();
262    }
263    out
264}
265
266/// The lock of `file` (`<file>.lock`), held until the returned file is dropped; None when it cannot be
267/// taken (`locked` runs unlocked then too). For holding several at once (moves.rs `with_locks`).
268pub fn hold(file: &Path) -> Option<std::fs::File> {
269    lock_of(file).ok()
270}
271
272/// `<file>.lock`, opened and locked exclusively.
273fn lock_of(file: &Path) -> std::io::Result<std::fs::File> {
274    let lock = std::fs::OpenOptions::new()
275        .create(true)
276        .truncate(false)
277        .write(true)
278        .open(file.with_extension(format!("{}.lock", file.extension().and_then(|e| e.to_str()).unwrap_or_default())))?;
279    lock.lock()?;
280    Ok(lock)
281}
282
283/// Whether a check should start now (Ok) or why not: not live mode, a check still running, no change
284/// since the last completed check, or the last check started less than `debounce` ago.
285pub fn decide(mode: Mode, lock: Lock, count: u64, mark: Option<Mark>, now: i64, debounce: i64) -> Result<(), &'static str> {
286    if mode != Mode::Live {
287        return Err("hook mode");
288    }
289    if lock == Lock::Held {
290        return Err("running");
291    }
292    if count <= mark.map_or(0, |m| m.covered) {
293        return Err("nothing new");
294    }
295    if mark.is_some_and(|m| now - m.started_ms < debounce) {
296        return Err("debounce");
297    }
298    Ok(())
299}
300
301/// The stop's skip in live mode: no check runs, a check that started in this turn (`turn`, main.rs
302/// `turn_id`) covered every change, and none came since. A check of an earlier turn, or one that
303/// finished after its turn's stop, never covers this turn's message.
304pub fn covered(lock: Lock, count: u64, mark: Option<Mark>, turn: u64) -> bool {
305    lock != Lock::Held && count > 0 && mark.is_some_and(|m| m.covered == count && m.turn == Some(turn))
306}
307
308/// Method: how often a stop waiting for a running check looks at its lock.
309const WAIT_POLL: Duration = Duration::from_millis(200);
310
311/// Waits up to `max_ms` for a running check to finish; the lock's state after.
312pub fn wait_free(ctx: &Path, max_ms: u64) -> Lock {
313    let until = std::time::Instant::now() + std::time::Duration::from_millis(max_ms);
314    loop {
315        let l = lock_state(ctx, crate::learn::now_ms(), stale_ms(), alive);
316        if l != Lock::Held || std::time::Instant::now() >= until {
317            return l;
318        }
319        std::thread::sleep(WAIT_POLL);
320    }
321}
322
323/// Guard: the most of the stop's budget its wait for a running check may take (a quarter), so a slow
324/// check never starves the stop's own model call, which gets the rest ([`call_limit`]).
325pub const WAIT_SHARE: u32 = 4;
326
327/// How long a stop in live mode waits for a running check at most. Derived: the share [`WAIT_SHARE`] of
328/// the budget a stop gets where the harness names none (`harness::DEFAULT_STOP_BUDGET`, 60 s: 15 s), so
329/// a harness with a longer budget does not keep the engineer waiting longer for a check.
330pub const STOP_WAIT: Duration = Duration::from_millis(crate::harness::DEFAULT_STOP_BUDGET.as_millis() as u64 / WAIT_SHARE as u64);
331
332/// Guard: what the stop's model call leaves of the budget for the rest of the hook (its records and its
333/// answer), so the answer is out before the harness kills the hook.
334pub const AFTER_CALL: Duration = Duration::from_secs(3);
335
336/// The stop's wait for a running check, inside the harness's budget (expert_route.rs `stop_budget`; none:
337/// Claude Code's): at most [`STOP_WAIT`], and at most its [`WAIT_SHARE`]. omp's 28 s gives 7 s.
338pub fn stop_wait(budget: Option<Duration>) -> Duration {
339    budget.map_or(STOP_WAIT, |b| STOP_WAIT.min(b / WAIT_SHARE))
340}
341
342/// The stop's model call's limit, `spent` into the hook: what the harness's budget leaves but
343/// [`AFTER_CALL`] for the rest of the hook, never over the model's own limit. omp's 28 s gives at most
344/// 18 s, after a 7 s wait.
345pub fn call_limit(budget: Option<Duration>, spent: Duration) -> Duration {
346    let Some(b) = budget else { return crate::predict::timeout() };
347    crate::predict::timeout().min(b.saturating_sub(spent).saturating_sub(AFTER_CALL))
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    fn tmp(tag: &str) -> PathBuf {
355        let d = std::env::temp_dir().join(format!("borg-live-{tag}-{}", std::process::id()));
356        let _ = std::fs::remove_dir_all(&d);
357        std::fs::create_dir_all(&d).unwrap();
358        d.join("s1.ctx")
359    }
360
361    #[test]
362    fn env_beats_config_and_unknown_values_fall_back_to_the_default() {
363        assert_eq!(resolve(None, None), Resolved { mode: Mode::Live, source: "default", warning: None });
364        assert_eq!(resolve(None, Some("live")).mode, Mode::Live);
365        assert_eq!(resolve(Some("hook"), Some("live")), Resolved { mode: Mode::Hook, source: "BORG_MODE", warning: None });
366        assert_eq!(resolve(Some(" LIVE "), None).mode, Mode::Live);
367        assert_eq!(resolve(Some(""), Some("live")).source, "config", "an empty BORG_MODE is unset");
368        let r = resolve(None, Some("eager"));
369        assert_eq!(r.mode, Mode::Live);
370        assert!(r.warning.unwrap().contains("\"eager\""));
371    }
372
373    #[test]
374    fn a_check_starts_only_in_live_mode_with_new_changes_no_running_check_and_after_the_debounce() {
375        let m = |covered, started_ms| Some(Mark { covered, started_ms, turn: None });
376        assert_eq!(decide(Mode::Live, Lock::Free, 1, None, 0, 45_000), Ok(()), "the first change checks at once");
377        assert_eq!(decide(Mode::Hook, Lock::Free, 1, None, 0, 45_000), Err("hook mode"));
378        assert_eq!(decide(Mode::Live, Lock::Held, 3, m(1, 0), 100_000, 45_000), Err("running"));
379        assert_eq!(decide(Mode::Live, Lock::Stale, 3, m(1, 0), 100_000, 45_000), Ok(()), "a stale lock does not block");
380        assert_eq!(decide(Mode::Live, Lock::Free, 2, m(2, 0), 100_000, 45_000), Err("nothing new"));
381        assert_eq!(decide(Mode::Live, Lock::Free, 0, None, 0, 45_000), Err("nothing new"));
382        assert_eq!(decide(Mode::Live, Lock::Free, 3, m(2, 60_000), 100_000, 45_000), Err("debounce"));
383        assert_eq!(decide(Mode::Live, Lock::Free, 3, m(2, 55_000), 100_000, 45_000), Ok(()));
384    }
385
386    #[test]
387    fn the_lock_is_held_while_its_process_lives_and_stale_when_gone_or_too_old() {
388        let ctx = tmp("lock");
389        assert_eq!(lock_state(&ctx, 1000, 120_000, |_| true), Lock::Free);
390        let g = acquire(&ctx, 1000).expect("free lock taken");
391        assert!(acquire(&ctx, 1001).is_none(), "a second check does not run");
392        assert_eq!(lock_state(&ctx, 2000, 120_000, |_| true), Lock::Held);
393        assert_eq!(lock_state(&ctx, 2000, 120_000, |_| false), Lock::Stale, "its process is gone");
394        assert_eq!(lock_state(&ctx, 200_000, 120_000, |_| true), Lock::Stale, "older than twice the timeout");
395        drop(g);
396        assert_eq!(lock_state(&ctx, 2000, 120_000, |_| true), Lock::Free, "the guard removes it");
397        // A lock left by a process that is gone is taken over.
398        std::fs::write(lock_file(&ctx), format!("{} 1000\n", i32::MAX)).unwrap();
399        assert!(!alive(i32::MAX));
400        let g = acquire(&ctx, 2000).expect("stale lock taken over");
401        // A guard whose lock was taken over leaves the new holder's file alone.
402        std::fs::write(lock_file(&ctx), "1 2000\n").unwrap();
403        drop(g);
404        assert!(lock_file(&ctx).exists(), "not this guard's lock any more");
405    }
406
407    #[test]
408    fn a_half_written_lock_is_held_until_its_file_is_old() {
409        assert_eq!(state_of("", 1000, 2000, 120_000, |_| false), Lock::Held, "being written: never taken over");
410        assert_eq!(state_of("12", 1000, 2000, 120_000, |_| false), Lock::Held);
411        assert_eq!(state_of("", 1000, 200_000, 120_000, |_| false), Lock::Stale, "left empty long ago");
412        assert_eq!(state_of("7 1000", 0, 2000, 120_000, |_| true), Lock::Held);
413        let ctx = tmp("half");
414        std::fs::write(lock_file(&ctx), "").unwrap();
415        assert!(acquire(&ctx, crate::learn::now_ms()).is_none(), "an empty, fresh lock is someone's");
416    }
417
418    #[test]
419    fn counter_and_mark_round_trip() {
420        let ctx = tmp("mark");
421        assert_eq!((count(&ctx), read_mark(&ctx)), (0, None));
422        bump(&ctx, "Edit");
423        bump(&ctx, "Bash");
424        assert_eq!(count(&ctx), 2);
425        write_mark(&ctx, Mark { covered: 2, started_ms: 77, turn: Some(9) });
426        assert_eq!(read_mark(&ctx), Some(Mark { covered: 2, started_ms: 77, turn: Some(9) }));
427        std::fs::write(mark_file(&ctx), "2 77\n").unwrap();
428        assert_eq!(read_mark(&ctx), Some(Mark { covered: 2, started_ms: 77, turn: None }), "a mark of before turns");
429    }
430
431    #[test]
432    fn the_stop_skips_only_when_a_check_of_this_turn_covered_every_change() {
433        let m = Some(Mark { covered: 3, started_ms: 0, turn: Some(40) });
434        assert!(covered(Lock::Free, 3, m, 40));
435        assert!(!covered(Lock::Held, 3, m, 40), "a running check");
436        assert!(!covered(Lock::Free, 4, m, 40), "a change after the check");
437        assert!(!covered(Lock::Free, 3, m, 41), "the check was an earlier turn's");
438        assert!(!covered(Lock::Free, 3, Some(Mark { covered: 3, started_ms: 0, turn: None }), 40), "a mark without its turn");
439        assert!(!covered(Lock::Free, 0, None, 40));
440    }
441
442    #[test]
443    fn the_stop_fits_its_wait_and_model_call_into_the_harness_budget() {
444        let s = Duration::from_secs;
445        assert_eq!(STOP_WAIT, s(15), "a quarter of the 60 s a stop gets where the harness names none");
446        assert_eq!((stop_wait(None), call_limit(None, s(30))), (s(15), s(45)), "Claude Code: unchanged");
447        assert_eq!((stop_wait(Some(s(90))), call_limit(Some(s(90)), s(15))), (s(15), s(45)), "opencode: room for both");
448        assert_eq!((stop_wait(Some(s(28))), call_limit(Some(s(28)), s(7))), (s(7), s(18)), "omp");
449        assert_eq!(call_limit(Some(s(28)), s(11)), s(14), "time spent before the call comes off it");
450        assert_eq!(call_limit(Some(s(28)), s(27)), Duration::ZERO);
451        assert_eq!(stop_wait(Some(s(8))), s(2));
452    }
453
454    #[test]
455    fn a_lock_that_cannot_be_taken_still_runs_f_unlocked() {
456        // The lock file cannot be opened (a directory in its place): `locked` runs `f` unlocked.
457        let d = tmp("try-locked");
458        let file = d.join("tasks.jsonl");
459        std::fs::create_dir_all(d.join("tasks.jsonl.lock")).unwrap();
460        assert!(locked(&file, || true));
461        let _ = std::fs::remove_dir_all(d);
462    }
463
464    #[test]
465    fn appends_under_the_queue_lock_never_land_in_a_taken_file() {
466        let ctx = tmp("queue");
467        let q = ctx.with_extension("queued");
468        let writers: Vec<_> = (0..8)
469            .map(|i| {
470                let q = q.clone();
471                std::thread::spawn(move || {
472                    for j in 0..50 {
473                        locked(&q, || std::fs::OpenOptions::new().create(true).append(true).open(&q).and_then(|mut f| writeln!(f, "{i}-{j}")).unwrap());
474                    }
475                })
476            })
477            .collect();
478        let mut got = 0;
479        let taken = q.with_extension("queued.t");
480        while writers.iter().any(|w| !w.is_finished()) {
481            if locked(&q, || std::fs::rename(&q, &taken).is_ok()) {
482                got += std::fs::read_to_string(&taken).unwrap().lines().count();
483            }
484        }
485        for w in writers {
486            w.join().unwrap();
487        }
488        if std::fs::rename(&q, &taken).is_ok() {
489            got += std::fs::read_to_string(&taken).unwrap().lines().count();
490        }
491        assert_eq!(got, 400, "every line taken once");
492    }
493}