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(¬ification).unwrap();
864 assert!(json.contains("\"taskId\":\"task-999\""));
865 assert!(json.contains("\"status\":\"completed\""));
866 assert!(json.contains("\"statusMessage\":\"Task finished successfully\""));
867 }
868}