use core::fmt;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use crate::domain::RequestId;
use crate::domain::session::{
AbortReason, CancelOutcome, CancelReason, CancellationToken, DuplicateRequest, InFlight,
LifecycleError, LifecycleEvent, ProgressToken, RequestOrigin, RequestRegistry, Session,
SessionId, SessionState,
};
use super::clock::Clock;
use super::event_log::{EventLog, SessionEvent};
use super::id_source::IdSource;
use super::lifecycle::{Classification, Declaration, DeclarationError, Lifecycle, Resolution};
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum EngineError {
#[error("the peer could not be answered: {0}")]
Declaration(#[from] DeclarationError),
#[error(transparent)]
Lifecycle(#[from] LifecycleError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Refusal {
SessionNotAccepting {
state: SessionState,
},
NotPermitted {
state: SessionState,
},
HandshakeIncomplete {
state: SessionState,
},
Unnegotiated {
state: SessionState,
},
}
impl Refusal {
#[must_use]
pub const fn state(&self) -> SessionState {
match self {
Self::SessionNotAccepting { state }
| Self::NotPermitted { state }
| Self::HandshakeIncomplete { state }
| Self::Unnegotiated { state } => *state,
}
}
}
impl fmt::Display for Refusal {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = self.state();
match self {
Self::SessionNotAccepting { .. } => {
write!(formatter, "the session is {state} and admits nothing")
}
Self::NotPermitted { .. } => {
write!(formatter, "the method is not permitted while {state}")
}
Self::HandshakeIncomplete { .. } => {
write!(
formatter,
"the session is {state} and the handshake is unfinished"
)
}
Self::Unnegotiated { .. } => {
write!(
formatter,
"nothing is negotiated yet and the session is {state}"
)
}
}
}
}
impl std::error::Error for Refusal {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Dispatch<T> {
Completed(T),
Aborted {
discarded: T,
reason: AbortReason,
},
}
impl<T> Dispatch<T> {
#[must_use]
pub fn into_inner(self) -> T {
match self {
Self::Completed(value)
| Self::Aborted {
discarded: value, ..
} => value,
}
}
#[must_use]
pub const fn is_completed(&self) -> bool {
matches!(self, Self::Completed(_))
}
}
pub struct SessionEngine<L, C, G>
where
L: Lifecycle,
{
lifecycle: L,
clock: C,
log: G,
session: Session,
registry: RequestRegistry,
resolved: Option<Resolution<L::Profile, L::Version>>,
}
impl<L, C, G> fmt::Debug for SessionEngine<L, C, G>
where
L: Lifecycle,
C: fmt::Debug,
G: fmt::Debug,
{
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SessionEngine")
.field("id", &self.session.id())
.field("state", &self.session.state())
.field("in_flight", &self.registry.len())
.field("negotiated", &self.resolved.is_some())
.finish_non_exhaustive()
}
}
impl<L, C, G> SessionEngine<L, C, G>
where
L: Lifecycle,
C: Clock,
G: EventLog,
{
#[must_use]
pub fn open<J: IdSource>(lifecycle: L, clock: C, ids: &J, log: G) -> Self {
let session = Session::new(ids.next_session_id(), clock.now());
log.record(SessionEvent::Opened {
session: session.id(),
at: session.opened_at(),
});
Self {
lifecycle,
clock,
log,
session,
registry: RequestRegistry::new(),
resolved: None,
}
}
#[must_use]
pub fn open_stateless<J: IdSource>(lifecycle: L, clock: C, ids: &J, log: G) -> Self {
let session = Session::stateless(ids.next_session_id(), clock.now());
log.record(SessionEvent::Opened {
session: session.id(),
at: session.opened_at(),
});
Self {
lifecycle,
clock,
log,
session,
registry: RequestRegistry::new(),
resolved: None,
}
}
#[must_use]
pub const fn id(&self) -> SessionId {
self.session.id()
}
#[must_use]
pub const fn state(&self) -> SessionState {
self.session.state()
}
#[must_use]
pub const fn resolution(&self) -> Option<Resolution<L::Profile, L::Version>> {
self.resolved
}
#[must_use]
pub const fn lifecycle(&self) -> &L {
&self.lifecycle
}
#[must_use]
pub fn in_flight(&self) -> usize {
self.registry.len()
}
pub fn in_flight_ids(&self) -> impl Iterator<Item = &RequestId> {
self.registry.ids()
}
#[must_use]
pub fn request(&self, id: &RequestId) -> Option<&InFlight> {
self.registry.get(id)
}
pub fn admit(&self, method: &str, id: Option<&RequestId>) -> Result<Classification, Refusal> {
let state = self.session.state();
if state.is_terminal() || state == SessionState::Closing {
return Err(self.refuse(Refusal::SessionNotAccepting { state }, method, id));
}
let profile = self.resolved.map(|resolved| resolved.profile);
match self.lifecycle.classify(state, profile, method) {
Classification::Refused => {
Err(self.refuse(Refusal::NotPermitted { state }, method, id))
}
classification
@ (Classification::HandshakeStart | Classification::HandshakeConfirm) => {
Ok(classification)
}
Classification::Ordinary if !state.admits_ordinary_work() => {
Err(self.refuse(Refusal::HandshakeIncomplete { state }, method, id))
}
Classification::Ordinary if self.resolved.is_none() => {
Err(self.refuse(Refusal::Unnegotiated { state }, method, id))
}
Classification::Ordinary => Ok(Classification::Ordinary),
}
}
pub fn negotiate(
&mut self,
declaration: Declaration<'_>,
) -> Result<Resolution<L::Profile, L::Version>, EngineError> {
let resolution = self.lifecycle.resolve(declaration)?;
if !self.session.state().is_stateless() {
self.transition(LifecycleEvent::HandshakeStarted)?;
}
let version = resolution.version.to_string();
self.log.record(SessionEvent::Negotiated {
session: self.session.id(),
at: self.clock.now(),
version,
echoed: resolution.echoed,
});
self.resolved = Some(resolution);
Ok(resolution)
}
pub fn confirm(&mut self) -> Result<(), EngineError> {
self.transition(LifecycleEvent::HandshakeConfirmed)?;
self.log.record(SessionEvent::Operating {
session: self.session.id(),
at: self.clock.now(),
});
Ok(())
}
pub fn close(&mut self) -> Result<Vec<RequestId>, EngineError> {
self.transition(LifecycleEvent::CloseRequested)?;
Ok(self.abandon(&AbortReason::SessionClosed))
}
pub fn finish(&mut self) -> Result<(), EngineError> {
self.transition(LifecycleEvent::Finished)
}
pub fn register(
&mut self,
id: RequestId,
origin: RequestOrigin,
method: &str,
token: Option<ProgressToken>,
budget: Option<Duration>,
) -> Result<CancellationToken, DuplicateRequest> {
let entry = InFlight::new(origin, method, token, budget, self.clock.now());
let handle = entry.cancellation_handle();
let method = Arc::from(method);
self.registry.register(id.clone(), entry)?;
self.log.record(SessionEvent::Registered {
session: self.session.id(),
at: self.clock.now(),
id,
method,
origin,
});
Ok(handle)
}
#[must_use]
pub fn cancellation(&self, id: &RequestId) -> Option<CancellationToken> {
self.registry.get(id).map(InFlight::cancellation_handle)
}
pub fn cancel(&mut self, id: &RequestId, reason: Option<CancelReason>) -> CancelOutcome {
let outcome = self.registry.cancel(id, reason.clone());
if outcome.cancelled() {
self.log_abort(id, AbortReason::Cancelled(reason));
}
outcome
}
pub fn progress(&mut self, id: &RequestId, progress: f64, message: Option<String>) -> bool {
self.registry
.get_mut(id)
.is_some_and(|entry| entry.advance_progress(progress, message))
}
pub fn expire(&mut self) -> Vec<RequestId> {
let expired = self.registry.expire(self.clock.now());
for id in &expired {
self.log_abort(id, AbortReason::Expired);
}
expired
}
pub fn complete(&mut self, id: &RequestId) -> Option<Option<AbortReason>> {
let entry = self.registry.complete(id)?;
let reason = entry.cancellation().reason().cloned();
let method = Arc::from(entry.method());
let at = self.clock.now();
match reason.as_ref() {
Some(reason) => self.log.record(SessionEvent::Aborted {
session: self.session.id(),
at,
id: id.clone(),
method,
reason: reason.clone(),
}),
None => self.log.record(SessionEvent::Completed {
session: self.session.id(),
at,
id: id.clone(),
method,
}),
}
Some(reason)
}
pub async fn dispatch<H, T>(
&mut self,
id: RequestId,
origin: RequestOrigin,
method: &str,
token: Option<ProgressToken>,
budget: Option<Duration>,
handler: H,
) -> Result<Dispatch<T>, DuplicateRequest>
where
H: AsyncFnOnce(CancellationToken) -> T,
{
let handle = self.register(id.clone(), origin, method, token, budget)?;
let produced: T = handler(handle).await;
Ok(match self.complete(&id) {
None => Dispatch::Aborted {
discarded: produced,
reason: AbortReason::SessionClosed,
},
Some(None) => Dispatch::Completed(produced),
Some(Some(reason)) => Dispatch::Aborted {
discarded: produced,
reason,
},
})
}
fn transition(&mut self, event: LifecycleEvent) -> Result<(), EngineError> {
self.session = self.session.apply(event)?;
self.log.record(SessionEvent::StateChanged {
session: self.session.id(),
at: self.clock.now(),
state: self.session.state(),
});
Ok(())
}
fn refuse(&self, refusal: Refusal, method: &str, id: Option<&RequestId>) -> Refusal {
self.log.record(SessionEvent::Refused {
session: self.session.id(),
at: self.clock.now(),
id: id.cloned(),
method: Arc::from(method),
state: refusal.state(),
});
refusal
}
fn log_abort(&self, id: &RequestId, fallback: AbortReason) {
let Some(entry) = self.registry.get(id) else {
return;
};
let reason = entry.cancellation().reason().cloned().unwrap_or(fallback);
self.log.record(SessionEvent::Aborted {
session: self.session.id(),
at: self.clock.now(),
id: id.clone(),
method: Arc::from(entry.method()),
reason,
});
}
fn abandon(&mut self, reason: &AbortReason) -> Vec<RequestId> {
let recorded: Vec<(RequestId, Arc<str>)> = self
.registry
.ids()
.map(|id| {
let method = self.registry.get(id).map_or("", InFlight::method);
(id.clone(), Arc::from(method))
})
.collect();
let ids = self.registry.drain();
let at = self.clock.now();
let session = self.session.id();
for (id, method) in recorded {
self.log.record(SessionEvent::Aborted {
session,
at,
id,
method,
reason: reason.clone(),
});
}
ids
}
}
#[cfg(test)]
mod tests {
use core::sync::atomic::{AtomicU64, Ordering};
use core::time::Duration;
use serde_json::json;
use super::*;
use crate::application::session::lifecycle::MetaView;
use crate::domain::session::{Instant, SessionId};
#[derive(Debug)]
#[expect(dead_code, reason = "the payloads are read by the derived Debug")]
enum Failure {
Engine(EngineError),
Refusal(Refusal),
Duplicate(DuplicateRequest),
Suspended,
TimedOut(&'static str),
NotAborted(String),
}
impl From<EngineError> for Failure {
fn from(error: EngineError) -> Self {
Self::Engine(error)
}
}
impl From<Refusal> for Failure {
fn from(refusal: Refusal) -> Self {
Self::Refusal(refusal)
}
}
impl From<DuplicateRequest> for Failure {
fn from(duplicate: DuplicateRequest) -> Self {
Self::Duplicate(duplicate)
}
}
struct Open;
impl Lifecycle for Open {
type Profile = ();
type Version = u16;
fn classify(
&self,
_state: SessionState,
_profile: Option<()>,
method: &str,
) -> Classification {
match method {
"initialize" => Classification::HandshakeStart,
"notifications/initialized" => Classification::HandshakeConfirm,
_ => Classification::Ordinary,
}
}
fn resolve(
&self,
declaration: Declaration<'_>,
) -> Result<Resolution<(), u16>, DeclarationError> {
let version = match declaration {
Declaration::Handshake { version, .. } => {
u16::try_from(version.len()).unwrap_or(u16::MAX)
}
Declaration::Stateless { .. } => 1,
};
Ok(Resolution {
profile: (),
version,
echoed: true,
})
}
}
struct Strict;
impl Lifecycle for Strict {
type Profile = bool;
type Version = u16;
fn classify(
&self,
_state: SessionState,
profile: Option<bool>,
_method: &str,
) -> Classification {
match profile {
Some(true) => Classification::Ordinary,
_ => Classification::Refused,
}
}
fn resolve(
&self,
declaration: Declaration<'_>,
) -> Result<Resolution<bool, u16>, DeclarationError> {
let Declaration::Handshake { version, .. } = declaration else {
return Err(DeclarationError::NoVersion);
};
Ok(Resolution {
profile: true,
version: u16::try_from(version.len()).unwrap_or(u16::MAX),
echoed: true,
})
}
}
struct Fixed(Instant);
impl Clock for Fixed {
fn now(&self) -> Instant {
self.0
}
}
struct Stepping {
now: AtomicU64,
}
impl Clock for &Stepping {
fn now(&self) -> Instant {
<Stepping as Clock>::now(self)
}
}
impl Stepping {
fn new() -> Self {
Self {
now: AtomicU64::new(0),
}
}
fn advance(&self, by: Duration) {
let millis = u64::try_from(by.as_millis()).unwrap_or(u64::MAX);
self.now.fetch_add(millis, Ordering::Relaxed);
}
}
impl Clock for Stepping {
fn now(&self) -> Instant {
Instant::from_duration(Duration::from_millis(self.now.load(Ordering::Relaxed)))
}
}
struct Counting;
impl IdSource for Counting {
fn next_session_id(&self) -> SessionId {
SessionId::new(1)
}
}
#[derive(Default)]
struct Recording {
events: core::cell::RefCell<Vec<SessionEvent>>,
}
impl EventLog for Recording {
fn record(&self, event: SessionEvent) {
self.events.borrow_mut().push(event);
}
}
impl Recording {
fn kinds(&self) -> Vec<&'static str> {
self.events
.borrow()
.iter()
.map(|event| match event {
SessionEvent::Opened { .. } => "opened",
SessionEvent::Negotiated { .. } => "negotiated",
SessionEvent::Operating { .. } => "operating",
SessionEvent::Registered { .. } => "registered",
SessionEvent::Completed { .. } => "completed",
SessionEvent::Aborted { .. } => "aborted",
SessionEvent::Refused { .. } => "refused",
SessionEvent::StateChanged { .. } => "state-changed",
})
.collect()
}
}
fn engine() -> SessionEngine<Open, Fixed, Recording> {
SessionEngine::open(
Open,
Fixed(Instant::from_duration(Duration::ZERO)),
&Counting,
Recording::default(),
)
}
fn negotiated() -> Result<SessionEngine<Open, Fixed, Recording>, Failure> {
let mut engine = engine();
engine.negotiate(Declaration::Handshake {
version: "2025-06-18",
capabilities: Some(&json!({})),
})?;
engine.confirm()?;
Ok(engine)
}
#[test]
fn a_new_session_admits_nothing_until_the_handshake_settles() {
let engine = engine();
assert_eq!(engine.state(), SessionState::Uninitialised);
assert_eq!(
engine.admit("tools/list", None),
Err(Refusal::HandshakeIncomplete {
state: SessionState::Uninitialised
})
);
assert!(engine.resolution().is_none());
}
#[test]
fn the_handshake_messages_pass_the_gate_that_ordinary_work_fails() -> Result<(), Failure> {
let mut engine = engine();
assert_eq!(
engine.admit("initialize", None)?,
Classification::HandshakeStart,
"the handshake is always admitted"
);
engine.negotiate(Declaration::Handshake {
version: "2025-06-18",
capabilities: None,
})?;
assert_eq!(
engine.admit("notifications/initialized", None)?,
Classification::HandshakeConfirm,
"the confirmation is admitted too"
);
Ok(())
}
#[test]
fn the_handshake_moves_the_session_to_operating() -> Result<(), Failure> {
let engine = negotiated()?;
assert_eq!(engine.state(), SessionState::Operating);
assert_eq!(engine.admit("tools/list", None)?, Classification::Ordinary);
Ok(())
}
#[test]
fn the_lifecycle_decides_admission_not_just_the_phase() -> Result<(), Failure> {
let mut engine = SessionEngine::open(
Strict,
Fixed(Instant::from_duration(Duration::ZERO)),
&Counting,
Recording::default(),
);
assert_eq!(
engine.admit("tools/list", None),
Err(Refusal::NotPermitted {
state: SessionState::Uninitialised
}),
"the protocol's own veto, not the phase gate"
);
engine.negotiate(Declaration::Handshake {
version: "2025-06-18",
capabilities: None,
})?;
engine.confirm()?;
assert_eq!(engine.admit("tools/list", None)?, Classification::Ordinary);
Ok(())
}
#[test]
fn a_stateless_session_never_waits_for_a_handshake() -> Result<(), Failure> {
let mut engine = SessionEngine::open_stateless(
Open,
Fixed(Instant::from_duration(Duration::ZERO)),
&Counting,
Recording::default(),
);
assert!(engine.state().is_stateless());
engine.negotiate(Declaration::Stateless {
meta: MetaView::new(None),
})?;
assert!(engine.state().is_stateless());
Ok(())
}
#[test]
fn a_duplicate_in_flight_id_is_refused_and_leaves_the_first_alone() -> Result<(), Failure> {
let mut engine = negotiated()?;
let id = RequestId::text("a");
let Ok(_) = engine.register(id.clone(), RequestOrigin::Inbound, "tools/call", None, None)
else {
return Err(Failure::Suspended);
};
assert_eq!(
engine
.register(id.clone(), RequestOrigin::Inbound, "tools/call", None, None)
.map(|_| ()),
Err(DuplicateRequest { id }),
"the id is already in flight"
);
assert_eq!(engine.in_flight(), 1, "the original entry survives");
Ok(())
}
#[test]
fn cancelling_an_unknown_or_finished_request_is_a_no_op() -> Result<(), Failure> {
let mut engine = negotiated()?;
let outcome = engine.cancel(&RequestId::text("never-seen"), None);
assert!(!outcome.cancelled());
assert_eq!(engine.in_flight(), 0);
Ok(())
}
#[test]
fn cancelling_an_in_flight_request_is_visible_to_its_handler() -> Result<(), Failure> {
let mut engine = negotiated()?;
let id = RequestId::text("a");
let Ok(token) =
engine.register(id.clone(), RequestOrigin::Inbound, "tools/call", None, None)
else {
return Err(Failure::Suspended);
};
assert!(!token.is_cancelled());
let outcome = engine.cancel(&id, None);
assert!(outcome.cancelled());
assert!(token.is_cancelled(), "the handler observes the abort");
assert_eq!(engine.in_flight(), 1);
assert_eq!(
engine.complete(&id),
Some(Some(AbortReason::Cancelled(None)))
);
assert_eq!(engine.in_flight(), 0);
Ok(())
}
#[test]
fn a_request_that_ran_to_completion_is_reported_as_completed() -> Result<(), Failure> {
let mut engine = negotiated()?;
let id = RequestId::text("a");
let _ = engine.register(id.clone(), RequestOrigin::Inbound, "tools/call", None, None)?;
assert_eq!(engine.complete(&id), Some(None), "no abort reason");
assert_eq!(engine.complete(&id), None, "already finished");
Ok(())
}
#[test]
fn a_cancelled_request_reports_an_abort_rather_than_a_completion() -> Result<(), Failure> {
let mut engine = negotiated()?;
let id = RequestId::text("a");
let _ = engine.register(id.clone(), RequestOrigin::Inbound, "tools/call", None, None)?;
engine.cancel(&id, None);
assert!(matches!(
engine.complete(&id),
Some(Some(AbortReason::Cancelled(_)))
));
Ok(())
}
#[tokio::test(start_paused = true)]
async fn a_cancelled_request_answers_nothing_even_when_the_handler_returns()
-> Result<(), Failure> {
use crate::domain::session::CancelReason;
let mut engine = negotiated()?;
let id = RequestId::text("a");
let dispatched = engine.dispatch(
id,
RequestOrigin::Inbound,
"tools/call",
None,
None,
|signal: CancellationToken| async move {
let peer = signal.clone();
let canceller = tokio::spawn(async move {
peer.cancel(AbortReason::Cancelled(Some(CancelReason::new("peer"))));
});
signal.cancelled().await;
let _ = canceller.await;
"a value the peer will never receive"
},
);
let outcome = tokio::time::timeout(Duration::from_secs(5), dispatched)
.await
.map_err(|_| Failure::TimedOut("the handler was parked, not woken"))??;
let Dispatch::Aborted { discarded, reason } = outcome else {
return Err(Failure::NotAborted(format!("{outcome:?}")));
};
assert_eq!(discarded, "a value the peer will never receive");
assert!(matches!(reason, AbortReason::Cancelled(_)), "{reason:?}");
Ok(())
}
#[tokio::test(start_paused = true)]
async fn a_spent_budget_wakes_a_handler_parked_on_its_handle() -> Result<(), Failure> {
let clock = &Stepping::new();
let mut engine = SessionEngine::open(Open, clock, &Counting, Recording::default());
engine.negotiate(Declaration::Handshake {
version: "2025-06-18",
capabilities: None,
})?;
engine.confirm()?;
let id = RequestId::text("a");
let handle = engine.register(
id.clone(),
RequestOrigin::Inbound,
"tools/call",
None,
Some(Duration::from_millis(50)),
)?;
let waiter = tokio::spawn({
let handle = handle.clone();
async move {
handle.cancelled().await;
handle.reason().cloned()
}
});
tokio::task::yield_now().await;
clock.advance(Duration::from_millis(25));
assert!(
engine.expire().is_empty(),
"a budget spent only half way is not spent"
);
clock.advance(Duration::from_millis(25));
assert_eq!(engine.expire(), vec![id], "the budget is now spent");
let reason = tokio::time::timeout(Duration::from_secs(5), waiter)
.await
.map_err(|_| Failure::TimedOut("the expiry never woke the parked task"))?
.map_err(|panic| Failure::NotAborted(format!("the parked task panicked: {panic}")))?;
assert_eq!(
reason,
Some(AbortReason::Expired),
"the handler woke for the reason the engine recorded"
);
Ok(())
}
#[test]
fn a_spent_budget_expires_and_nothing_else_does() -> Result<(), Failure> {
let clock = &Stepping::new();
let mut engine = SessionEngine::open(Open, clock, &Counting, Recording::default());
engine.negotiate(Declaration::Handshake {
version: "2025-06-18",
capabilities: None,
})?;
engine.confirm()?;
let quick = RequestId::text("quick");
let slow = RequestId::text("slow");
let _ = engine.register(
quick.clone(),
RequestOrigin::Inbound,
"tools/call",
None,
Some(Duration::from_millis(50)),
)?;
let _ = engine.register(
slow.clone(),
RequestOrigin::Inbound,
"tools/call",
None,
Some(Duration::from_millis(5_000)),
)?;
clock.advance(Duration::from_millis(100));
let expired = engine.expire();
assert_eq!(expired, vec![quick], "only the spent budget expires");
assert!(
engine.request(&slow).is_some(),
"the other is still running"
);
Ok(())
}
#[test]
fn closing_aborts_everything_still_in_flight() -> Result<(), Failure> {
let mut engine = negotiated()?;
let _ = engine.register(
RequestId::text("a"),
RequestOrigin::Outbound,
"sampling/createMessage",
None,
None,
)?;
let _ = engine.register(
RequestId::text("b"),
RequestOrigin::Outbound,
"elicitation/create",
None,
None,
)?;
assert_eq!(engine.in_flight(), 2);
engine.close()?;
assert_eq!(engine.in_flight(), 0, "nothing is left to leak");
assert_eq!(
engine.state(),
SessionState::Closing,
"closing is not yet terminal — the peer still has to finish"
);
engine.finish()?;
assert!(engine.state().is_terminal());
assert!(
engine.admit("tools/list", None).is_err(),
"a finished session admits nothing"
);
Ok(())
}
#[test]
fn dispatch_registers_runs_and_completes() -> Result<(), Failure> {
let mut engine = negotiated()?;
let outcome = futures_lite_block_on(engine.dispatch(
RequestId::text("a"),
RequestOrigin::Inbound,
"tools/call",
None,
None,
|_token| async { "ok" },
))
.ok_or(Failure::Suspended)??;
assert!(outcome.is_completed(), "the handler ran to the end");
assert_eq!(outcome.into_inner(), "ok");
assert_eq!(engine.in_flight(), 0);
assert_eq!(
engine.log.kinds(),
[
"opened",
"state-changed",
"negotiated",
"state-changed",
"operating",
"registered",
"completed",
],
"the log is a transcript: every transition, then the work"
);
Ok(())
}
#[test]
fn every_recorded_event_names_the_session_and_when_it_happened() {
let engine = engine();
let events = engine.log.events.borrow();
for event in events.iter() {
assert_eq!(event.session(), engine.id());
assert_eq!(event.at(), Instant::from_duration(Duration::ZERO));
}
assert!(!events.is_empty(), "opening records at least one event");
}
#[test]
fn a_refusal_is_recorded_before_it_is_returned() {
let engine = engine();
let id = RequestId::text("a");
assert!(engine.admit("tools/call", Some(&id)).is_err());
assert_eq!(engine.log.kinds(), ["opened", "refused"]);
}
#[test]
fn a_cancelled_dispatch_sends_no_response() -> Result<(), Failure> {
let mut engine = negotiated()?;
let outcome = futures_lite_block_on(engine.dispatch(
RequestId::text("a"),
RequestOrigin::Inbound,
"tools/call",
None,
None,
|token| async move {
let _ = engine_cancel(&token);
"should not be sent"
},
))
.ok_or(Failure::Suspended)??;
assert!(
matches!(outcome, Dispatch::Aborted { .. }),
"an aborted request is never answered"
);
assert!(!outcome.is_completed(), "an abort is not a completion");
assert_eq!(
outcome.into_inner(),
"should not be sent",
"the value is still handed back, for the log rather than the wire"
);
Ok(())
}
fn engine_cancel(token: &CancellationToken) -> bool {
token.cancel(AbortReason::Cancelled(None))
}
fn futures_lite_block_on<F: core::future::Future>(future: F) -> Option<F::Output> {
use core::task::{Context, Poll};
use std::task::Waker;
let mut context = Context::from_waker(Waker::noop());
let mut future = Box::pin(future);
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => Some(value),
Poll::Pending => None,
}
}
}