use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
use crate::brain::agent::service::background_tasks::BackgroundTaskManager;
use crate::brain::tools::subagent::manager::{SubAgent, SubAgentManager, SubAgentState};
use crate::channels::typing_tick::tick_while_detached;
fn child(parent: Uuid, awaiting: bool) -> SubAgent {
SubAgent {
state: if awaiting {
SubAgentState::AwaitingInput
} else {
SubAgentState::Running
},
..SubAgent::new(
Uuid::new_v4().to_string(),
"test child",
Uuid::new_v4(),
parent,
)
}
}
async fn run_tick(agents: Option<Arc<SubAgentManager>>, session: Uuid) -> usize {
run_tick_with_stop(agents, session, None).await
}
async fn run_tick_with_stop(
agents: Option<Arc<SubAgentManager>>,
session: Uuid,
stop: Option<CancellationToken>,
) -> usize {
let pings = Arc::new(AtomicUsize::new(0));
let counted = pings.clone();
let tick = tick_while_detached(
None,
agents,
session,
stop,
Duration::from_millis(10),
Duration::from_secs(5),
move || {
let pings = counted.clone();
async move {
pings.fetch_add(1, Ordering::SeqCst);
}
},
);
let _ = tokio::time::timeout(Duration::from_millis(150), tick).await;
pings.load(Ordering::SeqCst)
}
#[tokio::test]
async fn awaiting_children_do_not_hold_the_typing_tick() {
let mgr = Arc::new(SubAgentManager::new());
let session = Uuid::new_v4();
mgr.insert(child(session, true));
mgr.insert(child(session, true));
let pings = run_tick(Some(mgr), session).await;
assert_eq!(pings, 0, "awaiting-only session must not ping at all");
}
#[tokio::test]
async fn working_children_hold_the_typing_tick() {
let mgr = Arc::new(SubAgentManager::new());
let session = Uuid::new_v4();
mgr.insert(child(session, false));
let pings = run_tick(Some(mgr), session).await;
assert!(pings > 0, "a mid-round child must keep the tick alive");
}
#[tokio::test]
async fn unwired_surface_sends_nothing() {
let pings = run_tick(None, Uuid::new_v4()).await;
assert_eq!(pings, 0);
}
#[tokio::test]
async fn stop_token_ends_a_live_tick() {
let mgr = Arc::new(SubAgentManager::new());
let session = Uuid::new_v4();
mgr.insert(child(session, false));
let stop = CancellationToken::new();
let watcher = stop.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(35)).await;
watcher.cancel();
});
let pings = run_tick_with_stop(Some(mgr), session, Some(stop)).await;
assert!(pings > 0, "the tick must ping while work runs");
assert!(
pings <= 10,
"a cancelled stop must end the loop within a tick or two, got {pings}"
);
}
#[tokio::test]
async fn cancelled_stop_before_entry_sends_nothing() {
let mgr = Arc::new(SubAgentManager::new());
let session = Uuid::new_v4();
mgr.insert(child(session, false));
let stop = CancellationToken::new();
stop.cancel();
let pings = run_tick_with_stop(Some(mgr), session, Some(stop)).await;
assert_eq!(pings, 0, "an already-cancelled stop must ping nothing");
}
#[tokio::test]
async fn tick_loop_exits_at_its_ceiling() {
let bg = Arc::new(BackgroundTaskManager::new());
let session = Uuid::new_v4();
bg.mirror_started(session, "stuck row never finished");
let pings = Arc::new(AtomicUsize::new(0));
let counted = pings.clone();
let tick = tick_while_detached(
Some(bg.clone()),
None,
session,
None,
Duration::from_millis(10),
Duration::from_millis(60),
move || {
let pings = counted.clone();
async move {
pings.fetch_add(1, Ordering::SeqCst);
}
},
);
let ended_on_its_own = tokio::time::timeout(Duration::from_millis(150), tick)
.await
.is_ok();
let n = pings.load(Ordering::SeqCst);
assert!(ended_on_its_own, "the ceiling must end the loop itself");
assert!(
(1..=12).contains(&n),
"the ceiling should allow a handful of pings then stop, got {n}"
);
assert_eq!(
bg.running_for(session),
1,
"the ceiling stops the pinging, not the registry"
);
}