use tokio_util::sync::CancellationToken;
use super::background::{begin, cfg, next, running_run, runs_out, start};
use super::background_runs::BackgroundRun;
use super::parallel::KeyedRecorder;
use super::subagent::{hang, text};
use super::*;
use crate::app::events::{BackgroundKind, TASK_LANDED_CAP, TaskList, TaskRun};
use crate::entities::chat::DeletedExchange;
use crate::entities::message::MessageRole;
use crate::entities::subagent::{RunOutcome, SubagentRun};
fn task_row(e: &AppEvent, id: Uuid) -> Option<&TaskRun> {
let AppEvent::TaskList(list) = e else {
return None;
};
list.runs.iter().find(|r| r.id == id)
}
fn last_task_list(rx: &mut UnboundedReceiver<AppEvent>) -> TaskList {
let mut last = None;
while let Ok(e) = rx.try_recv() {
if let AppEvent::TaskList(list) = e {
last = Some(*list);
}
}
last.expect("a TaskList was sent")
}
fn chat_with(title: &str, runs: Vec<SubagentRun>) -> Chat {
let profile = Profile::new("P", "sys");
let mut chat = Chat::from_profile(&profile, title);
let mut msg = Message::assistant("");
msg.tool_calls = runs.into_iter().map(SubagentRun::on_record).collect();
chat.push_message(Message::user("go"));
chat.push_message(msg);
chat
}
fn seat(run: SubagentRun, chat: Uuid) -> BackgroundRun {
BackgroundRun {
generation: Uuid::new_v4(),
run_id: run.id,
chat,
cancel: CancellationToken::new(),
child: InflightChild {
run,
stream: Uuid::new_v4(),
partial: Default::default(),
line_role: MessageRole::Assistant,
position: None,
},
}
}
#[tokio::test]
async fn a_run_out_is_a_running_row_and_lands_with_its_outcome() {
let backend = KeyedRecorder::new(
vec![
("", vec![start("c1"), text("started it"), text("noted")]),
("be harsh", vec![hang("thinking")]),
],
10,
);
let (_dir, cmd_tx, mut rx, handle, chat_id) = begin(backend, cfg(2)).await;
let run_id = running_run(&mut rx).await;
let list = next(&mut rx, |e| {
task_row(e, run_id).is_some_and(|r| r.running && r.position.is_some())
})
.await;
let row = task_row(&list, run_id).unwrap();
assert_eq!(row.parent, chat_id);
assert!(row.background, "a seat of its own");
assert_eq!(row.outcome, None);
assert_eq!(row.position.as_ref().unwrap().round, 1);
assert_eq!(row.position.as_ref().unwrap().name, "Critic");
let AppEvent::TaskList(list) = &list else {
unreachable!()
};
assert_eq!(list.app.len(), 4, "every silent task has a row");
assert!(list.app.iter().all(|t| !t.running));
cmd_tx
.send(AppCommand::StopSubagentRun { id: run_id })
.unwrap();
next(&mut rx, runs_out(0)).await;
let list = next(&mut rx, |e| task_row(e, run_id).is_some_and(|r| !r.running)).await;
let row = task_row(&list, run_id).unwrap();
assert_eq!(row.outcome, Some(RunOutcome::Cancelled));
assert!(row.finished_at.is_some());
assert_eq!(row.position, None, "a landed row has no position");
assert!(row.background);
let AppEvent::TaskList(list) = &list else {
unreachable!()
};
assert!(
list.runs.iter().all(|r| !r.running),
"nothing is running once the only run landed"
);
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
}
#[test]
fn the_landed_half_comes_from_the_live_records_and_skips_the_archive() {
let (_d, mut orch) = bare_orch();
let done = SubagentRun::fixture("Reviewer", &["q", "a"]);
let done_id = done.id;
let mut chat = chat_with("Plans", vec![done]);
let gone = SubagentRun::fixture("Gone", &["q", "a"]);
let gone_id = gone.id;
let mut gone_msg = Message::assistant("");
gone_msg.tool_calls = vec![gone.on_record()];
chat.deleted.push(DeletedExchange {
deleted_at: chrono::Utc::now(),
messages: vec![gone_msg],
draft: String::new(),
cause: None,
});
orch.chats.push(chat);
let list = orch.task_list();
assert_eq!(list.runs.len(), 1, "{:?}", list.runs);
let row = &list.runs[0];
assert_eq!(row.id, done_id);
assert_eq!(row.parent_title, "Plans");
assert!(!row.running);
assert_eq!(row.outcome, Some(RunOutcome::Completed));
assert!(!row.background, "a turn's child, not a seat");
assert!(list.runs.iter().all(|r| r.id != gone_id));
assert_eq!(list.more_landed, 0);
}
#[test]
fn landed_runs_are_newest_first_and_capped_with_a_count() {
let (_d, mut orch) = bare_orch();
let now = chrono::Utc::now();
let runs: Vec<SubagentRun> = (0..TASK_LANDED_CAP + 10)
.map(|i| {
let mut run = SubagentRun::fixture(&format!("run-{i:02}"), &["q", "a"]);
run.finished_at =
Some(now - chrono::Duration::minutes((TASK_LANDED_CAP + 10 - i) as i64));
run
})
.collect();
orch.chats.push(chat_with("Plans", runs));
let list = orch.task_list();
assert_eq!(list.runs.len(), TASK_LANDED_CAP);
assert_eq!(list.more_landed, 10);
assert_eq!(
list.runs[0].title,
format!("run-{:02}", TASK_LANDED_CAP + 9)
);
assert!(
list.runs
.windows(2)
.all(|w| w[0].finished_at >= w[1].finished_at),
"newest first"
);
}
#[test]
fn the_bar_count_and_the_running_rows_agree() {
let (_d, mut orch, mut rx) = bare_orch_rx();
let out = SubagentRun {
outcome: None,
finished_at: None,
background: true,
..SubagentRun::fixture("Critic", &["q"])
};
let ended = SubagentRun {
outcome: Some(RunOutcome::Cancelled),
background: true,
..SubagentRun::fixture("Judge", &["q", "a"])
};
let (out_id, ended_id) = (out.id, ended.id);
let placeholders = [&out, &ended]
.into_iter()
.map(|r| SubagentRun {
outcome: None,
finished_at: None,
messages: Vec::new(),
..r.clone()
})
.collect();
let chat = chat_with("Space talk", placeholders);
let chat_id = chat.id;
orch.chats.push(chat);
orch.background_runs.push(seat(out, chat_id));
orch.background_runs.push(seat(ended, chat_id));
orch.emit_background_runs();
let mut bar = None;
while let Ok(e) = rx.try_recv() {
if let AppEvent::BackgroundRuns { out } = e {
bar = Some(out);
}
}
let list = orch.task_list();
let running: Vec<&TaskRun> = list.runs.iter().filter(|r| r.running).collect();
assert_eq!(bar, Some(running.len() as u32));
assert_eq!(running.len(), 1);
assert_eq!(running[0].id, out_id);
assert!(running[0].background);
let ended_rows: Vec<&TaskRun> = list.runs.iter().filter(|r| r.id == ended_id).collect();
assert_eq!(ended_rows.len(), 1, "one row per run, the mirror's");
assert_eq!(ended_rows[0].outcome, Some(RunOutcome::Cancelled));
assert!(!ended_rows[0].running);
assert_eq!(list.runs.len(), 2);
}
#[test]
fn a_request_is_answered_and_the_silent_tasks_are_followed() {
let (_d, mut orch, mut rx) = bare_orch_rx();
orch.handle_command(AppCommand::RequestTasks);
let list = last_task_list(&mut rx);
assert!(list.runs.is_empty());
assert_eq!(list.app.len(), 4);
assert!(list.app.iter().all(|t| !t.running));
orch.begin_bg(BackgroundKind::Reflection, CancellationToken::new(), None);
let list = last_task_list(&mut rx);
let reflection = list
.app
.iter()
.find(|t| t.kind == BackgroundKind::Reflection)
.unwrap();
assert!(reflection.running);
assert_eq!(list.app.iter().filter(|t| t.running).count(), 1);
orch.handle_bg_done(
BackgroundKind::Reflection,
super::super::background::BgOutcome::Done,
None,
);
let list = last_task_list(&mut rx);
assert!(list.app.iter().all(|t| !t.running));
}