use crate::brain::agent::ProgressEvent;
use crate::channels::telegram::flow::{
DisplayItem, FlowOutcome, StreamingState, SubagentCounts, row_display_name,
};
use crate::channels::telegram::progress::apply_progress_event;
use std::sync::{Arc, Mutex};
use teloxide::types::MessageId;
fn base_state() -> Arc<Mutex<StreamingState>> {
Arc::new(Mutex::new(StreamingState {
is_dm: false,
pending_suggestions: None,
pending_trailer: None,
delivered_files: Vec::new(),
media_intermediates: Vec::new(),
msg_id: None,
thinking: String::new(),
tool_msgs: Vec::new(),
display_queue: Vec::new(),
open_group_msg_id: None,
rich_transport_failures: 0,
flow_entries: Vec::new(),
flow_status: None,
flow_rich: false,
response: String::new(),
dirty: false,
recreate: false,
header_preview: None,
compacting: false,
sections: Default::default(),
retained_goal: None,
tool_round_count: 0,
tools_started_at: None,
turn_started_at: std::time::Instant::now(),
flow_outcome: Some(FlowOutcome::Finished),
bg_indicator: None,
bg_count: None,
subagent_counts: SubagentCounts {
working: 0,
awaiting: 0,
},
sent_intermediates: Vec::new(),
intermediate_bubbles: Vec::new(),
voice_msg_ids: Vec::new(),
applied_plan_kb: Default::default(),
processing: false,
final_bubble: None,
is_cli: false,
}))
}
fn start(id: &str, label: &str) -> ProgressEvent {
ProgressEvent::DetachedTaskStarted {
task_id: id.to_string(),
label: label.to_string(),
}
}
fn finish(id: &str, label: &str, success: bool) -> ProgressEvent {
ProgressEvent::DetachedTaskFinished {
task_id: id.to_string(),
label: label.to_string(),
success,
elapsed_secs: 42.0,
}
}
#[test]
fn detached_start_draws_one_row_with_its_task_id() {
let st = base_state();
apply_progress_event(&st, start("t-1", "cargo test"));
let s = st.lock().unwrap();
assert_eq!(s.tool_msgs.len(), 1, "a start draws exactly one row");
let row = &s.tool_msgs[0];
assert_eq!(row.task_id.as_deref(), Some("t-1"));
assert_eq!(row.name, "cargo test");
assert_eq!(row.completed, None, "a fresh detached row is running");
assert!(row.dirty, "the new row must be rendered");
assert!(
matches!(s.display_queue.as_slice(), [DisplayItem::NewTool(0)]),
"the start queues the send for that one row, got {:?}",
s.display_queue.len()
);
}
#[test]
fn detached_finish_settles_the_same_row_and_queues_no_new_message() {
let st = base_state();
apply_progress_event(&st, start("t-1", "cargo test"));
let sent_id = MessageId(777);
{
let mut s = st.lock().unwrap();
s.tool_msgs[0].msg_id = Some(sent_id);
s.tool_msgs[0].dirty = false;
s.display_queue.clear();
}
apply_progress_event(&st, finish("t-1", "cargo test", true));
let s = st.lock().unwrap();
assert_eq!(
s.tool_msgs.len(),
1,
"the finish must settle the existing row, never append a second one"
);
assert_eq!(
s.tool_msgs[0].msg_id,
Some(sent_id),
"the row keeps the message id it was sent with, so the edit is in place"
);
assert_eq!(
s.tool_msgs[0].completed,
Some(true),
"the row carries the outcome"
);
assert!(s.tool_msgs[0].dirty, "the settled row must be re-rendered");
assert!(
s.display_queue.is_empty(),
"a finish queues no new message; a display item here would post a second bubble"
);
}
#[test]
fn detached_finish_with_no_matching_row_queues_nothing() {
let st = base_state();
apply_progress_event(&st, finish("t-404", "cargo test", false));
let s = st.lock().unwrap();
assert!(s.tool_msgs.is_empty(), "no row to settle, none invented");
assert!(
s.display_queue.is_empty(),
"an orphan finish must not open a new block"
);
}
#[test]
fn two_detached_tasks_settle_by_their_own_id() {
let st = base_state();
apply_progress_event(&st, start("t-1", "cargo test"));
apply_progress_event(&st, start("t-2", "cargo test"));
apply_progress_event(&st, finish("t-2", "cargo test", false));
let s = st.lock().unwrap();
assert_eq!(s.tool_msgs.len(), 2, "both rows exist");
assert_eq!(
s.tool_msgs[0].completed, None,
"the first task is still running and must not be settled by the second"
);
assert_eq!(
s.tool_msgs[1].completed,
Some(false),
"the matching row carries its own outcome"
);
}
#[test]
fn row_display_name_marks_detached_rows_only() {
assert_eq!(
row_display_name("read_file", None, Some(true)),
"read_file",
"an ordinary tool call keeps its bare name"
);
assert_eq!(
row_display_name("cargo test", Some("t-1"), None),
"detached task running: cargo test"
);
assert_eq!(
row_display_name("cargo test", Some("t-1"), Some(true)),
"detached task finished: cargo test"
);
assert_eq!(
row_display_name("cargo test", Some("t-1"), Some(false)),
"detached task failed: cargo test"
);
}