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(&notification).unwrap();
868        assert!(json.contains("\"taskId\":\"task-999\""));
869        assert!(json.contains("\"status\":\"completed\""));
870        assert!(json.contains("\"statusMessage\":\"Task finished successfully\""));
871    }
872}