1use core::fmt;
26use std::sync::Arc;
27use std::time::Duration;
28
29use thiserror::Error;
30
31use crate::domain::RequestId;
32use crate::domain::session::{
33 AbortReason, CancelOutcome, CancelReason, CancellationToken, DuplicateRequest, InFlight,
34 LifecycleError, LifecycleEvent, ProgressToken, RequestOrigin, RequestRegistry, Session,
35 SessionId, SessionState,
36};
37
38use super::clock::Clock;
39use super::event_log::{EventLog, SessionEvent};
40use super::id_source::IdSource;
41use super::lifecycle::{Classification, Declaration, DeclarationError, Lifecycle, Resolution};
42
43#[derive(Debug, Clone, PartialEq, Eq, Error)]
45pub enum EngineError {
46 #[error("the peer could not be answered: {0}")]
48 Declaration(#[from] DeclarationError),
49 #[error(transparent)]
51 Lifecycle(#[from] LifecycleError),
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum Refusal {
63 SessionNotAccepting {
68 state: SessionState,
70 },
71 NotPermitted {
73 state: SessionState,
75 },
76 HandshakeIncomplete {
79 state: SessionState,
81 },
82 Unnegotiated {
89 state: SessionState,
91 },
92}
93
94impl Refusal {
95 #[must_use]
97 pub const fn state(&self) -> SessionState {
98 match self {
99 Self::SessionNotAccepting { state }
100 | Self::NotPermitted { state }
101 | Self::HandshakeIncomplete { state }
102 | Self::Unnegotiated { state } => *state,
103 }
104 }
105}
106
107impl fmt::Display for Refusal {
108 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
109 let state = self.state();
110 match self {
111 Self::SessionNotAccepting { .. } => {
112 write!(formatter, "the session is {state} and admits nothing")
113 }
114 Self::NotPermitted { .. } => {
115 write!(formatter, "the method is not permitted while {state}")
116 }
117 Self::HandshakeIncomplete { .. } => {
118 write!(
119 formatter,
120 "the session is {state} and the handshake is unfinished"
121 )
122 }
123 Self::Unnegotiated { .. } => {
124 write!(
125 formatter,
126 "nothing is negotiated yet and the session is {state}"
127 )
128 }
129 }
130 }
131}
132
133impl std::error::Error for Refusal {}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum Dispatch<T> {
142 Completed(T),
144 Aborted {
151 discarded: T,
153 reason: AbortReason,
155 },
156}
157
158impl<T> Dispatch<T> {
159 #[must_use]
161 pub fn into_inner(self) -> T {
162 match self {
163 Self::Completed(value)
164 | Self::Aborted {
165 discarded: value, ..
166 } => value,
167 }
168 }
169
170 #[must_use]
172 pub const fn is_completed(&self) -> bool {
173 matches!(self, Self::Completed(_))
174 }
175}
176
177pub struct SessionEngine<L, C, G>
257where
258 L: Lifecycle,
259{
260 lifecycle: L,
261 clock: C,
262 log: G,
263 session: Session,
264 registry: RequestRegistry,
265 resolved: Option<Resolution<L::Profile, L::Version>>,
266}
267
268impl<L, C, G> fmt::Debug for SessionEngine<L, C, G>
269where
270 L: Lifecycle,
271 C: fmt::Debug,
272 G: fmt::Debug,
273{
274 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
275 formatter
281 .debug_struct("SessionEngine")
282 .field("id", &self.session.id())
283 .field("state", &self.session.state())
284 .field("in_flight", &self.registry.len())
285 .field("negotiated", &self.resolved.is_some())
286 .finish_non_exhaustive()
287 }
288}
289
290impl<L, C, G> SessionEngine<L, C, G>
291where
292 L: Lifecycle,
293 C: Clock,
294 G: EventLog,
295{
296 #[must_use]
302 pub fn open<J: IdSource>(lifecycle: L, clock: C, ids: &J, log: G) -> Self {
303 let session = Session::new(ids.next_session_id(), clock.now());
304 log.record(SessionEvent::Opened {
305 session: session.id(),
306 at: session.opened_at(),
307 });
308 Self {
309 lifecycle,
310 clock,
311 log,
312 session,
313 registry: RequestRegistry::new(),
314 resolved: None,
315 }
316 }
317
318 #[must_use]
324 pub fn open_stateless<J: IdSource>(lifecycle: L, clock: C, ids: &J, log: G) -> Self {
325 let session = Session::stateless(ids.next_session_id(), clock.now());
326 log.record(SessionEvent::Opened {
327 session: session.id(),
328 at: session.opened_at(),
329 });
330 Self {
331 lifecycle,
332 clock,
333 log,
334 session,
335 registry: RequestRegistry::new(),
336 resolved: None,
337 }
338 }
339
340 #[must_use]
342 pub const fn id(&self) -> SessionId {
343 self.session.id()
344 }
345
346 #[must_use]
348 pub const fn state(&self) -> SessionState {
349 self.session.state()
350 }
351
352 #[must_use]
354 pub const fn resolution(&self) -> Option<Resolution<L::Profile, L::Version>> {
355 self.resolved
356 }
357
358 #[must_use]
360 pub const fn lifecycle(&self) -> &L {
361 &self.lifecycle
362 }
363
364 #[must_use]
366 pub fn in_flight(&self) -> usize {
367 self.registry.len()
368 }
369
370 pub fn in_flight_ids(&self) -> impl Iterator<Item = &RequestId> {
372 self.registry.ids()
373 }
374
375 #[must_use]
377 pub fn request(&self, id: &RequestId) -> Option<&InFlight> {
378 self.registry.get(id)
379 }
380
381 pub fn admit(&self, method: &str, id: Option<&RequestId>) -> Result<Classification, Refusal> {
394 let state = self.session.state();
395 if state.is_terminal() || state == SessionState::Closing {
396 return Err(self.refuse(Refusal::SessionNotAccepting { state }, method, id));
397 }
398 let profile = self.resolved.map(|resolved| resolved.profile);
399 match self.lifecycle.classify(state, profile, method) {
400 Classification::Refused => {
401 Err(self.refuse(Refusal::NotPermitted { state }, method, id))
402 }
403 classification
404 @ (Classification::HandshakeStart | Classification::HandshakeConfirm) => {
405 Ok(classification)
406 }
407 Classification::Ordinary if !state.admits_ordinary_work() => {
408 Err(self.refuse(Refusal::HandshakeIncomplete { state }, method, id))
409 }
410 Classification::Ordinary if self.resolved.is_none() => {
411 Err(self.refuse(Refusal::Unnegotiated { state }, method, id))
412 }
413 Classification::Ordinary => Ok(Classification::Ordinary),
414 }
415 }
416
417 pub fn negotiate(
432 &mut self,
433 declaration: Declaration<'_>,
434 ) -> Result<Resolution<L::Profile, L::Version>, EngineError> {
435 let resolution = self.lifecycle.resolve(declaration)?;
436 if !self.session.state().is_stateless() {
442 self.transition(LifecycleEvent::HandshakeStarted)?;
443 }
444 let version = resolution.version.to_string();
445 self.log.record(SessionEvent::Negotiated {
446 session: self.session.id(),
447 at: self.clock.now(),
448 version,
449 echoed: resolution.echoed,
450 });
451 self.resolved = Some(resolution);
452 Ok(resolution)
453 }
454
455 pub fn confirm(&mut self) -> Result<(), EngineError> {
467 self.transition(LifecycleEvent::HandshakeConfirmed)?;
468 self.log.record(SessionEvent::Operating {
469 session: self.session.id(),
470 at: self.clock.now(),
471 });
472 Ok(())
473 }
474
475 pub fn close(&mut self) -> Result<Vec<RequestId>, EngineError> {
487 self.transition(LifecycleEvent::CloseRequested)?;
488 Ok(self.abandon(&AbortReason::SessionClosed))
489 }
490
491 pub fn finish(&mut self) -> Result<(), EngineError> {
497 self.transition(LifecycleEvent::Finished)
498 }
499
500 pub fn register(
513 &mut self,
514 id: RequestId,
515 origin: RequestOrigin,
516 method: &str,
517 token: Option<ProgressToken>,
518 budget: Option<Duration>,
519 ) -> Result<CancellationToken, DuplicateRequest> {
520 let entry = InFlight::new(origin, method, token, budget, self.clock.now());
521 let handle = entry.cancellation_handle();
522 let method = Arc::from(method);
523 self.registry.register(id.clone(), entry)?;
524 self.log.record(SessionEvent::Registered {
525 session: self.session.id(),
526 at: self.clock.now(),
527 id,
528 method,
529 origin,
530 });
531 Ok(handle)
532 }
533
534 #[must_use]
539 pub fn cancellation(&self, id: &RequestId) -> Option<CancellationToken> {
540 self.registry.get(id).map(InFlight::cancellation_handle)
541 }
542
543 pub fn cancel(&mut self, id: &RequestId, reason: Option<CancelReason>) -> CancelOutcome {
550 let outcome = self.registry.cancel(id, reason.clone());
555 if outcome.cancelled() {
556 self.log_abort(id, AbortReason::Cancelled(reason));
557 }
558 outcome
559 }
560
561 pub fn progress(&mut self, id: &RequestId, progress: f64, message: Option<String>) -> bool {
566 self.registry
567 .get_mut(id)
568 .is_some_and(|entry| entry.advance_progress(progress, message))
569 }
570
571 pub fn expire(&mut self) -> Vec<RequestId> {
578 let expired = self.registry.expire(self.clock.now());
579 for id in &expired {
580 self.log_abort(id, AbortReason::Expired);
581 }
582 expired
583 }
584
585 pub fn complete(&mut self, id: &RequestId) -> Option<Option<AbortReason>> {
592 let entry = self.registry.complete(id)?;
593 let reason = entry.cancellation().reason().cloned();
594 let method = Arc::from(entry.method());
595 let at = self.clock.now();
596 match reason.as_ref() {
600 Some(reason) => self.log.record(SessionEvent::Aborted {
601 session: self.session.id(),
602 at,
603 id: id.clone(),
604 method,
605 reason: reason.clone(),
606 }),
607 None => self.log.record(SessionEvent::Completed {
608 session: self.session.id(),
609 at,
610 id: id.clone(),
611 method,
612 }),
613 }
614 Some(reason)
615 }
616
617 pub async fn dispatch<H, T>(
636 &mut self,
637 id: RequestId,
638 origin: RequestOrigin,
639 method: &str,
640 token: Option<ProgressToken>,
641 budget: Option<Duration>,
642 handler: H,
643 ) -> Result<Dispatch<T>, DuplicateRequest>
644 where
645 H: AsyncFnOnce(CancellationToken) -> T,
646 {
647 let handle = self.register(id.clone(), origin, method, token, budget)?;
648 let produced: T = handler(handle).await;
649 Ok(match self.complete(&id) {
650 None => Dispatch::Aborted {
654 discarded: produced,
655 reason: AbortReason::SessionClosed,
656 },
657 Some(None) => Dispatch::Completed(produced),
658 Some(Some(reason)) => Dispatch::Aborted {
659 discarded: produced,
660 reason,
661 },
662 })
663 }
664
665 fn transition(&mut self, event: LifecycleEvent) -> Result<(), EngineError> {
667 self.session = self.session.apply(event)?;
668 self.log.record(SessionEvent::StateChanged {
669 session: self.session.id(),
670 at: self.clock.now(),
671 state: self.session.state(),
672 });
673 Ok(())
674 }
675
676 fn refuse(&self, refusal: Refusal, method: &str, id: Option<&RequestId>) -> Refusal {
678 self.log.record(SessionEvent::Refused {
679 session: self.session.id(),
680 at: self.clock.now(),
681 id: id.cloned(),
682 method: Arc::from(method),
683 state: refusal.state(),
684 });
685 refusal
686 }
687
688 fn log_abort(&self, id: &RequestId, fallback: AbortReason) {
694 let Some(entry) = self.registry.get(id) else {
695 return;
696 };
697 let reason = entry.cancellation().reason().cloned().unwrap_or(fallback);
698 self.log.record(SessionEvent::Aborted {
699 session: self.session.id(),
700 at: self.clock.now(),
701 id: id.clone(),
702 method: Arc::from(entry.method()),
703 reason,
704 });
705 }
706
707 fn abandon(&mut self, reason: &AbortReason) -> Vec<RequestId> {
713 let recorded: Vec<(RequestId, Arc<str>)> = self
714 .registry
715 .ids()
716 .map(|id| {
717 let method = self.registry.get(id).map_or("", InFlight::method);
718 (id.clone(), Arc::from(method))
719 })
720 .collect();
721 let ids = self.registry.drain();
722 let at = self.clock.now();
723 let session = self.session.id();
724 for (id, method) in recorded {
725 self.log.record(SessionEvent::Aborted {
726 session,
727 at,
728 id,
729 method,
730 reason: reason.clone(),
731 });
732 }
733 ids
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use core::sync::atomic::{AtomicU64, Ordering};
740 use core::time::Duration;
741
742 use serde_json::json;
743
744 use super::*;
745 use crate::application::session::lifecycle::MetaView;
746 use crate::domain::session::{Instant, SessionId};
747
748 #[derive(Debug)]
757 #[expect(dead_code, reason = "the payloads are read by the derived Debug")]
758 enum Failure {
759 Engine(EngineError),
761 Refusal(Refusal),
763 Duplicate(DuplicateRequest),
765 Suspended,
768 TimedOut(&'static str),
773 NotAborted(String),
776 }
777
778 impl From<EngineError> for Failure {
779 fn from(error: EngineError) -> Self {
780 Self::Engine(error)
781 }
782 }
783
784 impl From<Refusal> for Failure {
785 fn from(refusal: Refusal) -> Self {
786 Self::Refusal(refusal)
787 }
788 }
789
790 impl From<DuplicateRequest> for Failure {
791 fn from(duplicate: DuplicateRequest) -> Self {
792 Self::Duplicate(duplicate)
793 }
794 }
795
796 struct Open;
801
802 impl Lifecycle for Open {
803 type Profile = ();
804 type Version = u16;
805
806 fn classify(
807 &self,
808 _state: SessionState,
809 _profile: Option<()>,
810 method: &str,
811 ) -> Classification {
812 match method {
813 "initialize" => Classification::HandshakeStart,
814 "notifications/initialized" => Classification::HandshakeConfirm,
815 _ => Classification::Ordinary,
816 }
817 }
818
819 fn resolve(
820 &self,
821 declaration: Declaration<'_>,
822 ) -> Result<Resolution<(), u16>, DeclarationError> {
823 let version = match declaration {
824 Declaration::Handshake { version, .. } => {
825 u16::try_from(version.len()).unwrap_or(u16::MAX)
826 }
827 Declaration::Stateless { .. } => 1,
828 };
829 Ok(Resolution {
830 profile: (),
831 version,
832 echoed: true,
833 })
834 }
835 }
836
837 struct Strict;
841
842 impl Lifecycle for Strict {
843 type Profile = bool;
844 type Version = u16;
845
846 fn classify(
847 &self,
848 _state: SessionState,
849 profile: Option<bool>,
850 _method: &str,
851 ) -> Classification {
852 match profile {
853 Some(true) => Classification::Ordinary,
854 _ => Classification::Refused,
855 }
856 }
857
858 fn resolve(
859 &self,
860 declaration: Declaration<'_>,
861 ) -> Result<Resolution<bool, u16>, DeclarationError> {
862 let Declaration::Handshake { version, .. } = declaration else {
863 return Err(DeclarationError::NoVersion);
864 };
865 Ok(Resolution {
866 profile: true,
867 version: u16::try_from(version.len()).unwrap_or(u16::MAX),
868 echoed: true,
869 })
870 }
871 }
872
873 struct Fixed(Instant);
874
875 impl Clock for Fixed {
876 fn now(&self) -> Instant {
877 self.0
878 }
879 }
880
881 struct Stepping {
883 now: AtomicU64,
884 }
885
886 impl Clock for &Stepping {
889 fn now(&self) -> Instant {
890 <Stepping as Clock>::now(self)
891 }
892 }
893
894 impl Stepping {
895 fn new() -> Self {
896 Self {
897 now: AtomicU64::new(0),
898 }
899 }
900
901 fn advance(&self, by: Duration) {
902 let millis = u64::try_from(by.as_millis()).unwrap_or(u64::MAX);
903 self.now.fetch_add(millis, Ordering::Relaxed);
904 }
905 }
906
907 impl Clock for Stepping {
908 fn now(&self) -> Instant {
909 Instant::from_duration(Duration::from_millis(self.now.load(Ordering::Relaxed)))
910 }
911 }
912
913 struct Counting;
914
915 impl IdSource for Counting {
916 fn next_session_id(&self) -> SessionId {
917 SessionId::new(1)
918 }
919 }
920
921 #[derive(Default)]
924 struct Recording {
925 events: core::cell::RefCell<Vec<SessionEvent>>,
926 }
927
928 impl EventLog for Recording {
929 fn record(&self, event: SessionEvent) {
930 self.events.borrow_mut().push(event);
931 }
932 }
933
934 impl Recording {
935 fn kinds(&self) -> Vec<&'static str> {
936 self.events
937 .borrow()
938 .iter()
939 .map(|event| match event {
940 SessionEvent::Opened { .. } => "opened",
941 SessionEvent::Negotiated { .. } => "negotiated",
942 SessionEvent::Operating { .. } => "operating",
943 SessionEvent::Registered { .. } => "registered",
944 SessionEvent::Completed { .. } => "completed",
945 SessionEvent::Aborted { .. } => "aborted",
946 SessionEvent::Refused { .. } => "refused",
947 SessionEvent::StateChanged { .. } => "state-changed",
948 })
949 .collect()
950 }
951 }
952
953 fn engine() -> SessionEngine<Open, Fixed, Recording> {
954 SessionEngine::open(
955 Open,
956 Fixed(Instant::from_duration(Duration::ZERO)),
957 &Counting,
958 Recording::default(),
959 )
960 }
961
962 fn negotiated() -> Result<SessionEngine<Open, Fixed, Recording>, Failure> {
964 let mut engine = engine();
965 engine.negotiate(Declaration::Handshake {
966 version: "2025-06-18",
967 capabilities: Some(&json!({})),
968 })?;
969 engine.confirm()?;
970 Ok(engine)
971 }
972
973 #[test]
974 fn a_new_session_admits_nothing_until_the_handshake_settles() {
975 let engine = engine();
976 assert_eq!(engine.state(), SessionState::Uninitialised);
977 assert_eq!(
978 engine.admit("tools/list", None),
979 Err(Refusal::HandshakeIncomplete {
980 state: SessionState::Uninitialised
981 })
982 );
983 assert!(engine.resolution().is_none());
984 }
985
986 #[test]
989 fn the_handshake_messages_pass_the_gate_that_ordinary_work_fails() -> Result<(), Failure> {
990 let mut engine = engine();
991 assert_eq!(
992 engine.admit("initialize", None)?,
993 Classification::HandshakeStart,
994 "the handshake is always admitted"
995 );
996 engine.negotiate(Declaration::Handshake {
997 version: "2025-06-18",
998 capabilities: None,
999 })?;
1000 assert_eq!(
1001 engine.admit("notifications/initialized", None)?,
1002 Classification::HandshakeConfirm,
1003 "the confirmation is admitted too"
1004 );
1005 Ok(())
1006 }
1007
1008 #[test]
1009 fn the_handshake_moves_the_session_to_operating() -> Result<(), Failure> {
1010 let engine = negotiated()?;
1011 assert_eq!(engine.state(), SessionState::Operating);
1012 assert_eq!(engine.admit("tools/list", None)?, Classification::Ordinary);
1013 Ok(())
1014 }
1015
1016 #[test]
1017 fn the_lifecycle_decides_admission_not_just_the_phase() -> Result<(), Failure> {
1018 let mut engine = SessionEngine::open(
1019 Strict,
1020 Fixed(Instant::from_duration(Duration::ZERO)),
1021 &Counting,
1022 Recording::default(),
1023 );
1024 assert_eq!(
1027 engine.admit("tools/list", None),
1028 Err(Refusal::NotPermitted {
1029 state: SessionState::Uninitialised
1030 }),
1031 "the protocol's own veto, not the phase gate"
1032 );
1033
1034 engine.negotiate(Declaration::Handshake {
1035 version: "2025-06-18",
1036 capabilities: None,
1037 })?;
1038 engine.confirm()?;
1039 assert_eq!(engine.admit("tools/list", None)?, Classification::Ordinary);
1040 Ok(())
1041 }
1042
1043 #[test]
1044 fn a_stateless_session_never_waits_for_a_handshake() -> Result<(), Failure> {
1045 let mut engine = SessionEngine::open_stateless(
1046 Open,
1047 Fixed(Instant::from_duration(Duration::ZERO)),
1048 &Counting,
1049 Recording::default(),
1050 );
1051 assert!(engine.state().is_stateless());
1052 engine.negotiate(Declaration::Stateless {
1053 meta: MetaView::new(None),
1054 })?;
1055 assert!(engine.state().is_stateless());
1057 Ok(())
1058 }
1059
1060 #[test]
1061 fn a_duplicate_in_flight_id_is_refused_and_leaves_the_first_alone() -> Result<(), Failure> {
1062 let mut engine = negotiated()?;
1063 let id = RequestId::text("a");
1064 let Ok(_) = engine.register(id.clone(), RequestOrigin::Inbound, "tools/call", None, None)
1065 else {
1066 return Err(Failure::Suspended);
1067 };
1068 assert_eq!(
1069 engine
1070 .register(id.clone(), RequestOrigin::Inbound, "tools/call", None, None)
1071 .map(|_| ()),
1072 Err(DuplicateRequest { id }),
1073 "the id is already in flight"
1074 );
1075 assert_eq!(engine.in_flight(), 1, "the original entry survives");
1076 Ok(())
1077 }
1078
1079 #[test]
1080 fn cancelling_an_unknown_or_finished_request_is_a_no_op() -> Result<(), Failure> {
1081 let mut engine = negotiated()?;
1082 let outcome = engine.cancel(&RequestId::text("never-seen"), None);
1083 assert!(!outcome.cancelled());
1084 assert_eq!(engine.in_flight(), 0);
1085 Ok(())
1086 }
1087
1088 #[test]
1089 fn cancelling_an_in_flight_request_is_visible_to_its_handler() -> Result<(), Failure> {
1090 let mut engine = negotiated()?;
1091 let id = RequestId::text("a");
1092 let Ok(token) =
1093 engine.register(id.clone(), RequestOrigin::Inbound, "tools/call", None, None)
1094 else {
1095 return Err(Failure::Suspended);
1096 };
1097 assert!(!token.is_cancelled());
1098 let outcome = engine.cancel(&id, None);
1099 assert!(outcome.cancelled());
1100 assert!(token.is_cancelled(), "the handler observes the abort");
1101 assert_eq!(engine.in_flight(), 1);
1105 assert_eq!(
1106 engine.complete(&id),
1107 Some(Some(AbortReason::Cancelled(None)))
1108 );
1109 assert_eq!(engine.in_flight(), 0);
1110 Ok(())
1111 }
1112
1113 #[test]
1114 fn a_request_that_ran_to_completion_is_reported_as_completed() -> Result<(), Failure> {
1115 let mut engine = negotiated()?;
1116 let id = RequestId::text("a");
1117 let _ = engine.register(id.clone(), RequestOrigin::Inbound, "tools/call", None, None)?;
1118 assert_eq!(engine.complete(&id), Some(None), "no abort reason");
1119 assert_eq!(engine.complete(&id), None, "already finished");
1120 Ok(())
1121 }
1122
1123 #[test]
1124 fn a_cancelled_request_reports_an_abort_rather_than_a_completion() -> Result<(), Failure> {
1125 let mut engine = negotiated()?;
1126 let id = RequestId::text("a");
1127 let _ = engine.register(id.clone(), RequestOrigin::Inbound, "tools/call", None, None)?;
1128 engine.cancel(&id, None);
1129 assert!(matches!(
1130 engine.complete(&id),
1131 Some(Some(AbortReason::Cancelled(_)))
1132 ));
1133 Ok(())
1134 }
1135
1136 #[tokio::test(start_paused = true)]
1149 async fn a_cancelled_request_answers_nothing_even_when_the_handler_returns()
1150 -> Result<(), Failure> {
1151 use crate::domain::session::CancelReason;
1152
1153 let mut engine = negotiated()?;
1154 let id = RequestId::text("a");
1155
1156 let dispatched = engine.dispatch(
1157 id,
1158 RequestOrigin::Inbound,
1159 "tools/call",
1160 None,
1161 None,
1162 |signal: CancellationToken| async move {
1163 let peer = signal.clone();
1166 let canceller = tokio::spawn(async move {
1167 peer.cancel(AbortReason::Cancelled(Some(CancelReason::new("peer"))));
1168 });
1169 signal.cancelled().await;
1170 let _ = canceller.await;
1171 "a value the peer will never receive"
1172 },
1173 );
1174
1175 let outcome = tokio::time::timeout(Duration::from_secs(5), dispatched)
1176 .await
1177 .map_err(|_| Failure::TimedOut("the handler was parked, not woken"))??;
1178
1179 let Dispatch::Aborted { discarded, reason } = outcome else {
1183 return Err(Failure::NotAborted(format!("{outcome:?}")));
1184 };
1185 assert_eq!(discarded, "a value the peer will never receive");
1186 assert!(matches!(reason, AbortReason::Cancelled(_)), "{reason:?}");
1187 Ok(())
1188 }
1189
1190 #[tokio::test(start_paused = true)]
1199 async fn a_spent_budget_wakes_a_handler_parked_on_its_handle() -> Result<(), Failure> {
1200 let clock = &Stepping::new();
1201 let mut engine = SessionEngine::open(Open, clock, &Counting, Recording::default());
1202 engine.negotiate(Declaration::Handshake {
1203 version: "2025-06-18",
1204 capabilities: None,
1205 })?;
1206 engine.confirm()?;
1207
1208 let id = RequestId::text("a");
1209 let handle = engine.register(
1210 id.clone(),
1211 RequestOrigin::Inbound,
1212 "tools/call",
1213 None,
1214 Some(Duration::from_millis(50)),
1215 )?;
1216
1217 let waiter = tokio::spawn({
1220 let handle = handle.clone();
1221 async move {
1222 handle.cancelled().await;
1223 handle.reason().cloned()
1224 }
1225 });
1226 tokio::task::yield_now().await;
1227
1228 clock.advance(Duration::from_millis(25));
1230 assert!(
1231 engine.expire().is_empty(),
1232 "a budget spent only half way is not spent"
1233 );
1234
1235 clock.advance(Duration::from_millis(25));
1236 assert_eq!(engine.expire(), vec![id], "the budget is now spent");
1237
1238 let reason = tokio::time::timeout(Duration::from_secs(5), waiter)
1239 .await
1240 .map_err(|_| Failure::TimedOut("the expiry never woke the parked task"))?
1241 .map_err(|panic| Failure::NotAborted(format!("the parked task panicked: {panic}")))?;
1242 assert_eq!(
1243 reason,
1244 Some(AbortReason::Expired),
1245 "the handler woke for the reason the engine recorded"
1246 );
1247 Ok(())
1248 }
1249
1250 #[test]
1251 fn a_spent_budget_expires_and_nothing_else_does() -> Result<(), Failure> {
1252 let clock = &Stepping::new();
1253 let mut engine = SessionEngine::open(Open, clock, &Counting, Recording::default());
1254 engine.negotiate(Declaration::Handshake {
1255 version: "2025-06-18",
1256 capabilities: None,
1257 })?;
1258 engine.confirm()?;
1259
1260 let quick = RequestId::text("quick");
1261 let slow = RequestId::text("slow");
1262 let _ = engine.register(
1263 quick.clone(),
1264 RequestOrigin::Inbound,
1265 "tools/call",
1266 None,
1267 Some(Duration::from_millis(50)),
1268 )?;
1269 let _ = engine.register(
1270 slow.clone(),
1271 RequestOrigin::Inbound,
1272 "tools/call",
1273 None,
1274 Some(Duration::from_millis(5_000)),
1275 )?;
1276
1277 clock.advance(Duration::from_millis(100));
1278 let expired = engine.expire();
1279 assert_eq!(expired, vec![quick], "only the spent budget expires");
1280 assert!(
1281 engine.request(&slow).is_some(),
1282 "the other is still running"
1283 );
1284 Ok(())
1285 }
1286
1287 #[test]
1288 fn closing_aborts_everything_still_in_flight() -> Result<(), Failure> {
1289 let mut engine = negotiated()?;
1290 let _ = engine.register(
1291 RequestId::text("a"),
1292 RequestOrigin::Outbound,
1293 "sampling/createMessage",
1294 None,
1295 None,
1296 )?;
1297 let _ = engine.register(
1298 RequestId::text("b"),
1299 RequestOrigin::Outbound,
1300 "elicitation/create",
1301 None,
1302 None,
1303 )?;
1304 assert_eq!(engine.in_flight(), 2);
1305 engine.close()?;
1306 assert_eq!(engine.in_flight(), 0, "nothing is left to leak");
1307 assert_eq!(
1308 engine.state(),
1309 SessionState::Closing,
1310 "closing is not yet terminal — the peer still has to finish"
1311 );
1312 engine.finish()?;
1313 assert!(engine.state().is_terminal());
1314 assert!(
1315 engine.admit("tools/list", None).is_err(),
1316 "a finished session admits nothing"
1317 );
1318 Ok(())
1319 }
1320
1321 #[test]
1322 fn dispatch_registers_runs_and_completes() -> Result<(), Failure> {
1323 let mut engine = negotiated()?;
1324 let outcome = futures_lite_block_on(engine.dispatch(
1325 RequestId::text("a"),
1326 RequestOrigin::Inbound,
1327 "tools/call",
1328 None,
1329 None,
1330 |_token| async { "ok" },
1331 ))
1332 .ok_or(Failure::Suspended)??;
1333 assert!(outcome.is_completed(), "the handler ran to the end");
1334 assert_eq!(outcome.into_inner(), "ok");
1335 assert_eq!(engine.in_flight(), 0);
1336 assert_eq!(
1337 engine.log.kinds(),
1338 [
1339 "opened",
1340 "state-changed",
1341 "negotiated",
1342 "state-changed",
1343 "operating",
1344 "registered",
1345 "completed",
1346 ],
1347 "the log is a transcript: every transition, then the work"
1348 );
1349 Ok(())
1350 }
1351
1352 #[test]
1353 fn every_recorded_event_names_the_session_and_when_it_happened() {
1354 let engine = engine();
1355 let events = engine.log.events.borrow();
1356 for event in events.iter() {
1360 assert_eq!(event.session(), engine.id());
1361 assert_eq!(event.at(), Instant::from_duration(Duration::ZERO));
1362 }
1363 assert!(!events.is_empty(), "opening records at least one event");
1364 }
1365
1366 #[test]
1367 fn a_refusal_is_recorded_before_it_is_returned() {
1368 let engine = engine();
1369 let id = RequestId::text("a");
1370 assert!(engine.admit("tools/call", Some(&id)).is_err());
1371 assert_eq!(engine.log.kinds(), ["opened", "refused"]);
1372 }
1373
1374 #[test]
1375 fn a_cancelled_dispatch_sends_no_response() -> Result<(), Failure> {
1376 let mut engine = negotiated()?;
1377 let outcome = futures_lite_block_on(engine.dispatch(
1378 RequestId::text("a"),
1379 RequestOrigin::Inbound,
1380 "tools/call",
1381 None,
1382 None,
1383 |token| async move {
1384 let _ = engine_cancel(&token);
1386 "should not be sent"
1387 },
1388 ))
1389 .ok_or(Failure::Suspended)??;
1390 assert!(
1391 matches!(outcome, Dispatch::Aborted { .. }),
1392 "an aborted request is never answered"
1393 );
1394 assert!(!outcome.is_completed(), "an abort is not a completion");
1395 assert_eq!(
1396 outcome.into_inner(),
1397 "should not be sent",
1398 "the value is still handed back, for the log rather than the wire"
1399 );
1400 Ok(())
1401 }
1402
1403 fn engine_cancel(token: &CancellationToken) -> bool {
1405 token.cancel(AbortReason::Cancelled(None))
1406 }
1407
1408 fn futures_lite_block_on<F: core::future::Future>(future: F) -> Option<F::Output> {
1412 use core::task::{Context, Poll};
1413 use std::task::Waker;
1414
1415 let mut context = Context::from_waker(Waker::noop());
1416 let mut future = Box::pin(future);
1417 match future.as_mut().poll(&mut context) {
1418 Poll::Ready(value) => Some(value),
1419 Poll::Pending => None,
1420 }
1421 }
1422}