use std::sync::Arc;
use crate::brain::agent::{PushOrigin, QueuedUserMessage};
use crate::channels::discord::DiscordState;
fn queued(text: &str) -> QueuedUserMessage {
QueuedUserMessage {
context_text: text.to_string(),
display_text: text.to_string(),
origin: PushOrigin::Ingress,
bg_meta: None,
}
}
#[test]
fn slot_claim_is_exclusive_and_released_by_drop() {
let state = Arc::new(DiscordState::new());
let sid = uuid::Uuid::new_v4();
let guard = state
.try_begin_turn(sid)
.expect("first claim takes the slot");
assert!(
state.try_begin_turn(sid).is_none(),
"the slot is held, no double claim"
);
drop(guard);
let second = state
.try_begin_turn(sid)
.expect("the slot is free after the guard dropped");
let other = uuid::Uuid::new_v4();
let other_guard = state
.try_begin_turn(other)
.expect("a second session claims its own slot while sid is held");
assert!(
state.try_begin_turn(sid).is_none() && state.try_begin_turn(other).is_none(),
"both sessions hold their own slots at once"
);
drop(other_guard);
drop(second);
}
#[test]
fn followups_join_fifo_and_leave_no_husk() {
let state = DiscordState::new();
let sid = uuid::Uuid::new_v4();
assert!(state.drain_followups(sid).is_none());
state.enqueue_followup(sid, queued("first message"));
state.enqueue_followup(sid, queued("second message"));
let joined = state
.drain_followups(sid)
.expect("two queued items join into one");
let ctx = &joined.context_text;
let (Some(i1), Some(i2)) = (ctx.find("first message"), ctx.find("second message")) else {
panic!("joined context lost an item: {ctx:?}");
};
assert!(i1 < i2, "FIFO order kept: {ctx:?}");
assert!(state.drain_followups(sid).is_none());
}
#[tokio::test]
async fn queue_callback_drains_the_session_queue_per_session() {
let state = Arc::new(DiscordState::new());
let sid_a = uuid::Uuid::new_v4();
let sid_b = uuid::Uuid::new_v4();
state.enqueue_followup(sid_a, queued("alpha follow-up"));
state.enqueue_followup(sid_b, queued("beta follow-up"));
let cb = state.followup_queue_callback();
let drained_a = cb(sid_a).await.expect("session A had a follow-up");
assert!(drained_a.context_text.contains("alpha follow-up"));
assert!(cb(sid_a).await.is_none());
let drained_b = cb(sid_b).await.expect("session B survives A's drain");
assert!(drained_b.context_text.contains("beta follow-up"));
}
#[tokio::test]
async fn concurrent_claims_contend_for_one_slot() {
let state = Arc::new(DiscordState::new());
let sid = uuid::Uuid::new_v4();
let held = state
.try_begin_turn(sid)
.expect("the parent claims the slot first");
let mut handles = Vec::new();
for _ in 0..4 {
let s = state.clone();
handles.push(tokio::spawn(async move { s.try_begin_turn(sid).is_some() }));
}
for h in handles {
assert!(
!h.await.unwrap(),
"a held slot excludes every concurrent claimer"
);
}
drop(held);
assert!(state.try_begin_turn(sid).is_some());
}