#[derive(Debug)]
pub(crate) struct LaneLedger {
committed: String,
prompt_cursor: String,
prompt_answered: bool,
piped: Vec<Piped>,
sealed: bool,
}
#[derive(Debug)]
struct Piped {
file: String,
cursor: String,
answered: bool,
signalled: bool,
completed: bool,
}
impl LaneLedger {
pub(crate) fn new(previous: String, prompt_cursor: String) -> Self {
Self {
committed: previous,
prompt_cursor,
prompt_answered: false,
piped: Vec::new(),
sealed: false,
}
}
pub(crate) fn is_sealed(&self) -> bool {
self.sealed
}
pub(crate) fn record_pipe(&mut self, file: String, cursor: String) {
self.piped.push(Piped {
file,
cursor,
answered: false,
signalled: false,
completed: false,
});
}
pub(crate) fn signalled(&mut self, file: &str) -> bool {
let Some(p) = self.piped.iter_mut().find(|p| p.file == file) else {
return false;
};
p.signalled = true;
if p.answered && !p.completed {
p.completed = true;
true
} else {
false
}
}
pub(crate) fn turn_answered(&mut self, consumed: Option<&[String]>) -> usize {
let mut completions = 0;
if !self.prompt_answered {
self.prompt_answered = true;
completions += 1;
}
for p in self.piped.iter_mut().filter(|p| !p.answered) {
if consumed.is_none_or(|files| files.contains(&p.file)) {
p.answered = true;
if p.signalled {
p.completed = true;
completions += 1;
}
}
}
self.recompute();
completions
}
pub(crate) fn finish(&mut self, clean: bool) {
self.sealed = true;
if clean {
self.prompt_answered = true;
}
self.recompute();
}
pub(crate) fn committed(&self) -> &str {
&self.committed
}
pub(crate) fn all_answered(&self) -> bool {
self.prompt_answered && self.piped.iter().all(|p| p.answered)
}
fn recompute(&mut self) {
if !self.prompt_answered {
return;
}
let mut committed = &self.prompt_cursor;
for p in &self.piped {
if !p.answered {
break;
}
committed = &p.cursor;
}
self.committed.clone_from(committed);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn files(names: &[&str]) -> Vec<String> {
names.iter().map(|n| (*n).to_string()).collect()
}
#[test]
fn follow_ups_are_committed_only_when_answered() {
let mut l = LaneLedger::new("t0".into(), "t1".into());
assert_eq!(l.committed(), "t0");
l.record_pipe("a.json".into(), "t2".into());
assert!(!l.signalled("a.json"));
l.record_pipe("b.json".into(), "t3".into());
assert!(!l.signalled("b.json"));
assert_eq!(l.turn_answered(Some(&files(&["a.json"]))), 2);
assert_eq!(l.committed(), "t2");
assert!(!l.all_answered());
l.finish(false);
assert!(l.is_sealed());
assert_eq!(l.committed(), "t2");
assert!(!l.all_answered());
}
#[test]
fn a_completion_waits_for_its_turn_start() {
let mut l = LaneLedger::new(String::new(), "t1".into());
l.record_pipe("a.json".into(), "t2".into());
assert_eq!(l.turn_answered(Some(&files(&["a.json"]))), 1);
assert!(l.signalled("a.json"));
assert!(!l.signalled("a.json"));
assert_eq!(l.turn_answered(Some(&[])), 0);
assert!(l.all_answered());
assert_eq!(l.committed(), "t2");
}
#[test]
fn an_error_before_any_answer_keeps_the_previous_cursor() {
let mut l = LaneLedger::new("t0".into(), "t1".into());
l.finish(false);
assert_eq!(l.committed(), "t0");
assert!(!l.all_answered());
let mut l = LaneLedger::new("t0".into(), "t1".into());
l.finish(true);
assert_eq!(l.committed(), "t1");
assert!(l.all_answered());
}
#[test]
fn unknown_and_unread_files_answer_nothing() {
let mut l = LaneLedger::new("t0".into(), "t1".into());
l.record_pipe("a.json".into(), "t2".into());
l.signalled("a.json");
assert_eq!(l.turn_answered(Some(&files(&["old.json"]))), 1);
assert_eq!(l.committed(), "t1");
l.finish(true);
assert_eq!(l.committed(), "t1");
assert!(!l.all_answered());
}
#[test]
fn agents_without_reports_answer_everything_piped_so_far() {
let mut l = LaneLedger::new("t0".into(), "t1".into());
l.record_pipe("a.json".into(), "t2".into());
l.signalled("a.json");
assert_eq!(l.turn_answered(None), 2);
l.record_pipe("b.json".into(), "t3".into());
l.finish(true);
assert_eq!(l.committed(), "t2");
assert!(!l.all_answered());
}
}