turbomcp_protocol/types/tasks.rs
1//! Tasks API for durable long-running operations
2//!
3//! The Tasks API (MCP 2025-11-25) provides durable state machines for
4//! long-running operations, enabling requestor polling and deferred result retrieval.
5//!
6//! This is an official feature of the MCP 2025-11-25 specification, released on
7//! November 25, 2025. See the [official specification](https://modelcontextprotocol.io/specification/2025-11-25)
8//! for authoritative documentation.
9//!
10//! ## Overview
11//!
12//! Tasks enable:
13//! - **Durable state machines** - Long-running operations that outlive individual connections
14//! - **Requestor polling** - Clients can poll for completion status
15//! - **Deferred results** - Results available after task completion
16//! - **Input requests** - Tasks can request additional input during execution
17//! - **Bidirectional support** - Works for both client→server and server→client requests
18//!
19//! ## Key Concepts
20//!
21//! ### Task Lifecycle
22//!
23//! ```text
24//! [*] → working
25//! ↓
26//! ├─→ input_required ──┬─→ working ──→ terminal
27//! │ └─→ terminal
28//! │
29//! └─→ terminal
30//!
31//! Terminal states: completed, failed, cancelled
32//! ```
33//!
34//! ### Supported Requests
35//!
36//! **Client → Server** (Server as receiver):
37//! - `tools/call` - Long-running tool execution
38//!
39//! **Server → Client** (Client as receiver):
40//! - `sampling/createMessage` - LLM inference operations
41//! - `elicitation/create` - User input collection
42//!
43//! ## Usage Example
44//!
45//! ```rust,no_run
46//! use turbomcp_protocol::types::tasks::{Task, TaskStatus, TaskMetadata, CreateTaskResult};
47//! use turbomcp_protocol::types::CallToolRequest;
48//! use std::collections::HashMap;
49//! use serde_json::json;
50//!
51//! // Client requests task-augmented tool call
52//! let mut arguments = HashMap::new();
53//! arguments.insert("data".to_string(), json!("large_dataset"));
54//! let request = CallToolRequest {
55//! name: "long_running_analysis".to_string(),
56//! arguments: Some(arguments),
57//! task: Some(TaskMetadata {
58//! ttl: Some(300_000), // 5 minute lifetime
59//! }),
60//! _meta: None,
61//! };
62//!
63//! // Server responds immediately with task
64//! let response = CreateTaskResult {
65//! task: Task {
66//! task_id: "task-123".to_string(),
67//! status: TaskStatus::Working,
68//! status_message: None,
69//! created_at: "2025-11-25T10:30:00Z".to_string(),
70//! last_updated_at: "2025-11-25T10:30:00Z".to_string(),
71//! ttl: Some(300_000),
72//! poll_interval: Some(5_000), // Poll every 5s
73//! },
74//! _meta: None,
75//! };
76//!
77//! // Client polls for status
78//! // ... tasks/get request ...
79//!
80//! // When completed, retrieve results
81//! // ... tasks/result request ...
82//! ```
83//!
84//! ## Security Considerations
85//!
86//! ### Task ID Access Control
87//!
88//! Task IDs are the **primary access control mechanism**. Implementations MUST:
89//!
90//! 1. **Bind to authorization context** - Reject operations from different contexts
91//! 2. **Use cryptographic entropy** - Task IDs must be unpredictable (use UUID v4)
92//! 3. **Enforce TTL limits** - Shorter TTLs reduce exposure windows
93//! 4. **Audit access** - Log all task operations for security monitoring
94//!
95//! ### Resource Management
96//!
97//! Implementations SHOULD:
98//! - Enforce concurrent task limits per requestor
99//! - Enforce maximum TTL durations
100//! - Clean up expired tasks promptly
101//! - Implement rate limiting on task operations
102
103use serde::{Deserialize, Serialize};
104use std::collections::HashMap;
105
106/// Task status representing the current state of a long-running operation
107///
108/// ## State Transitions
109///
110/// Valid transitions:
111/// - `Working` → `InputRequired`, `Completed`, `Failed`, `Cancelled`
112/// - `InputRequired` → `Working`, `Completed`, `Failed`, `Cancelled`
113/// - Terminal states (`Completed`, `Failed`, `Cancelled`) → **NO TRANSITIONS**
114///
115/// ## Examples
116///
117/// ```rust
118/// use turbomcp_protocol::types::tasks::TaskStatus;
119///
120/// let status = TaskStatus::Working;
121/// assert!(!status.is_terminal());
122///
123/// let status = TaskStatus::Completed;
124/// assert!(status.is_terminal());
125/// ```
126#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
127#[serde(rename_all = "snake_case")]
128pub enum TaskStatus {
129 /// Request is currently being processed
130 Working,
131
132 /// Task requires additional input from requestor (e.g., user confirmation)
133 ///
134 /// When in this state:
135 /// - Requestor should call `tasks/result` which will receive input requests
136 /// - All input requests MUST include `io.modelcontextprotocol/related-task` metadata
137 /// - After providing input, task transitions back to `Working`
138 #[serde(rename = "input_required")]
139 InputRequired,
140
141 /// Request completed successfully
142 ///
143 /// This is a terminal state - no further transitions allowed.
144 Completed,
145
146 /// Request did not complete successfully
147 ///
148 /// This is a terminal state. The `status_message` field typically contains
149 /// diagnostic information about the failure.
150 Failed,
151
152 /// Request was cancelled before completion
153 ///
154 /// This is a terminal state. The `status_message` field may contain the
155 /// reason for cancellation.
156 Cancelled,
157}
158
159impl TaskStatus {
160 /// Check if this status is terminal (no further transitions allowed)
161 ///
162 /// Terminal states: `Completed`, `Failed`, `Cancelled`
163 pub fn is_terminal(&self) -> bool {
164 matches!(
165 self,
166 TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Cancelled
167 )
168 }
169
170 /// Check if this status indicates the task is still active
171 ///
172 /// Active states: `Working`, `InputRequired`
173 pub fn is_active(&self) -> bool {
174 !self.is_terminal()
175 }
176
177 /// Check if task can transition to the given status
178 ///
179 /// # Examples
180 ///
181 /// ```rust
182 /// use turbomcp_protocol::types::tasks::TaskStatus;
183 ///
184 /// let working = TaskStatus::Working;
185 /// assert!(working.can_transition_to(&TaskStatus::Completed));
186 /// assert!(working.can_transition_to(&TaskStatus::InputRequired));
187 ///
188 /// let completed = TaskStatus::Completed;
189 /// assert!(!completed.can_transition_to(&TaskStatus::Working)); // Terminal
190 /// ```
191 pub fn can_transition_to(&self, _next: &TaskStatus) -> bool {
192 match self {
193 TaskStatus::Working => true, // Can transition to any state
194 TaskStatus::InputRequired => true, // Can transition to any state
195 TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Cancelled => false, // Terminal
196 }
197 }
198}
199
200/// Core task type representing a long-running operation
201///
202/// ## Fields
203///
204/// - `task_id`: Unique identifier (MUST be cryptographically secure)
205/// - `status`: Current task state
206/// - `status_message`: Optional human-readable status (any state)
207/// - `created_at`: ISO 8601 timestamp of creation
208/// - `last_updated_at`: ISO 8601 timestamp when task was last updated
209/// - `ttl`: Time-to-live in milliseconds from creation (null = unlimited)
210/// - `poll_interval`: Suggested polling interval in milliseconds
211///
212/// ## TTL Behavior
213///
214/// TTL is measured from `created_at`, not from last update:
215///
216/// ```text
217/// Creation: 10:00:00, TTL: 60000ms (60s)
218/// Expiry: 10:01:00 (regardless of updates)
219/// ```
220///
221/// After TTL expiry, the receiver MAY delete the task and its results.
222///
223/// ## Examples
224///
225/// ```rust
226/// use turbomcp_protocol::types::tasks::{Task, TaskStatus};
227///
228/// let task = Task {
229/// task_id: "task-123".to_string(),
230/// status: TaskStatus::Working,
231/// status_message: Some("Processing data...".to_string()),
232/// created_at: "2025-11-25T10:30:00Z".to_string(),
233/// last_updated_at: "2025-11-25T10:30:00Z".to_string(),
234/// ttl: Some(300_000), // 5 minutes
235/// poll_interval: Some(5_000), // Poll every 5s
236/// };
237///
238/// assert!(!task.status.is_terminal());
239/// assert_eq!(task.ttl, Some(300_000));
240/// ```
241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
242pub struct Task {
243 /// Unique identifier for this task
244 ///
245 /// MUST be generated by receiver with cryptographic entropy (e.g., UUID v4).
246 /// Task IDs are the primary access control mechanism.
247 #[serde(rename = "taskId")]
248 pub task_id: String,
249
250 /// Current task status
251 pub status: TaskStatus,
252
253 /// Optional human-readable status message
254 ///
255 /// Usage by status:
256 /// - `Cancelled`: Reason for cancellation
257 /// - `Completed`: Summary of results
258 /// - `Failed`: Diagnostic info, error details
259 /// - `Working`/`InputRequired`: Progress updates
260 #[serde(rename = "statusMessage", skip_serializing_if = "Option::is_none")]
261 pub status_message: Option<String>,
262
263 /// ISO 8601 timestamp when task was created
264 ///
265 /// Format: `YYYY-MM-DDTHH:MM:SSZ` (UTC)
266 /// TTL is measured from this timestamp.
267 #[serde(rename = "createdAt")]
268 pub created_at: String,
269
270 /// ISO 8601 timestamp when task was last updated
271 ///
272 /// Format: `YYYY-MM-DDTHH:MM:SSZ` (UTC)
273 /// Updated whenever task status or other fields change.
274 #[serde(rename = "lastUpdatedAt")]
275 pub last_updated_at: String,
276
277 /// Time-to-live in milliseconds from creation
278 ///
279 /// - `Some(ms)`: Task expires after this duration from `created_at`
280 /// - `None`: Unlimited retention (use with caution)
281 ///
282 /// After expiry, receiver MAY delete task and results.
283 /// Shorter TTLs improve security by reducing task ID exposure.
284 pub ttl: Option<u64>,
285
286 /// Suggested polling interval in milliseconds
287 ///
288 /// Requestors SHOULD respect this value to avoid excessive polling.
289 /// Receivers MAY adjust based on task complexity and load.
290 #[serde(rename = "pollInterval", skip_serializing_if = "Option::is_none")]
291 pub poll_interval: Option<u64>,
292}
293
294/// Metadata for requesting task augmentation on a request
295///
296/// Include this in request parameters to augment the request with task support:
297///
298/// ```rust
299/// use turbomcp_protocol::types::tasks::TaskMetadata;
300/// use turbomcp_protocol::types::CallToolRequest;
301/// use std::collections::HashMap;
302/// use serde_json::json;
303///
304/// let mut arguments = HashMap::new();
305/// arguments.insert("data".to_string(), json!("value"));
306/// let request = CallToolRequest {
307/// name: "long_tool".to_string(),
308/// arguments: Some(arguments),
309/// task: Some(TaskMetadata {
310/// ttl: Some(300_000), // Request 5 minute lifetime
311/// }),
312/// _meta: None,
313/// };
314/// ```
315///
316/// ## TTL Negotiation
317///
318/// The receiver MAY override the requested TTL. Check the actual `ttl` value
319/// in the returned `Task` object.
320#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
321pub struct TaskMetadata {
322 /// Requested time-to-live in milliseconds from creation
323 ///
324 /// - Receiver MAY override this value
325 /// - Omit for server default TTL
326 /// - Use `null` (or omit) for unlimited (if server supports)
327 #[serde(skip_serializing_if = "Option::is_none")]
328 pub ttl: Option<u64>,
329}
330
331/// Metadata for associating messages with a task
332///
333/// Used in `_meta` field to link messages to a specific task during `input_required` state.
334///
335/// ## Usage
336///
337/// All messages during input_required MUST include this metadata:
338///
339/// ```json
340/// {
341/// "_meta": {
342/// "io.modelcontextprotocol/related-task": {
343/// "taskId": "task-123"
344/// }
345/// }
346/// }
347/// ```
348#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
349pub struct RelatedTaskMetadata {
350 /// Task ID this message is associated with
351 ///
352 /// MUST match the task ID across all related messages.
353 #[serde(rename = "taskId")]
354 pub task_id: String,
355}
356
357/// Result type for task creation (immediate response to task-augmented requests)
358///
359/// When a request is augmented with `task` metadata, the receiver responds immediately
360/// with this result containing the task object. The actual operation result is available
361/// later via `tasks/result`.
362///
363/// ## Two-Phase Response Pattern
364///
365/// ```text
366/// Phase 1 (Immediate):
367/// Client → tools/call (task: {...})
368/// Server → CreateTaskResult (task with status: working)
369///
370/// Phase 2 (Deferred):
371/// Client → tasks/result (taskId)
372/// Server → CallToolResult (actual tool response)
373/// ```
374///
375/// ## Examples
376///
377/// ```rust
378/// use turbomcp_protocol::types::tasks::{CreateTaskResult, Task, TaskStatus};
379///
380/// let response = CreateTaskResult {
381/// task: Task {
382/// task_id: "task-abc123".to_string(),
383/// status: TaskStatus::Working,
384/// status_message: None,
385/// created_at: "2025-11-25T10:30:00Z".to_string(),
386/// last_updated_at: "2025-11-25T10:30:00Z".to_string(),
387/// ttl: Some(60_000),
388/// poll_interval: Some(5_000),
389/// },
390/// _meta: None,
391/// };
392/// ```
393#[derive(Debug, Clone, Serialize, Deserialize)]
394pub struct CreateTaskResult {
395 /// The created task with initial state (typically `Working`)
396 pub task: Task,
397
398 /// Optional metadata
399 ///
400 /// Host applications can use `io.modelcontextprotocol/model-immediate-response`
401 /// to provide immediate feedback to the model before task completion.
402 #[serde(skip_serializing_if = "Option::is_none")]
403 pub _meta: Option<HashMap<String, serde_json::Value>>,
404}
405
406// ========== Task Method Request/Response Types ==========
407
408/// Request to retrieve task status
409///
410/// Poll for task completion and status updates.
411///
412/// ## Usage
413///
414/// ```rust
415/// use turbomcp_protocol::types::tasks::GetTaskRequest;
416///
417/// let request = GetTaskRequest {
418/// task_id: "task-123".to_string(),
419/// };
420/// ```
421///
422/// ## Errors
423///
424/// - Invalid taskId: JSON-RPC error -32602 (Invalid params)
425/// - Task expired: JSON-RPC error -32602
426/// - Unauthorized: JSON-RPC error -32602 (if different auth context)
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct GetTaskRequest {
429 /// Task identifier to query
430 #[serde(rename = "taskId")]
431 pub task_id: String,
432}
433
434/// Response from tasks/get containing current task status
435///
436/// This is a type alias - the response is a `Task` object with all current information.
437pub type GetTaskResult = Task;
438
439/// Request to retrieve task results (or receive input requests during input_required)
440///
441/// ## Blocking Behavior
442///
443/// - **Terminal states** (`Completed`, `Failed`, `Cancelled`): Returns immediately
444/// - **Non-terminal states** (`Working`, `InputRequired`): **BLOCKS** until terminal
445///
446/// During `InputRequired` state, this request may receive input requests from the receiver
447/// (e.g., elicitation/create) before finally returning the result.
448///
449/// ## Usage
450///
451/// ```rust
452/// use turbomcp_protocol::types::tasks::GetTaskPayloadRequest;
453///
454/// let request = GetTaskPayloadRequest {
455/// task_id: "task-123".to_string(),
456/// };
457/// ```
458///
459/// ## Errors
460///
461/// Same as GetTaskRequest
462#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct GetTaskPayloadRequest {
464 /// Task identifier to retrieve results for
465 #[serde(rename = "taskId")]
466 pub task_id: String,
467}
468
469/// Response from tasks/result containing the actual operation result
470///
471/// The structure matches the original request type:
472/// - For `tools/call` task: `CallToolResult`
473/// - For `sampling/createMessage` task: `CreateMessageResult`
474/// - For `elicitation/create` task: `ElicitResult`
475///
476/// The `_meta` field SHOULD include `io.modelcontextprotocol/related-task` metadata.
477///
478/// ## Examples
479///
480/// ```json
481/// {
482/// "content": [{"type": "text", "text": "Result data"}],
483/// "isError": false,
484/// "_meta": {
485/// "io.modelcontextprotocol/related-task": {
486/// "taskId": "task-123"
487/// }
488/// }
489/// }
490/// ```
491#[derive(Debug, Clone, Serialize, Deserialize)]
492pub struct GetTaskPayloadResult {
493 /// Dynamic result content (structure depends on original request type)
494 #[serde(flatten)]
495 pub result: serde_json::Value,
496
497 /// Optional metadata (SHOULD include related-task)
498 #[serde(skip_serializing_if = "Option::is_none")]
499 pub _meta: Option<HashMap<String, serde_json::Value>>,
500}
501
502/// Request to list all tasks (with pagination)
503///
504/// Returns a paginated list of tasks. Use `cursor` for pagination.
505///
506/// ## Usage
507///
508/// ```rust
509/// use turbomcp_protocol::types::tasks::ListTasksRequest;
510///
511/// // First page
512/// let request = ListTasksRequest {
513/// cursor: None,
514/// limit: None,
515/// };
516///
517/// // Subsequent pages with custom limit
518/// let request = ListTasksRequest {
519/// cursor: Some("next-page-cursor".to_string()),
520/// limit: Some(50),
521/// };
522/// ```
523#[derive(Debug, Clone, Serialize, Deserialize, Default)]
524pub struct ListTasksRequest {
525 /// Opaque pagination cursor
526 ///
527 /// - Omit for first page
528 /// - Use `nextCursor` from previous response for subsequent pages
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub cursor: Option<String>,
531 /// Maximum number of tasks to return
532 ///
533 /// - Omit for server default (typically 100)
534 /// - Values > 1000 may be truncated by server
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub limit: Option<usize>,
537}
538
539/// Response from tasks/list containing paginated task list
540///
541/// ## Pagination
542///
543/// If `next_cursor` is present, more tasks are available:
544///
545/// ```rust
546/// use turbomcp_protocol::types::tasks::ListTasksResult;
547///
548/// let response = ListTasksResult {
549/// tasks: vec![/* tasks */],
550/// next_cursor: Some("next-page".to_string()),
551/// _meta: None,
552/// };
553///
554/// if response.next_cursor.is_some() {
555/// // More pages available
556/// }
557/// ```
558#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct ListTasksResult {
560 /// Array of tasks (may be empty)
561 pub tasks: Vec<Task>,
562
563 /// Opaque cursor for next page (if more results available)
564 #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
565 pub next_cursor: Option<String>,
566
567 /// Optional metadata
568 #[serde(skip_serializing_if = "Option::is_none")]
569 pub _meta: Option<HashMap<String, serde_json::Value>>,
570}
571
572/// Request to cancel a task
573///
574/// Attempt to cancel a running task. This is a **best-effort** operation.
575///
576/// ## Behavior
577///
578/// - Receiver MAY ignore cancellation for tasks that cannot be interrupted
579/// - Terminal tasks cannot be cancelled (returns error -32602)
580/// - Successful cancellation transitions task to `Cancelled` status
581///
582/// ## Usage
583///
584/// ```rust
585/// use turbomcp_protocol::types::tasks::CancelTaskRequest;
586///
587/// let request = CancelTaskRequest {
588/// task_id: "task-123".to_string(),
589/// };
590/// ```
591///
592/// ## Errors
593///
594/// - Invalid taskId: -32602
595/// - Already terminal: -32602 ("Cannot cancel task: already in terminal status")
596/// - Unauthorized: -32602
597#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct CancelTaskRequest {
599 /// Task identifier to cancel
600 #[serde(rename = "taskId")]
601 pub task_id: String,
602}
603
604/// Response from tasks/cancel containing updated task with cancelled status
605///
606/// This is a type alias - the response is a `Task` object with `status: Cancelled`.
607pub type CancelTaskResult = Task;
608
609/// Task status change notification (optional, not required by spec)
610///
611/// Receivers MAY send notifications when task status changes, but requestors
612/// MUST NOT rely on these - they must continue polling via `tasks/get`.
613///
614/// ## Usage
615///
616/// ```json
617/// {
618/// "jsonrpc": "2.0",
619/// "method": "notifications/tasks/status",
620/// "params": {
621/// "taskId": "task-123",
622/// "status": "completed",
623/// "createdAt": "2025-11-25T10:30:00Z",
624/// "ttl": 60000
625/// }
626/// }
627/// ```
628#[derive(Debug, Clone, Serialize, Deserialize)]
629pub struct TaskStatusNotification {
630 /// Task ID this notification is for
631 #[serde(rename = "taskId")]
632 pub task_id: String,
633
634 /// New task status
635 pub status: TaskStatus,
636
637 /// Optional status message
638 #[serde(rename = "statusMessage", skip_serializing_if = "Option::is_none")]
639 pub status_message: Option<String>,
640
641 /// Task creation timestamp (ISO 8601)
642 #[serde(rename = "createdAt")]
643 pub created_at: String,
644
645 /// Time-to-live in milliseconds
646 pub ttl: Option<u64>,
647
648 /// Suggested poll interval
649 #[serde(rename = "pollInterval", skip_serializing_if = "Option::is_none")]
650 pub poll_interval: Option<u64>,
651
652 /// Optional metadata
653 #[serde(skip_serializing_if = "Option::is_none")]
654 pub _meta: Option<HashMap<String, serde_json::Value>>,
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660
661 #[test]
662 fn test_task_status_terminal() {
663 assert!(!TaskStatus::Working.is_terminal());
664 assert!(!TaskStatus::InputRequired.is_terminal());
665 assert!(TaskStatus::Completed.is_terminal());
666 assert!(TaskStatus::Failed.is_terminal());
667 assert!(TaskStatus::Cancelled.is_terminal());
668 }
669
670 #[test]
671 fn test_task_status_active() {
672 assert!(TaskStatus::Working.is_active());
673 assert!(TaskStatus::InputRequired.is_active());
674 assert!(!TaskStatus::Completed.is_active());
675 assert!(!TaskStatus::Failed.is_active());
676 assert!(!TaskStatus::Cancelled.is_active());
677 }
678
679 #[test]
680 fn test_task_status_transitions() {
681 // Working can transition to anything
682 assert!(TaskStatus::Working.can_transition_to(&TaskStatus::InputRequired));
683 assert!(TaskStatus::Working.can_transition_to(&TaskStatus::Completed));
684 assert!(TaskStatus::Working.can_transition_to(&TaskStatus::Failed));
685 assert!(TaskStatus::Working.can_transition_to(&TaskStatus::Cancelled));
686
687 // InputRequired can transition to anything
688 assert!(TaskStatus::InputRequired.can_transition_to(&TaskStatus::Working));
689 assert!(TaskStatus::InputRequired.can_transition_to(&TaskStatus::Completed));
690
691 // Terminal states cannot transition
692 assert!(!TaskStatus::Completed.can_transition_to(&TaskStatus::Working));
693 assert!(!TaskStatus::Failed.can_transition_to(&TaskStatus::Working));
694 assert!(!TaskStatus::Cancelled.can_transition_to(&TaskStatus::Working));
695 }
696
697 #[test]
698 fn test_task_status_serialization() {
699 assert_eq!(
700 serde_json::to_string(&TaskStatus::Working).unwrap(),
701 "\"working\""
702 );
703 assert_eq!(
704 serde_json::to_string(&TaskStatus::InputRequired).unwrap(),
705 "\"input_required\""
706 );
707 assert_eq!(
708 serde_json::to_string(&TaskStatus::Completed).unwrap(),
709 "\"completed\""
710 );
711 assert_eq!(
712 serde_json::to_string(&TaskStatus::Failed).unwrap(),
713 "\"failed\""
714 );
715 assert_eq!(
716 serde_json::to_string(&TaskStatus::Cancelled).unwrap(),
717 "\"cancelled\""
718 );
719 }
720
721 #[test]
722 fn test_task_serialization() {
723 let task = Task {
724 task_id: "task-123".to_string(),
725 status: TaskStatus::Working,
726 status_message: Some("Processing...".to_string()),
727 created_at: "2025-11-25T10:30:00Z".to_string(),
728 last_updated_at: "2025-11-25T10:30:00Z".to_string(),
729 ttl: Some(60000),
730 poll_interval: Some(5000),
731 };
732
733 let json = serde_json::to_string(&task).unwrap();
734 assert!(json.contains("\"taskId\":\"task-123\""));
735 assert!(json.contains("\"status\":\"working\""));
736 assert!(json.contains("\"statusMessage\":\"Processing...\""));
737 assert!(json.contains("\"createdAt\":\"2025-11-25T10:30:00Z\""));
738 assert!(json.contains("\"lastUpdatedAt\":\"2025-11-25T10:30:00Z\""));
739 assert!(json.contains("\"ttl\":60000"));
740 assert!(json.contains("\"pollInterval\":5000"));
741
742 // Verify deserialization
743 let deserialized: Task = serde_json::from_str(&json).unwrap();
744 assert_eq!(deserialized.task_id, "task-123");
745 assert_eq!(deserialized.status, TaskStatus::Working);
746 }
747
748 #[test]
749 fn test_task_metadata_serialization() {
750 let metadata = TaskMetadata { ttl: Some(300000) };
751
752 let json = serde_json::to_string(&metadata).unwrap();
753 assert!(json.contains("\"ttl\":300000"));
754
755 // Verify deserialization
756 let deserialized: TaskMetadata = serde_json::from_str(&json).unwrap();
757 assert_eq!(deserialized.ttl, Some(300000));
758
759 // Test with no TTL
760 let metadata = TaskMetadata { ttl: None };
761 let json = serde_json::to_string(&metadata).unwrap();
762 assert_eq!(json, "{}"); // Empty object when ttl is None
763 }
764
765 #[test]
766 fn test_related_task_metadata() {
767 let metadata = RelatedTaskMetadata {
768 task_id: "task-abc".to_string(),
769 };
770
771 let json = serde_json::to_string(&metadata).unwrap();
772 assert!(json.contains("\"taskId\":\"task-abc\""));
773
774 let deserialized: RelatedTaskMetadata = serde_json::from_str(&json).unwrap();
775 assert_eq!(deserialized.task_id, "task-abc");
776 }
777
778 #[test]
779 fn test_create_task_result() {
780 let result = CreateTaskResult {
781 task: Task {
782 task_id: "task-123".to_string(),
783 status: TaskStatus::Working,
784 status_message: None,
785 created_at: "2025-11-25T10:30:00Z".to_string(),
786 last_updated_at: "2025-11-25T10:30:00Z".to_string(),
787 ttl: Some(60000),
788 poll_interval: Some(5000),
789 },
790 _meta: None,
791 };
792
793 let json = serde_json::to_string(&result).unwrap();
794 assert!(json.contains("\"task\""));
795 assert!(json.contains("\"taskId\":\"task-123\""));
796 }
797
798 #[test]
799 fn test_get_task_request() {
800 let request = GetTaskRequest {
801 task_id: "task-456".to_string(),
802 };
803
804 let json = serde_json::to_string(&request).unwrap();
805 assert!(json.contains("\"taskId\":\"task-456\""));
806
807 let deserialized: GetTaskRequest = serde_json::from_str(&json).unwrap();
808 assert_eq!(deserialized.task_id, "task-456");
809 }
810
811 #[test]
812 fn test_list_tasks_result() {
813 let result = ListTasksResult {
814 tasks: vec![
815 Task {
816 task_id: "task-1".to_string(),
817 status: TaskStatus::Working,
818 status_message: None,
819 created_at: "2025-11-25T10:30:00Z".to_string(),
820 last_updated_at: "2025-11-25T10:30:00Z".to_string(),
821 ttl: Some(60000),
822 poll_interval: None,
823 },
824 Task {
825 task_id: "task-2".to_string(),
826 status: TaskStatus::Completed,
827 status_message: Some("Done".to_string()),
828 created_at: "2025-11-25T09:00:00Z".to_string(),
829 last_updated_at: "2025-11-25T09:30:00Z".to_string(),
830 ttl: Some(30000),
831 poll_interval: None,
832 },
833 ],
834 next_cursor: Some("next-page".to_string()),
835 _meta: None,
836 };
837
838 let json = serde_json::to_string(&result).unwrap();
839 assert!(json.contains("\"tasks\""));
840 assert!(json.contains("\"task-1\""));
841 assert!(json.contains("\"task-2\""));
842 assert!(json.contains("\"nextCursor\":\"next-page\""));
843 }
844
845 #[test]
846 fn test_cancel_task_request() {
847 let request = CancelTaskRequest {
848 task_id: "task-789".to_string(),
849 };
850
851 let json = serde_json::to_string(&request).unwrap();
852 assert!(json.contains("\"taskId\":\"task-789\""));
853 }
854
855 #[test]
856 fn test_task_status_notification() {
857 let notification = TaskStatusNotification {
858 task_id: "task-999".to_string(),
859 status: TaskStatus::Completed,
860 status_message: Some("Task finished successfully".to_string()),
861 created_at: "2025-11-25T10:30:00Z".to_string(),
862 ttl: Some(60000),
863 poll_interval: None,
864 _meta: None,
865 };
866
867 let json = serde_json::to_string(¬ification).unwrap();
868 assert!(json.contains("\"taskId\":\"task-999\""));
869 assert!(json.contains("\"status\":\"completed\""));
870 assert!(json.contains("\"statusMessage\":\"Task finished successfully\""));
871 }
872}