use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
#[derive(Default)]
struct Paused {
entered: tokio::sync::Notify,
release: AtomicBool,
}
extern "C" fn paused_handler(_: StablePluginEvent, data: *mut std::ffi::c_void) -> i32 {
let state = unsafe { Arc::from_raw(data.cast::<Paused>()) };
state.entered.notify_one();
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while !state.release.load(Ordering::Acquire) && std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(1));
}
0
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dropping_lifecycle_dispatch_preserves_its_foreign_callback_and_child_capacity() {
let state = Arc::new(Paused::default());
let mut registry = crate::ExtensionRegistry::new();
registry.register_event_handler(
"paused".into(),
EventTag::SessionStart,
paused_handler,
Arc::into_raw(state.clone()) as *mut std::ffi::c_void,
);
let snapshot = registry.snapshot();
let keepalive = crate::PluginKeepalive::empty();
let weak = Arc::downgrade(&keepalive);
let scope = Arc::new(rpi_agent::tool_execution_scope::ToolExecutionScope::default());
let task_scope = scope.clone();
let task = tokio::spawn(async move {
task_scope
.run(dispatch_lifecycle_event(
&snapshot,
EventTag::SessionStart,
keepalive,
))
.await
});
tokio::time::timeout(Duration::from_secs(2), state.entered.notified())
.await
.unwrap();
task.abort();
assert!(task.await.unwrap_err().is_cancelled());
let retained = weak.upgrade().is_some();
let occupied = tokio::time::timeout(Duration::from_millis(25), scope.wait_idle())
.await
.is_err();
state.release.store(true, Ordering::Release);
tokio::time::timeout(Duration::from_secs(2), async {
scope.wait_idle().await;
while weak.upgrade().is_some() {
tokio::time::sleep(Duration::from_millis(1)).await;
}
})
.await
.unwrap();
assert!(
retained,
"cancelling lifecycle dispatch must not unload its executing handler"
);
assert!(
occupied,
"the handler must retain child capacity until it really returns"
);
}