use super::*;
#[tokio::test]
async fn completed_task_flows_into_queue() {
let bg = BackgroundTasks::new(CancellationToken::new(), Default::default());
let id = bg.spawn("reviewer".to_string(), |_cancel, _progress| async move {
BackgroundResult::Completed("done".to_string())
});
assert_eq!(id, "bg-0");
let outcome = wait_for_one(&bg).await;
assert_eq!(outcome.task_id, "bg-0");
assert_eq!(outcome.label, "reviewer");
assert_eq!(
outcome.result,
BackgroundResult::Completed("done".to_string())
);
assert!(bg.drain_completed().is_empty());
}
#[tokio::test]
async fn task_outlives_spawn_call() {
let bg = BackgroundTasks::new(CancellationToken::new(), Default::default());
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
bg.spawn("slow".to_string(), |_cancel, _progress| async move {
let _ = rx.await;
BackgroundResult::Completed("late".to_string())
});
assert_eq!(
bg.running_count(),
1,
"task should still be running after spawn returned"
);
assert!(bg.drain_completed().is_empty(), "not done yet");
tx.send(()).unwrap();
let outcome = wait_for_one(&bg).await;
assert_eq!(
outcome.result,
BackgroundResult::Completed("late".to_string())
);
assert_eq!(bg.running_count(), 0);
}
#[tokio::test]
async fn cancel_all_propagates_to_task_token() {
let bg = BackgroundTasks::new(CancellationToken::new(), Default::default());
bg.spawn("cancellable".to_string(), |cancel, _progress| async move {
cancel.cancelled().await;
BackgroundResult::Failed("cancelled".to_string())
});
bg.cancel_all();
let outcome = wait_for_one(&bg).await;
assert_eq!(
outcome.result,
BackgroundResult::Failed("cancelled".to_string())
);
}
#[tokio::test]
async fn ids_are_unique_and_monotonic() {
let bg = BackgroundTasks::new(CancellationToken::new(), Default::default());
let id0 = bg.spawn("a".to_string(), |_c, _p| async {
BackgroundResult::Completed(String::new())
});
let id1 = bg.spawn("b".to_string(), |_c, _p| async {
BackgroundResult::Completed(String::new())
});
assert_eq!(id0, "bg-0");
assert_eq!(id1, "bg-1");
}
#[test]
fn format_outcome_labels_source_and_status() {
let ok = BackgroundOutcome {
task_id: "bg-3".to_string(),
label: "reviewer".to_string(),
result: BackgroundResult::Completed("looks good".to_string()),
};
let s = format_background_outcome(&ok);
assert!(s.contains("bg-3"));
assert!(s.contains("reviewer"));
assert!(s.contains("completed"));
assert!(s.contains("looks good"));
let err = BackgroundOutcome {
task_id: "bg-4".to_string(),
label: "builder".to_string(),
result: BackgroundResult::Failed("boom".to_string()),
};
assert!(format_background_outcome(&err).contains("failed"));
}
#[test]
fn truncate_body_respects_limit() {
assert_eq!(truncate_body("hello world", 0), "");
assert_eq!(truncate_body("hello", 10), "hello");
assert_eq!(truncate_body("hello", 5), "hello");
let out = truncate_body("hello world", 5);
assert!(out.starts_with("hello "), "kept prefix: {out}");
assert!(out.contains("+6 more chars"), "marker: {out}");
let cjk = "你好世界啊"; assert_eq!(truncate_body(cjk, 5), cjk);
let cut = truncate_body(cjk, 2);
assert!(cut.starts_with("你好"));
assert!(cut.contains("+3 more chars"));
}
fn assistant_msg(text: &str, tool: &str) -> Message {
Message {
role: Role::Assistant,
content: vec![
MessageContent::Text {
text: text.to_string(),
},
MessageContent::ToolUse {
id: "tu-1".to_string(),
name: tool.to_string(),
args: serde_json::json!({}),
},
]
.into(),
}
}
fn user_msg(text: &str) -> Message {
Message {
role: Role::User,
content: vec![MessageContent::Text {
text: text.to_string(),
}]
.into(),
}
}
#[test]
fn recent_blocks_flattens_messages_and_orders() {
let msgs = vec![
user_msg("do the thing"),
assistant_msg("working on it", "read_file"),
];
let (total, recent) = recent_blocks_of(&msgs, 10, 1000);
assert_eq!(total, 3);
let kinds: Vec<BlockKind> = recent.iter().map(|b| b.kind).collect();
assert_eq!(
kinds,
vec![
BlockKind::User,
BlockKind::AssistantText,
BlockKind::ToolUse
]
);
assert_eq!(recent[2].text, "read_file");
let (_total, last2) = recent_blocks_of(&msgs, 2, 1000);
assert_eq!(last2.len(), 2);
assert_eq!(last2[0].kind, BlockKind::AssistantText);
assert_eq!(last2[1].kind, BlockKind::ToolUse);
}
#[test]
fn recent_blocks_default_limit_drops_free_form_body_keeps_tool_name() {
let msgs = vec![assistant_msg("a long assistant reply", "read_file")];
let (_total, recent) = recent_blocks_of(&msgs, 10, 0);
let text_block = recent
.iter()
.find(|b| b.kind == BlockKind::AssistantText)
.unwrap();
assert_eq!(text_block.text, "", "free-form body dropped at limit 0");
let tool_block = recent
.iter()
.find(|b| b.kind == BlockKind::ToolUse)
.unwrap();
assert_eq!(
tool_block.text, "read_file",
"tool name kept (not a free-form body)"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn peek_reads_attached_history_committed_blocks() {
use crate::session::{History, VecHistory};
let bg = BackgroundTasks::new(
CancellationToken::new(),
BackgroundProgressConfig::default(),
);
let history: Arc<dyn History> = Arc::new(VecHistory::new());
let history_for_task = history.clone();
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
let id = bg.spawn("worker".to_string(), move |_c, handle| async move {
handle.attach_history(history_for_task);
let _ = rx.await; BackgroundResult::Completed("done".to_string())
});
history.append(user_msg("task instructions"));
history.append(assistant_msg("on it", "read_file"));
let mut snap = bg.peek(&id, None).expect("task exists");
for _ in 0..200 {
if snap.block_count == 3 {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
snap = bg.peek(&id, None).expect("task exists");
}
assert_eq!(snap.status, TaskStatus::Running);
assert_eq!(snap.block_count, 3);
let kinds: Vec<BlockKind> = snap.recent.iter().map(|b| b.kind).collect();
assert_eq!(
kinds,
vec![
BlockKind::User,
BlockKind::AssistantText,
BlockKind::ToolUse
]
);
assert_eq!(snap.recent[2].text, "read_file");
let _ = tx.send(());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn peek_without_attached_history_returns_empty_blocks() {
let bg = BackgroundTasks::new(
CancellationToken::new(),
BackgroundProgressConfig::default(),
);
let id = bg.spawn("worker".to_string(), |_c, _handle| async {
BackgroundResult::Completed("done".to_string())
});
for _ in 0..200 {
if bg.peek(&id, None).map(|s| s.status) == Some(TaskStatus::Completed) {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
let snap = bg.peek(&id, None).expect("task exists");
assert_eq!(snap.block_count, 0);
assert!(snap.recent.is_empty());
}
async fn wait_for_one(bg: &BackgroundTasks) -> BackgroundOutcome {
for _ in 0..200 {
let mut done = bg.drain_completed();
if let Some(o) = done.pop() {
return o;
}
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
panic!("background task did not complete in time");
}