use std::time::Duration;
use crate::brain::agent::ProgressEvent;
use crate::channels::whatsapp::flow_render::{
FlushAction, Phase, WindowPlan, fold, render, within_edit_window,
};
fn task_started(task_id: &str, label: &str) -> ProgressEvent {
ProgressEvent::DetachedTaskStarted {
task_id: task_id.to_string(),
label: label.to_string(),
}
}
fn task_finished(task_id: &str, success: bool) -> ProgressEvent {
ProgressEvent::DetachedTaskFinished {
task_id: task_id.to_string(),
label: String::new(),
success,
elapsed_secs: 12.0,
}
}
struct Counter {
plan: WindowPlan,
posts: usize,
edits: usize,
}
impl Counter {
fn new() -> Self {
Self {
plan: WindowPlan::default(),
posts: 0,
edits: 0,
}
}
fn flush(&mut self, text: &str, settled: bool, window_open: bool) -> FlushAction {
let action = self.plan.decide(text, settled, window_open);
match action {
FlushAction::Post => {
self.plan.landed(text);
self.posts += 1;
}
FlushAction::Edit => {
self.plan.landed(text);
self.edits += 1;
}
FlushAction::Skip => {}
}
action
}
}
#[test]
fn an_update_inside_the_window_edits_the_row_it_posted() {
let mut counter = Counter::new();
assert_eq!(
counter.flush("running 0:05", false, true),
FlushAction::Post
);
assert_eq!(counter.posts, 1, "the first render is the row");
assert_eq!(
counter.flush("running 0:10", false, true),
FlushAction::Edit
);
assert_eq!(counter.posts, 1, "inside the window the row is edited");
assert_eq!(counter.edits, 1);
assert_eq!(
counter.flush("running 0:10", false, true),
FlushAction::Skip
);
assert_eq!(counter.edits, 1);
}
#[test]
fn a_detached_row_updates_in_place_while_the_window_is_open() {
let mut steps = Vec::new();
fold(&mut steps, &task_started("t1", "cargo test"));
let mut counter = Counter::new();
let running = render(&steps, Phase::Running, Duration::from_secs(5));
assert_eq!(counter.flush(&running, false, true), FlushAction::Post);
assert!(running.contains("cargo test"), "{running}");
let later = render(&steps, Phase::Running, Duration::from_secs(30));
assert_eq!(counter.flush(&later, false, true), FlushAction::Edit);
assert_eq!(counter.posts, 1, "one row for the whole task");
}
#[test]
fn after_the_window_closes_running_ticks_stop_posting() {
let mut counter = Counter::new();
assert_eq!(
counter.flush("running 0:05", false, true),
FlushAction::Post
);
for tick in 1..=20 {
let text = format!("running 16:{tick:02}");
assert_eq!(counter.flush(&text, false, false), FlushAction::Skip);
}
assert_eq!(
counter.posts, 1,
"a closed window posts nothing while running"
);
assert_eq!(counter.edits, 0);
assert_eq!(counter.flush("done 16:30", true, false), FlushAction::Post);
assert_eq!(counter.edits, 0, "an expired plan never edits again");
}
#[test]
fn the_settled_row_posts_exactly_once_after_the_window_closed() {
let mut counter = Counter::new();
assert_eq!(
counter.flush("running 0:05", false, true),
FlushAction::Post
);
assert_eq!(
counter.flush("running 15:01", false, false),
FlushAction::Skip
);
assert_eq!(
counter.flush("running 15:06", false, false),
FlushAction::Skip
);
assert_eq!(counter.flush("done 18:20", true, false), FlushAction::Post);
assert_eq!(counter.posts, 2, "the start row plus one final row");
assert_eq!(counter.flush("done 18:20", true, false), FlushAction::Skip);
assert_eq!(counter.flush("done 18:21", true, true), FlushAction::Edit);
assert_eq!(counter.posts, 2, "the final row is never duplicated");
}
#[test]
fn a_finish_after_the_window_renders_a_settled_row_not_a_running_one() {
let mut steps = Vec::new();
fold(&mut steps, &task_started("t1", "cargo test"));
let running = render(&steps, Phase::Running, Duration::from_secs(5));
let mut counter = Counter::new();
assert_eq!(counter.flush(&running, false, true), FlushAction::Post);
fold(&mut steps, &task_finished("t1", true));
let settled = render(&steps, Phase::Finished, Duration::from_secs(16 * 60));
assert_eq!(counter.flush(&settled, true, false), FlushAction::Post);
assert!(settled.contains("✅"), "{settled}");
assert!(settled.contains("detached task finished"), "{settled}");
assert_eq!(counter.posts, 2, "start row, then one settled row");
}
#[test]
fn a_refused_edit_falls_back_to_one_final_row() {
let mut counter = Counter::new();
assert_eq!(
counter.flush("running 0:05", false, true),
FlushAction::Post
);
assert_eq!(
counter.flush("running 0:10", false, true),
FlushAction::Edit
);
counter.plan.edit_refused();
assert_eq!(
counter.flush("running 0:15", false, true),
FlushAction::Skip
);
assert_eq!(counter.flush("done 0:20", true, true), FlushAction::Post);
assert_eq!(
counter.posts, 2,
"the refusal costs one final post, not a stream"
);
}
#[test]
fn a_refused_post_retries_only_when_the_turn_settles() {
let mut counter = Counter::new();
assert_eq!(
counter.flush("running 0:05", false, true),
FlushAction::Post
);
counter.plan.post_refused();
counter.posts = 0;
assert_eq!(
counter.flush("running 0:10", false, true),
FlushAction::Skip
);
assert_eq!(counter.posts, 0, "no retry on every tick");
assert_eq!(counter.flush("done 0:15", true, true), FlushAction::Post);
assert_eq!(counter.posts, 1, "the settled state still lands once");
}
#[test]
fn the_local_window_check_matches_the_outbox_window() {
assert!(within_edit_window(Duration::from_secs(0)));
assert!(within_edit_window(Duration::from_secs(14 * 60 + 59)));
assert!(!within_edit_window(Duration::from_secs(15 * 60)));
}