1use std::collections::{BTreeSet, HashMap};
103use std::fmt::Write as _;
104use std::sync::atomic::{AtomicBool, Ordering};
105use std::sync::{Arc, RwLock};
106use std::time::{Duration, Instant};
107
108use async_trait::async_trait;
109
110use crate::error::JsonRpcError;
111use crate::protocol::{CallToolResult, InputRequests, InputResponses, TaskObject, TaskStatus};
112
113const DEFAULT_TTL_MS: u64 = 300_000;
118
119const DEFAULT_POLL_INTERVAL_MS: u64 = 2_000;
121
122#[derive(Debug)]
124pub struct Task {
125 pub id: String,
127 pub tool_name: String,
129 pub arguments: serde_json::Value,
131 pub status: TaskStatus,
133 pub created_at: Instant,
135 pub created_at_str: String,
137 pub last_updated_at_str: String,
139 pub ttl: u64,
141 pub poll_interval: u64,
143 pub status_message: Option<String>,
145 pub meta: Option<serde_json::Value>,
147 pub result: Option<CallToolResult>,
149 pub error: Option<JsonRpcError>,
155 pub owner: TaskOwner,
160 pub input_requests: InputRequests,
162 pub answered_input_keys: BTreeSet<String>,
164 pub superseded_input_keys: BTreeSet<String>,
167 pub cancellation_token: CancellationToken,
169 pub completed_at: Option<Instant>,
171 pub completion_notify: Arc<tokio::sync::Notify>,
173}
174
175impl Task {
176 fn new(
178 id: String,
179 tool_name: String,
180 arguments: serde_json::Value,
181 ttl: Option<u64>,
182 owner: TaskOwner,
183 ) -> Self {
184 let cancelled = Arc::new(AtomicBool::new(false));
185 let now_str = chrono_now_iso8601();
186 Self {
187 id,
188 tool_name,
189 arguments,
190 status: TaskStatus::Working,
191 created_at: Instant::now(),
192 created_at_str: now_str.clone(),
193 last_updated_at_str: now_str,
194 ttl: ttl.unwrap_or(DEFAULT_TTL_MS),
195 poll_interval: DEFAULT_POLL_INTERVAL_MS,
196 status_message: Some("Task started".to_string()),
197 meta: None,
198 result: None,
199 error: None,
200 owner,
201 input_requests: InputRequests::new(),
202 answered_input_keys: BTreeSet::new(),
203 superseded_input_keys: BTreeSet::new(),
204 cancellation_token: CancellationToken { cancelled },
205 completed_at: None,
206 completion_notify: Arc::new(tokio::sync::Notify::new()),
207 }
208 }
209
210 pub fn to_task_object(&self) -> TaskObject {
212 TaskObject {
213 task_id: self.id.clone(),
214 status: self.status,
215 status_message: self.status_message.clone(),
216 created_at: self.created_at_str.clone(),
217 last_updated_at: self.last_updated_at_str.clone(),
218 ttl: Some(self.ttl),
219 poll_interval: Some(self.poll_interval),
220 result: None,
221 error: None,
222 meta: self.meta.clone(),
223 }
224 }
225
226 pub fn is_expired(&self) -> bool {
233 self.created_at.elapsed() > Duration::from_millis(self.ttl)
234 }
235
236 pub fn outstanding_input_requests(&self) -> &InputRequests {
238 &self.input_requests
239 }
240
241 pub fn is_cancelled(&self) -> bool {
243 self.cancellation_token.is_cancelled()
244 }
245}
246
247pub fn generate_task_id() -> String {
260 let mut bytes = [0u8; 16];
261 getrandom::fill(&mut bytes).expect("system entropy source unavailable for task ID generation");
262 let mut id = String::with_capacity(2 * bytes.len());
263 for byte in bytes {
264 let _ = write!(id, "{byte:02x}");
265 }
266 id
267}
268
269pub type TaskOwner = Option<String>;
278
279pub fn owner_matches(owner: &TaskOwner, principal: Option<&str>) -> bool {
286 owner.as_deref() == principal
287}
288
289#[derive(Debug, Clone, Default, PartialEq, Eq)]
295pub struct AppliedInputResponses {
296 pub accepted: BTreeSet<String>,
298 pub ignored: BTreeSet<String>,
301 pub still_outstanding: BTreeSet<String>,
303}
304
305impl AppliedInputResponses {
306 pub fn is_complete(&self) -> bool {
308 self.still_outstanding.is_empty()
309 }
310}
311
312#[derive(Debug, Clone)]
314pub struct CancellationToken {
315 cancelled: Arc<AtomicBool>,
316}
317
318impl CancellationToken {
319 pub fn is_cancelled(&self) -> bool {
321 self.cancelled.load(Ordering::Relaxed)
322 }
323
324 pub fn cancel(&self) {
326 self.cancelled.store(true, Ordering::Relaxed);
327 }
328}
329
330#[derive(Debug, thiserror::Error)]
338#[non_exhaustive]
339pub enum TaskStoreError {
340 #[error("encode error: {0}")]
342 Encode(String),
343 #[error("decode error: {0}")]
345 Decode(String),
346 #[error("backend error: {0}")]
348 Backend(String),
349}
350
351pub type Result<T> = std::result::Result<T, TaskStoreError>;
353
354pub type TaskSnapshot = (TaskObject, Option<CallToolResult>, Option<JsonRpcError>);
360
361#[async_trait]
383pub trait TaskStore: Send + Sync + 'static {
384 async fn create_task(
390 &self,
391 tool_name: &str,
392 arguments: serde_json::Value,
393 ttl: Option<u64>,
394 owner: TaskOwner,
395 ) -> Result<(String, CancellationToken)>;
396
397 async fn task_owner(&self, task_id: &str) -> Result<Option<TaskOwner>>;
403
404 async fn get_task(&self, task_id: &str) -> Result<Option<TaskObject>>;
406
407 async fn set_task_meta(&self, task_id: &str, meta: serde_json::Value) -> Result<bool> {
412 let _ = (task_id, meta);
413 Ok(false)
414 }
415
416 async fn discard_task(&self, task_id: &str) -> Result<bool> {
421 let _ = task_id;
422 Ok(false)
423 }
424
425 async fn get_task_result(&self, task_id: &str) -> Result<Option<TaskSnapshot>>;
427
428 async fn wait_for_completion(&self, task_id: &str) -> Result<Option<TaskSnapshot>>;
434
435 async fn list_tasks(&self, status_filter: Option<TaskStatus>) -> Result<Vec<TaskObject>>;
437
438 async fn require_input(
447 &self,
448 task_id: &str,
449 requests: InputRequests,
450 message: Option<&str>,
451 ) -> Result<bool>;
452
453 async fn outstanding_input_requests(&self, task_id: &str) -> Result<Option<InputRequests>>;
458
459 async fn apply_input_responses(
467 &self,
468 task_id: &str,
469 responses: InputResponses,
470 ) -> Result<Option<AppliedInputResponses>>;
471
472 async fn set_ttl(&self, task_id: &str, ttl_ms: u64) -> Result<bool>;
477
478 async fn complete_task(&self, task_id: &str, result: CallToolResult) -> Result<bool>;
487
488 async fn fail_task(&self, task_id: &str, error: JsonRpcError) -> Result<bool>;
493
494 async fn cancel_task(&self, task_id: &str, reason: Option<&str>) -> Result<Option<TaskObject>>;
500}
501
502#[derive(Debug, Clone)]
510pub struct MemoryTaskStore {
511 tasks: Arc<RwLock<HashMap<String, Task>>>,
512}
513
514impl Default for MemoryTaskStore {
515 fn default() -> Self {
516 Self::new()
517 }
518}
519
520impl MemoryTaskStore {
521 pub fn new() -> Self {
523 Self {
524 tasks: Arc::new(RwLock::new(HashMap::new())),
525 }
526 }
527
528 pub fn cleanup_expired(&self) -> usize {
536 if let Ok(mut tasks) = self.tasks.write() {
537 let before = tasks.len();
538 tasks.retain(|_, t| !t.is_expired());
539 before - tasks.len()
540 } else {
541 0
542 }
543 }
544
545 #[cfg(test)]
547 pub fn len(&self) -> usize {
548 if let Ok(tasks) = self.tasks.read() {
549 tasks.len()
550 } else {
551 0
552 }
553 }
554
555 #[cfg(test)]
557 pub fn is_empty(&self) -> bool {
558 self.len() == 0
559 }
560}
561
562#[async_trait]
563impl TaskStore for MemoryTaskStore {
564 async fn create_task(
565 &self,
566 tool_name: &str,
567 arguments: serde_json::Value,
568 ttl: Option<u64>,
569 owner: TaskOwner,
570 ) -> Result<(String, CancellationToken)> {
571 let id = generate_task_id();
572 let task = Task::new(id.clone(), tool_name.to_string(), arguments, ttl, owner);
573 let token = task.cancellation_token.clone();
574
575 if let Ok(mut tasks) = self.tasks.write() {
576 tasks.insert(id.clone(), task);
577 }
578
579 Ok((id, token))
580 }
581
582 async fn get_task(&self, task_id: &str) -> Result<Option<TaskObject>> {
583 Ok(if let Ok(tasks) = self.tasks.read() {
584 tasks
585 .get(task_id)
586 .filter(|t| !t.is_expired())
587 .map(|t| t.to_task_object())
588 } else {
589 None
590 })
591 }
592
593 async fn set_task_meta(&self, task_id: &str, meta: serde_json::Value) -> Result<bool> {
594 let Ok(mut tasks) = self.tasks.write() else {
595 return Ok(false);
596 };
597 let Some(task) = tasks.get_mut(task_id).filter(|task| !task.is_expired()) else {
598 return Ok(false);
599 };
600 task.meta = Some(meta);
601 Ok(true)
602 }
603
604 async fn discard_task(&self, task_id: &str) -> Result<bool> {
605 Ok(self
606 .tasks
607 .write()
608 .ok()
609 .and_then(|mut tasks| tasks.remove(task_id))
610 .is_some())
611 }
612
613 async fn task_owner(&self, task_id: &str) -> Result<Option<TaskOwner>> {
614 Ok(if let Ok(tasks) = self.tasks.read() {
615 tasks
616 .get(task_id)
617 .filter(|t| !t.is_expired())
618 .map(|t| t.owner.clone())
619 } else {
620 None
621 })
622 }
623
624 async fn get_task_result(&self, task_id: &str) -> Result<Option<TaskSnapshot>> {
625 Ok(if let Ok(tasks) = self.tasks.read() {
626 tasks
627 .get(task_id)
628 .filter(|t| !t.is_expired())
629 .map(|t| (t.to_task_object(), t.result.clone(), t.error.clone()))
630 } else {
631 None
632 })
633 }
634
635 async fn wait_for_completion(&self, task_id: &str) -> Result<Option<TaskSnapshot>> {
636 let notify = {
638 let Ok(tasks) = self.tasks.read() else {
639 return Ok(None);
640 };
641 let Some(task) = tasks.get(task_id).filter(|t| !t.is_expired()) else {
642 return Ok(None);
643 };
644 if task.status.is_terminal() {
645 return Ok(Some((
646 task.to_task_object(),
647 task.result.clone(),
648 task.error.clone(),
649 )));
650 }
651 task.completion_notify.clone()
652 };
653
654 notify.notified().await;
656
657 self.get_task_result(task_id).await
659 }
660
661 async fn list_tasks(&self, status_filter: Option<TaskStatus>) -> Result<Vec<TaskObject>> {
662 Ok(if let Ok(tasks) = self.tasks.read() {
663 tasks
664 .values()
665 .filter(|t| !t.is_expired())
666 .filter(|t| status_filter.is_none() || status_filter == Some(t.status))
667 .map(|t| t.to_task_object())
668 .collect()
669 } else {
670 vec![]
671 })
672 }
673
674 async fn require_input(
675 &self,
676 task_id: &str,
677 requests: InputRequests,
678 message: Option<&str>,
679 ) -> Result<bool> {
680 let Ok(mut tasks) = self.tasks.write() else {
681 return Ok(false);
682 };
683 let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
684 return Ok(false);
685 };
686 if task.status.is_terminal() {
687 return Ok(false);
688 }
689
690 for key in std::mem::take(&mut task.input_requests).into_keys() {
692 if !requests.contains_key(&key) {
693 task.superseded_input_keys.insert(key);
694 }
695 }
696 for key in requests.keys() {
698 task.answered_input_keys.remove(key);
699 task.superseded_input_keys.remove(key);
700 }
701
702 task.input_requests = requests;
703 task.status = TaskStatus::InputRequired;
704 task.status_message = Some(
705 message
706 .map(str::to_string)
707 .unwrap_or_else(|| "Awaiting client input".to_string()),
708 );
709 task.last_updated_at_str = chrono_now_iso8601();
710 Ok(true)
711 }
712
713 async fn outstanding_input_requests(&self, task_id: &str) -> Result<Option<InputRequests>> {
714 Ok(if let Ok(tasks) = self.tasks.read() {
715 tasks
716 .get(task_id)
717 .filter(|t| !t.is_expired())
718 .map(|t| t.input_requests.clone())
719 } else {
720 None
721 })
722 }
723
724 async fn apply_input_responses(
725 &self,
726 task_id: &str,
727 responses: InputResponses,
728 ) -> Result<Option<AppliedInputResponses>> {
729 let Ok(mut tasks) = self.tasks.write() else {
730 return Ok(None);
731 };
732 let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
733 return Ok(None);
734 };
735 if task.status.is_terminal() {
736 return Ok(None);
737 }
738
739 let mut applied = AppliedInputResponses::default();
740 for key in responses.into_keys() {
741 if task.input_requests.remove(&key).is_some() {
742 task.answered_input_keys.insert(key.clone());
743 applied.accepted.insert(key);
744 } else {
745 applied.ignored.insert(key);
749 }
750 }
751 applied.still_outstanding = task.input_requests.keys().cloned().collect();
752
753 if !applied.accepted.is_empty() {
754 task.last_updated_at_str = chrono_now_iso8601();
755 }
756 if applied.is_complete() && task.status == TaskStatus::InputRequired {
757 task.status = TaskStatus::Working;
758 task.status_message = Some("Task resumed".to_string());
759 }
760 Ok(Some(applied))
761 }
762
763 async fn set_ttl(&self, task_id: &str, ttl_ms: u64) -> Result<bool> {
764 let Ok(mut tasks) = self.tasks.write() else {
765 return Ok(false);
766 };
767 let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
768 return Ok(false);
769 };
770 task.ttl = ttl_ms;
771 task.last_updated_at_str = chrono_now_iso8601();
772 Ok(true)
773 }
774
775 async fn complete_task(&self, task_id: &str, result: CallToolResult) -> Result<bool> {
776 let Ok(mut tasks) = self.tasks.write() else {
777 return Ok(false);
778 };
779 let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
780 return Ok(false);
781 };
782 if task.status.is_terminal() {
783 return Ok(false);
784 }
785 task.status = TaskStatus::Completed;
786 task.status_message = Some("Task completed".to_string());
787 task.result = Some(result);
788 task.input_requests.clear();
789 task.completed_at = Some(Instant::now());
790 task.last_updated_at_str = chrono_now_iso8601();
791 task.completion_notify.notify_waiters();
792 Ok(true)
793 }
794
795 async fn fail_task(&self, task_id: &str, error: JsonRpcError) -> Result<bool> {
796 let Ok(mut tasks) = self.tasks.write() else {
797 return Ok(false);
798 };
799 let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
800 return Ok(false);
801 };
802 if task.status.is_terminal() {
803 return Ok(false);
804 }
805 task.status = TaskStatus::Failed;
806 task.status_message = Some(format!("Task failed: {}", error.message));
807 task.error = Some(error);
808 task.input_requests.clear();
809 task.completed_at = Some(Instant::now());
810 task.last_updated_at_str = chrono_now_iso8601();
811 task.completion_notify.notify_waiters();
812 Ok(true)
813 }
814
815 async fn cancel_task(&self, task_id: &str, reason: Option<&str>) -> Result<Option<TaskObject>> {
816 let Ok(mut tasks) = self.tasks.write() else {
817 return Ok(None);
818 };
819 let Some(task) = tasks.get_mut(task_id).filter(|t| !t.is_expired()) else {
820 return Ok(None);
821 };
822
823 task.cancellation_token.cancel();
825
826 if !task.status.is_terminal() {
828 task.input_requests.clear();
829 task.status = TaskStatus::Cancelled;
830 task.status_message = Some(
831 reason
832 .map(|r| format!("Cancelled: {}", r))
833 .unwrap_or_else(|| "Task cancelled".to_string()),
834 );
835 task.completed_at = Some(Instant::now());
836 task.last_updated_at_str = chrono_now_iso8601();
837 task.completion_notify.notify_waiters();
838 }
839 Ok(Some(task.to_task_object()))
840 }
841}
842
843pub fn tasks_extension() -> crate::ExtensionDeclaration {
848 crate::ExtensionDeclaration::empty(crate::protocol::TASKS_EXTENSION_ID)
849 .expect("the built-in Tasks extension declaration is valid")
850}
851
852impl crate::McpRouter {
853 pub fn with_tasks(self) -> Self {
861 self.with_protocol_extension(tasks_extension())
862 }
863}
864
865impl crate::McpClientBuilder {
866 pub fn with_tasks(self) -> Self {
868 self.with_protocol_extension(tasks_extension())
869 }
870}
871
872impl crate::RequestContext {
873 pub fn supports_tasks(&self) -> bool {
879 self.negotiated_extensions()
880 .is_some_and(|extensions| extensions.contains(crate::protocol::TASKS_EXTENSION_ID))
881 }
882}
883
884fn chrono_now_iso8601() -> String {
886 use std::time::SystemTime;
887
888 let now = SystemTime::now();
889 let duration = now
890 .duration_since(SystemTime::UNIX_EPOCH)
891 .unwrap_or_default();
892
893 let secs = duration.as_secs();
894 let millis = duration.subsec_millis();
895
896 let days = secs / 86400;
899 let remaining = secs % 86400;
900 let hours = remaining / 3600;
901 let remaining = remaining % 3600;
902 let minutes = remaining / 60;
903 let seconds = remaining % 60;
904
905 let mut year = 1970i32;
908 let mut remaining_days = days as i32;
909
910 loop {
911 let days_in_year = if is_leap_year(year) { 366 } else { 365 };
912 if remaining_days < days_in_year {
913 break;
914 }
915 remaining_days -= days_in_year;
916 year += 1;
917 }
918
919 let days_in_months: [i32; 12] = if is_leap_year(year) {
920 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
921 } else {
922 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
923 };
924
925 let mut month = 1;
926 for days_in_month in days_in_months.iter() {
927 if remaining_days < *days_in_month {
928 break;
929 }
930 remaining_days -= days_in_month;
931 month += 1;
932 }
933
934 let day = remaining_days + 1;
935
936 format!(
937 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
938 year, month, day, hours, minutes, seconds, millis
939 )
940}
941
942fn is_leap_year(year: i32) -> bool {
943 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
944}
945
946#[cfg(test)]
947mod tests {
948 use super::*;
949 use crate::protocol::{
950 ElicitAction, ElicitResult, InputRequest, InputResponse, ListRootsParams,
951 };
952
953 #[tokio::test]
954 async fn test_create_task() {
955 let store = MemoryTaskStore::new();
956 let (id, token) = store
957 .create_task("test-tool", serde_json::json!({"a": 1}), None, None)
958 .await
959 .unwrap();
960
961 assert!(!id.is_empty());
962 assert!(!token.is_cancelled());
963
964 let info = store
965 .get_task(&id)
966 .await
967 .unwrap()
968 .expect("task should exist");
969 assert_eq!(info.task_id, id);
970 assert_eq!(info.status, TaskStatus::Working);
971 }
972
973 #[tokio::test]
974 async fn test_task_lifecycle() {
975 let store = MemoryTaskStore::new();
976 let (id, _) = store
977 .create_task("test-tool", serde_json::json!({}), None, None)
978 .await
979 .unwrap();
980
981 assert!(
983 store
984 .complete_task(&id, CallToolResult::text("Done"))
985 .await
986 .unwrap()
987 );
988
989 let info = store.get_task(&id).await.unwrap().unwrap();
990 assert_eq!(info.status, TaskStatus::Completed);
991 }
992
993 #[tokio::test]
994 async fn test_task_cancellation() {
995 let store = MemoryTaskStore::new();
996 let (id, token) = store
997 .create_task("test-tool", serde_json::json!({}), None, None)
998 .await
999 .unwrap();
1000
1001 assert!(!token.is_cancelled());
1002
1003 let task_obj = store
1004 .cancel_task(&id, Some("User requested"))
1005 .await
1006 .unwrap();
1007 assert!(task_obj.is_some());
1008 assert_eq!(task_obj.unwrap().status, TaskStatus::Cancelled);
1009 assert!(token.is_cancelled());
1010
1011 let info = store.get_task(&id).await.unwrap().unwrap();
1012 assert_eq!(info.status, TaskStatus::Cancelled);
1013 }
1014
1015 #[tokio::test]
1016 async fn test_task_failure() {
1017 let store = MemoryTaskStore::new();
1018 let (id, _) = store
1019 .create_task("test-tool", serde_json::json!({}), None, None)
1020 .await
1021 .unwrap();
1022
1023 assert!(
1024 store
1025 .fail_task(&id, JsonRpcError::internal_error("Something went wrong"))
1026 .await
1027 .unwrap()
1028 );
1029
1030 let info = store.get_task(&id).await.unwrap().unwrap();
1031 assert_eq!(info.status, TaskStatus::Failed);
1032 assert!(info.status_message.as_ref().unwrap().contains("failed"));
1033 }
1034
1035 #[tokio::test]
1036 async fn test_list_tasks() {
1037 let store = MemoryTaskStore::new();
1038 store
1039 .create_task("tool1", serde_json::json!({}), None, None)
1040 .await
1041 .unwrap();
1042 store
1043 .create_task("tool2", serde_json::json!({}), None, None)
1044 .await
1045 .unwrap();
1046 let (id3, _) = store
1047 .create_task("tool3", serde_json::json!({}), None, None)
1048 .await
1049 .unwrap();
1050
1051 store
1053 .complete_task(&id3, CallToolResult::text("Done"))
1054 .await
1055 .unwrap();
1056
1057 let all = store.list_tasks(None).await.unwrap();
1059 assert_eq!(all.len(), 3);
1060
1061 let working = store.list_tasks(Some(TaskStatus::Working)).await.unwrap();
1063 assert_eq!(working.len(), 2);
1064
1065 let completed = store.list_tasks(Some(TaskStatus::Completed)).await.unwrap();
1067 assert_eq!(completed.len(), 1);
1068 }
1069
1070 #[tokio::test]
1071 async fn test_terminal_state_immutable() {
1072 let store = MemoryTaskStore::new();
1073 let (id, _) = store
1074 .create_task("test-tool", serde_json::json!({}), None, None)
1075 .await
1076 .unwrap();
1077
1078 store
1080 .complete_task(&id, CallToolResult::text("Done"))
1081 .await
1082 .unwrap();
1083
1084 assert!(
1086 !store
1087 .fail_task(&id, JsonRpcError::internal_error("Error"))
1088 .await
1089 .unwrap()
1090 );
1091
1092 let info = store.get_task(&id).await.unwrap().unwrap();
1094 assert_eq!(info.status, TaskStatus::Completed);
1095 }
1096
1097 #[tokio::test]
1098 async fn test_task_ids_unique() {
1099 let store = MemoryTaskStore::new();
1100 let (id1, _) = store
1101 .create_task("tool", serde_json::json!({}), None, None)
1102 .await
1103 .unwrap();
1104 let (id2, _) = store
1105 .create_task("tool", serde_json::json!({}), None, None)
1106 .await
1107 .unwrap();
1108 let (id3, _) = store
1109 .create_task("tool", serde_json::json!({}), None, None)
1110 .await
1111 .unwrap();
1112
1113 assert_ne!(id1, id2);
1114 assert_ne!(id2, id3);
1115 assert_ne!(id1, id3);
1116 }
1117
1118 #[tokio::test]
1119 async fn test_get_task_result() {
1120 let store = MemoryTaskStore::new();
1121 let (id, _) = store
1122 .create_task("test-tool", serde_json::json!({}), None, None)
1123 .await
1124 .unwrap();
1125
1126 let result = CallToolResult::text("The result");
1128 store.complete_task(&id, result).await.unwrap();
1129
1130 let (task_obj, result, error) = store.get_task_result(&id).await.unwrap().unwrap();
1131 assert_eq!(task_obj.status, TaskStatus::Completed);
1132 assert!(result.is_some());
1133 assert!(error.is_none());
1134 }
1135
1136 #[tokio::test]
1137 async fn test_wait_for_completion_returns_terminal_snapshot() {
1138 let store = MemoryTaskStore::new();
1139 let (id, _) = store
1140 .create_task("test-tool", serde_json::json!({}), None, None)
1141 .await
1142 .unwrap();
1143
1144 let waiter_store = store.clone();
1146 let waiter_id = id.clone();
1147 let waiter =
1148 tokio::spawn(async move { waiter_store.wait_for_completion(&waiter_id).await });
1149
1150 tokio::time::sleep(Duration::from_millis(10)).await;
1151 store
1152 .complete_task(&id, CallToolResult::text("Done"))
1153 .await
1154 .unwrap();
1155
1156 let (task_obj, result, error) = waiter.await.unwrap().unwrap().unwrap();
1157 assert_eq!(task_obj.status, TaskStatus::Completed);
1158 assert!(result.is_some());
1159 assert!(error.is_none());
1160 }
1161
1162 #[tokio::test]
1163 async fn dyn_task_store_object_safe() {
1164 let store: Arc<dyn TaskStore> = Arc::new(MemoryTaskStore::new());
1166 let (id, _) = store
1167 .create_task("tool", serde_json::json!({}), None, None)
1168 .await
1169 .unwrap();
1170 assert!(store.get_task(&id).await.unwrap().is_some());
1171 }
1172
1173 #[test]
1174 fn test_iso8601_timestamp() {
1175 let ts = chrono_now_iso8601();
1176 assert!(ts.ends_with('Z'));
1178 assert!(ts.contains('T'));
1179 assert_eq!(ts.len(), 24); }
1181
1182 #[test]
1183 fn test_task_status_display() {
1184 assert_eq!(TaskStatus::Working.to_string(), "working");
1185 assert_eq!(TaskStatus::InputRequired.to_string(), "input_required");
1186 assert_eq!(TaskStatus::Completed.to_string(), "completed");
1187 assert_eq!(TaskStatus::Failed.to_string(), "failed");
1188 assert_eq!(TaskStatus::Cancelled.to_string(), "cancelled");
1189 }
1190
1191 #[test]
1192 fn test_task_status_is_terminal() {
1193 assert!(!TaskStatus::Working.is_terminal());
1194 assert!(!TaskStatus::InputRequired.is_terminal());
1195 assert!(TaskStatus::Completed.is_terminal());
1196 assert!(TaskStatus::Failed.is_terminal());
1197 assert!(TaskStatus::Cancelled.is_terminal());
1198 }
1199
1200 fn requests(keys: &[&str]) -> InputRequests {
1201 keys.iter()
1202 .map(|k| {
1203 (
1204 k.to_string(),
1205 InputRequest::ListRoots(ListRootsParams { meta: None }),
1206 )
1207 })
1208 .collect()
1209 }
1210
1211 fn accept(key: &str) -> (String, InputResponse) {
1212 (
1213 key.to_string(),
1214 InputResponse::Elicit(ElicitResult {
1215 action: ElicitAction::Accept,
1216 content: None,
1217 meta: None,
1218 }),
1219 )
1220 }
1221
1222 async fn working_task(store: &MemoryTaskStore, ttl: Option<u64>) -> String {
1223 store
1224 .create_task("tool", serde_json::json!({}), ttl, None)
1225 .await
1226 .unwrap()
1227 .0
1228 }
1229
1230 #[tokio::test]
1231 async fn task_ids_are_unguessable_not_sequential() {
1232 let store = MemoryTaskStore::new();
1233 let mut ids = BTreeSet::new();
1234 for _ in 0..64 {
1235 ids.insert(working_task(&store, None).await);
1236 }
1237 assert_eq!(ids.len(), 64, "task IDs collided");
1238
1239 for id in &ids {
1240 assert_eq!(id.len(), 32, "expected 128 bits of hex: {id}");
1241 assert!(id.chars().all(|c| c.is_ascii_hexdigit()), "{id}");
1242 assert!(!id.starts_with("task-"), "sequential-looking ID: {id}");
1243 }
1244
1245 let leading: BTreeSet<&str> = ids.iter().map(|id| &id[..2]).collect();
1248 assert!(
1249 leading.len() > 32,
1250 "only {} distinct leading bytes across 64 IDs",
1251 leading.len()
1252 );
1253 }
1254
1255 #[tokio::test]
1256 async fn ttl_runs_from_creation_and_expired_tasks_read_as_absent() {
1257 let store = MemoryTaskStore::new();
1258 let id = working_task(&store, Some(0)).await;
1259
1260 tokio::time::sleep(Duration::from_millis(5)).await;
1263
1264 assert!(store.get_task(&id).await.unwrap().is_none());
1265 assert!(store.get_task_result(&id).await.unwrap().is_none());
1266 assert!(store.list_tasks(None).await.unwrap().is_empty());
1267 assert!(
1268 store
1269 .outstanding_input_requests(&id)
1270 .await
1271 .unwrap()
1272 .is_none()
1273 );
1274 assert!(store.cancel_task(&id, None).await.unwrap().is_none());
1275 assert!(!store.set_ttl(&id, 60_000).await.unwrap());
1276 assert!(
1277 !store
1278 .complete_task(&id, CallToolResult::text("late"))
1279 .await
1280 .unwrap()
1281 );
1282 }
1283
1284 #[tokio::test]
1285 async fn ttl_is_mutable_over_the_task_lifetime() {
1286 let store = MemoryTaskStore::new();
1287 let id = working_task(&store, Some(60_000)).await;
1288
1289 assert!(store.set_ttl(&id, 120_000).await.unwrap());
1290 let task = store.get_task(&id).await.unwrap().unwrap();
1291 assert_eq!(task.ttl, Some(120_000));
1292
1293 assert!(store.set_ttl(&id, 0).await.unwrap());
1295 tokio::time::sleep(Duration::from_millis(5)).await;
1296 assert!(store.get_task(&id).await.unwrap().is_none());
1297 }
1298
1299 #[tokio::test]
1300 async fn require_input_records_requests_and_exposes_them() {
1301 let store = MemoryTaskStore::new();
1302 let id = working_task(&store, None).await;
1303
1304 assert!(
1305 store
1306 .require_input(&id, requests(&["approval", "region"]), Some("need input"))
1307 .await
1308 .unwrap()
1309 );
1310
1311 let task = store.get_task(&id).await.unwrap().unwrap();
1312 assert_eq!(task.status, TaskStatus::InputRequired);
1313 assert_eq!(task.status_message.as_deref(), Some("need input"));
1314
1315 let outstanding = store
1316 .outstanding_input_requests(&id)
1317 .await
1318 .unwrap()
1319 .unwrap();
1320 assert_eq!(
1321 outstanding.keys().collect::<Vec<_>>(),
1322 vec!["approval", "region"],
1323 "every outstanding request must be exposed, not just the newest"
1324 );
1325 }
1326
1327 #[tokio::test]
1328 async fn partial_input_responses_leave_the_rest_outstanding() {
1329 let store = MemoryTaskStore::new();
1330 let id = working_task(&store, None).await;
1331 store
1332 .require_input(&id, requests(&["approval", "region"]), None)
1333 .await
1334 .unwrap();
1335
1336 let applied = store
1337 .apply_input_responses(&id, [accept("approval")].into_iter().collect())
1338 .await
1339 .unwrap()
1340 .unwrap();
1341
1342 assert_eq!(applied.accepted, ["approval".to_string()].into());
1343 assert!(applied.ignored.is_empty());
1344 assert_eq!(applied.still_outstanding, ["region".to_string()].into());
1345 assert!(!applied.is_complete());
1346
1347 let task = store.get_task(&id).await.unwrap().unwrap();
1349 assert_eq!(task.status, TaskStatus::InputRequired);
1350 assert_eq!(
1351 store
1352 .outstanding_input_requests(&id)
1353 .await
1354 .unwrap()
1355 .unwrap()
1356 .keys()
1357 .collect::<Vec<_>>(),
1358 vec!["region"]
1359 );
1360
1361 let applied = store
1363 .apply_input_responses(&id, [accept("region")].into_iter().collect())
1364 .await
1365 .unwrap()
1366 .unwrap();
1367 assert!(applied.is_complete());
1368 assert_eq!(
1369 store.get_task(&id).await.unwrap().unwrap().status,
1370 TaskStatus::Working
1371 );
1372 }
1373
1374 #[tokio::test]
1375 async fn unknown_answered_and_superseded_response_keys_are_ignored() {
1376 let store = MemoryTaskStore::new();
1377 let id = working_task(&store, None).await;
1378 store
1379 .require_input(&id, requests(&["approval", "stale"]), None)
1380 .await
1381 .unwrap();
1382
1383 store
1385 .apply_input_responses(&id, [accept("approval")].into_iter().collect())
1386 .await
1387 .unwrap()
1388 .unwrap();
1389 store
1390 .require_input(&id, requests(&["region"]), None)
1391 .await
1392 .unwrap();
1393
1394 let applied = store
1395 .apply_input_responses(
1396 &id,
1397 [accept("never-issued"), accept("approval"), accept("stale")]
1398 .into_iter()
1399 .collect(),
1400 )
1401 .await
1402 .unwrap()
1403 .unwrap();
1404
1405 assert!(
1406 applied.accepted.is_empty(),
1407 "none of these keys are outstanding"
1408 );
1409 assert_eq!(
1410 applied.ignored,
1411 [
1412 "never-issued".to_string(),
1413 "approval".to_string(),
1414 "stale".to_string()
1415 ]
1416 .into(),
1417 "unknown, already-answered, and superseded keys are all ignored"
1418 );
1419 assert_eq!(applied.still_outstanding, ["region".to_string()].into());
1420 assert_eq!(
1421 store.get_task(&id).await.unwrap().unwrap().status,
1422 TaskStatus::InputRequired,
1423 "ignoring a stale update must not resume or fail the task"
1424 );
1425 }
1426
1427 #[tokio::test]
1428 async fn reissued_key_becomes_a_fresh_question() {
1429 let store = MemoryTaskStore::new();
1430 let id = working_task(&store, None).await;
1431 store
1432 .require_input(&id, requests(&["approval"]), None)
1433 .await
1434 .unwrap();
1435 store
1436 .apply_input_responses(&id, [accept("approval")].into_iter().collect())
1437 .await
1438 .unwrap()
1439 .unwrap();
1440
1441 store
1444 .require_input(&id, requests(&["approval"]), None)
1445 .await
1446 .unwrap();
1447 let applied = store
1448 .apply_input_responses(&id, [accept("approval")].into_iter().collect())
1449 .await
1450 .unwrap()
1451 .unwrap();
1452 assert_eq!(applied.accepted, ["approval".to_string()].into());
1453 assert!(applied.is_complete());
1454 }
1455
1456 #[tokio::test]
1457 async fn failed_tasks_preserve_the_structured_error() {
1458 let store = MemoryTaskStore::new();
1459 let id = working_task(&store, None).await;
1460
1461 let mut error = JsonRpcError::invalid_params("bad region");
1462 error.data = Some(serde_json::json!({"field": "region"}));
1463 assert!(store.fail_task(&id, error).await.unwrap());
1464
1465 let (_, result, error) = store.get_task_result(&id).await.unwrap().unwrap();
1466 assert!(result.is_none());
1467 let error = error.expect("structured error must survive the store");
1468 assert_eq!(
1469 error.code, -32602,
1470 "the original code must not be flattened"
1471 );
1472 assert_eq!(error.message, "bad region");
1473 assert_eq!(error.data.unwrap()["field"], "region");
1474 }
1475
1476 #[tokio::test]
1477 async fn tool_error_results_complete_the_task() {
1478 let store = MemoryTaskStore::new();
1479 let id = working_task(&store, None).await;
1480
1481 let mut result = CallToolResult::text("domain failure");
1482 result.is_error = true;
1483 assert!(store.complete_task(&id, result).await.unwrap());
1484
1485 let (task, result, error) = store.get_task_result(&id).await.unwrap().unwrap();
1486 assert_eq!(
1487 task.status,
1488 TaskStatus::Completed,
1489 "isError is a domain error, not an execution failure"
1490 );
1491 assert!(result.unwrap().is_error);
1492 assert!(error.is_none(), "no JSON-RPC error accompanies isError");
1493 }
1494
1495 #[tokio::test]
1496 async fn tasks_record_their_creating_principal() {
1497 let store = MemoryTaskStore::new();
1498 let (owned, _) = store
1499 .create_task("tool", serde_json::json!({}), None, Some("alice".into()))
1500 .await
1501 .unwrap();
1502 let (unowned, _) = store
1503 .create_task("tool", serde_json::json!({}), None, None)
1504 .await
1505 .unwrap();
1506
1507 assert_eq!(
1508 store.task_owner(&owned).await.unwrap(),
1509 Some(Some("alice".to_string()))
1510 );
1511 assert_eq!(store.task_owner(&unowned).await.unwrap(), Some(None));
1512 assert_eq!(
1513 store.task_owner("does-not-exist").await.unwrap(),
1514 None,
1515 "an unknown task has no owner record at all"
1516 );
1517
1518 let wire = serde_json::to_value(store.get_task(&owned).await.unwrap().unwrap()).unwrap();
1520 assert!(
1521 wire.get("owner").is_none(),
1522 "owner leaked to the wire: {wire}"
1523 );
1524 assert!(!wire.to_string().contains("alice"));
1525 }
1526
1527 #[test]
1528 fn owner_matching_is_equality_not_leniency() {
1529 assert!(owner_matches(&None, None), "no auth configured");
1530 assert!(owner_matches(&Some("alice".into()), Some("alice")));
1531
1532 assert!(
1533 !owner_matches(&Some("alice".into()), Some("bob")),
1534 "a different principal must not inherit the task"
1535 );
1536 assert!(
1537 !owner_matches(&Some("alice".into()), None),
1538 "dropping the token must not grant access"
1539 );
1540 assert!(
1541 !owner_matches(&None, Some("alice")),
1542 "an unowned task belongs to a different security context"
1543 );
1544 }
1545
1546 #[tokio::test]
1547 async fn terminal_states_clear_outstanding_requests() {
1548 for (label, terminate) in [("completed", true), ("cancelled", false)] {
1549 let store = MemoryTaskStore::new();
1550 let id = working_task(&store, None).await;
1551 store
1552 .require_input(&id, requests(&["approval"]), None)
1553 .await
1554 .unwrap();
1555
1556 if terminate {
1557 store
1558 .complete_task(&id, CallToolResult::text("done"))
1559 .await
1560 .unwrap();
1561 } else {
1562 store.cancel_task(&id, None).await.unwrap();
1563 }
1564
1565 assert!(
1566 store
1567 .outstanding_input_requests(&id)
1568 .await
1569 .unwrap()
1570 .unwrap()
1571 .is_empty(),
1572 "{label} task still advertises outstanding input requests"
1573 );
1574 assert!(
1575 store
1576 .apply_input_responses(&id, [accept("approval")].into_iter().collect())
1577 .await
1578 .unwrap()
1579 .is_none(),
1580 "{label} task accepted a late input response"
1581 );
1582 }
1583 }
1584}