turbomcp_protocol/types/
tasks.rs

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