use std::sync::Arc;
use crate::channels::discord::DiscordState;
#[test]
fn interaction_waits_for_turn_slot() {
let state = Arc::new(DiscordState::new());
let sid = uuid::Uuid::new_v4();
let live = state
.try_begin_turn(sid)
.expect("the live turn owns the slot");
let admission = state.claim_interaction_turn(
sid,
"picked \"Run the tests\" from your select menu".to_string(),
"[System: adolfo picked \"Run the tests\"]".to_string(),
);
assert!(
admission.is_none(),
"a component interaction must not start a second loop while a turn is live"
);
let queued = state
.drain_followups(sid)
.expect("the interaction was queued for the live turn, not dropped");
assert!(
queued.context_text.contains("Run the tests"),
"the live turn must receive what the user picked, got: {}",
queued.context_text
);
assert!(
!queued.display_text.contains("select menu"),
"history keeps the compact system tag, not the prompt scaffolding, got: {}",
queued.display_text
);
drop(live);
let guard = state
.claim_interaction_turn(
sid,
"picked \"Ship it\"".to_string(),
"[System: adolfo picked \"Ship it\"]".to_string(),
)
.expect("the slot is free again after the live turn ended");
assert!(
state.drain_followups(sid).is_none(),
"a claimed interaction runs now, it is not queued"
);
assert!(
state
.claim_interaction_turn(
sid,
"modal submit".to_string(),
"[System: modal]".to_string()
)
.is_none(),
"the running interaction holds the slot until it finishes"
);
drop(guard);
}
#[test]
fn a_free_slot_lets_the_interaction_run() {
let state = Arc::new(DiscordState::new());
let sid = uuid::Uuid::new_v4();
let guard = state
.claim_interaction_turn(
sid,
"submitted the deploy form".to_string(),
"[System: adolfo submitted the \"deploy\" form]".to_string(),
)
.expect("an idle session admits the interaction");
assert!(
state.drain_followups(sid).is_none(),
"an admitted interaction has nothing to queue"
);
assert!(
state.try_begin_turn(sid).is_none(),
"the admitted interaction holds the slot, so a typed message waits too"
);
let other = uuid::Uuid::new_v4();
assert!(
state
.claim_interaction_turn(
other,
"picked x".to_string(),
"[System: picked x]".to_string()
)
.is_some(),
"sessions are independent"
);
drop(guard);
assert!(
state.try_begin_turn(sid).is_some(),
"dropping the guard frees the slot for the next turn"
);
}
#[test]
fn every_interaction_entry_point_claims_the_slot() {
let interactions = include_str!("../channels/discord/interactions.rs");
let start = interactions
.find("pub(crate) async fn route_interaction_turn(")
.expect("the bare interaction route is present");
let body = &interactions[start..];
let end = body
.find("/// Run a tapped follow-up suggestion")
.expect("the tap helper terminates the bare route");
let bare_route = &body[..end];
assert!(
bare_route.contains("claim_interaction_turn("),
"the select menu and modal submit route must claim the session turn slot (#2191)"
);
assert!(
bare_route.contains("discord_state: Arc<super::DiscordState>,"),
"the bare route needs the state the slot lives on"
);
let reactions = include_str!("../channels/discord/reactions.rs");
assert!(
reactions.contains("claim_interaction_turn("),
"a reaction must not fork a second loop onto a busy session (#2191)"
);
let agent = include_str!("../channels/discord/agent.rs");
assert_eq!(
agent
.matches("dstate.clone(),\n )")
.count(),
2,
"both synthetic callers (modal form, select menu) hand the state over; \
one short means a dispatch went back to the unclaimed shape"
);
}