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