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 /// Last update timestamp (ISO 8601). Spec field per
646 /// `TaskStatusNotificationParams = NotificationParams & Task`
647 /// (schema.ts:1490) — populated by spec-compliant peers; previously
648 /// silently dropped on deserialize.
649 #[serde(
650 rename = "lastUpdatedAt",
651 skip_serializing_if = "Option::is_none",
652 default
653 )]
654 pub last_updated_at: Option<String>,
655
656 /// Time-to-live in milliseconds
657 pub ttl: Option<u64>,
658
659 /// Suggested poll interval
660 #[serde(rename = "pollInterval", skip_serializing_if = "Option::is_none")]
661 pub poll_interval: Option<u64>,
662
663 /// Optional metadata
664 #[serde(skip_serializing_if = "Option::is_none")]
665 pub _meta: Option<HashMap<String, serde_json::Value>>,
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[test]
673 fn test_task_status_terminal() {
674 assert!(!TaskStatus::Working.is_terminal());
675 assert!(!TaskStatus::InputRequired.is_terminal());
676 assert!(TaskStatus::Completed.is_terminal());
677 assert!(TaskStatus::Failed.is_terminal());
678 assert!(TaskStatus::Cancelled.is_terminal());
679 }
680
681 #[test]
682 fn test_task_status_active() {
683 assert!(TaskStatus::Working.is_active());
684 assert!(TaskStatus::InputRequired.is_active());
685 assert!(!TaskStatus::Completed.is_active());
686 assert!(!TaskStatus::Failed.is_active());
687 assert!(!TaskStatus::Cancelled.is_active());
688 }
689
690 #[test]
691 fn test_task_status_transitions() {
692 // Working can transition to anything
693 assert!(TaskStatus::Working.can_transition_to(&TaskStatus::InputRequired));
694 assert!(TaskStatus::Working.can_transition_to(&TaskStatus::Completed));
695 assert!(TaskStatus::Working.can_transition_to(&TaskStatus::Failed));
696 assert!(TaskStatus::Working.can_transition_to(&TaskStatus::Cancelled));
697
698 // InputRequired can transition to anything
699 assert!(TaskStatus::InputRequired.can_transition_to(&TaskStatus::Working));
700 assert!(TaskStatus::InputRequired.can_transition_to(&TaskStatus::Completed));
701
702 // Terminal states cannot transition
703 assert!(!TaskStatus::Completed.can_transition_to(&TaskStatus::Working));
704 assert!(!TaskStatus::Failed.can_transition_to(&TaskStatus::Working));
705 assert!(!TaskStatus::Cancelled.can_transition_to(&TaskStatus::Working));
706 }
707
708 #[test]
709 fn test_task_status_serialization() {
710 assert_eq!(
711 serde_json::to_string(&TaskStatus::Working).unwrap(),
712 "\"working\""
713 );
714 assert_eq!(
715 serde_json::to_string(&TaskStatus::InputRequired).unwrap(),
716 "\"input_required\""
717 );
718 assert_eq!(
719 serde_json::to_string(&TaskStatus::Completed).unwrap(),
720 "\"completed\""
721 );
722 assert_eq!(
723 serde_json::to_string(&TaskStatus::Failed).unwrap(),
724 "\"failed\""
725 );
726 assert_eq!(
727 serde_json::to_string(&TaskStatus::Cancelled).unwrap(),
728 "\"cancelled\""
729 );
730 }
731
732 #[test]
733 fn test_task_serialization() {
734 let task = Task {
735 task_id: "task-123".to_string(),
736 status: TaskStatus::Working,
737 status_message: Some("Processing...".to_string()),
738 created_at: "2025-11-25T10:30:00Z".to_string(),
739 last_updated_at: "2025-11-25T10:30:00Z".to_string(),
740 ttl: Some(60000),
741 poll_interval: Some(5000),
742 };
743
744 let json = serde_json::to_string(&task).unwrap();
745 assert!(json.contains("\"taskId\":\"task-123\""));
746 assert!(json.contains("\"status\":\"working\""));
747 assert!(json.contains("\"statusMessage\":\"Processing...\""));
748 assert!(json.contains("\"createdAt\":\"2025-11-25T10:30:00Z\""));
749 assert!(json.contains("\"lastUpdatedAt\":\"2025-11-25T10:30:00Z\""));
750 assert!(json.contains("\"ttl\":60000"));
751 assert!(json.contains("\"pollInterval\":5000"));
752
753 // Verify deserialization
754 let deserialized: Task = serde_json::from_str(&json).unwrap();
755 assert_eq!(deserialized.task_id, "task-123");
756 assert_eq!(deserialized.status, TaskStatus::Working);
757 }
758
759 #[test]
760 fn test_task_metadata_serialization() {
761 let metadata = TaskMetadata { ttl: Some(300000) };
762
763 let json = serde_json::to_string(&metadata).unwrap();
764 assert!(json.contains("\"ttl\":300000"));
765
766 // Verify deserialization
767 let deserialized: TaskMetadata = serde_json::from_str(&json).unwrap();
768 assert_eq!(deserialized.ttl, Some(300000));
769
770 // Test with no TTL
771 let metadata = TaskMetadata { ttl: None };
772 let json = serde_json::to_string(&metadata).unwrap();
773 assert_eq!(json, "{}"); // Empty object when ttl is None
774 }
775
776 #[test]
777 fn test_related_task_metadata() {
778 let metadata = RelatedTaskMetadata {
779 task_id: "task-abc".to_string(),
780 };
781
782 let json = serde_json::to_string(&metadata).unwrap();
783 assert!(json.contains("\"taskId\":\"task-abc\""));
784
785 let deserialized: RelatedTaskMetadata = serde_json::from_str(&json).unwrap();
786 assert_eq!(deserialized.task_id, "task-abc");
787 }
788
789 #[test]
790 fn test_create_task_result() {
791 let result = CreateTaskResult {
792 task: Task {
793 task_id: "task-123".to_string(),
794 status: TaskStatus::Working,
795 status_message: None,
796 created_at: "2025-11-25T10:30:00Z".to_string(),
797 last_updated_at: "2025-11-25T10:30:00Z".to_string(),
798 ttl: Some(60000),
799 poll_interval: Some(5000),
800 },
801 _meta: None,
802 };
803
804 let json = serde_json::to_string(&result).unwrap();
805 assert!(json.contains("\"task\""));
806 assert!(json.contains("\"taskId\":\"task-123\""));
807 }
808
809 #[test]
810 fn test_get_task_request() {
811 let request = GetTaskRequest {
812 task_id: "task-456".to_string(),
813 };
814
815 let json = serde_json::to_string(&request).unwrap();
816 assert!(json.contains("\"taskId\":\"task-456\""));
817
818 let deserialized: GetTaskRequest = serde_json::from_str(&json).unwrap();
819 assert_eq!(deserialized.task_id, "task-456");
820 }
821
822 #[test]
823 fn test_list_tasks_result() {
824 let result = ListTasksResult {
825 tasks: vec![
826 Task {
827 task_id: "task-1".to_string(),
828 status: TaskStatus::Working,
829 status_message: None,
830 created_at: "2025-11-25T10:30:00Z".to_string(),
831 last_updated_at: "2025-11-25T10:30:00Z".to_string(),
832 ttl: Some(60000),
833 poll_interval: None,
834 },
835 Task {
836 task_id: "task-2".to_string(),
837 status: TaskStatus::Completed,
838 status_message: Some("Done".to_string()),
839 created_at: "2025-11-25T09:00:00Z".to_string(),
840 last_updated_at: "2025-11-25T09:30:00Z".to_string(),
841 ttl: Some(30000),
842 poll_interval: None,
843 },
844 ],
845 next_cursor: Some("next-page".to_string()),
846 _meta: None,
847 };
848
849 let json = serde_json::to_string(&result).unwrap();
850 assert!(json.contains("\"tasks\""));
851 assert!(json.contains("\"task-1\""));
852 assert!(json.contains("\"task-2\""));
853 assert!(json.contains("\"nextCursor\":\"next-page\""));
854 }
855
856 #[test]
857 fn test_cancel_task_request() {
858 let request = CancelTaskRequest {
859 task_id: "task-789".to_string(),
860 };
861
862 let json = serde_json::to_string(&request).unwrap();
863 assert!(json.contains("\"taskId\":\"task-789\""));
864 }
865
866 #[test]
867 fn test_task_status_notification() {
868 let notification = TaskStatusNotification {
869 task_id: "task-999".to_string(),
870 status: TaskStatus::Completed,
871 status_message: Some("Task finished successfully".to_string()),
872 created_at: "2025-11-25T10:30:00Z".to_string(),
873 last_updated_at: None,
874 ttl: Some(60000),
875 poll_interval: None,
876 _meta: None,
877 };
878
879 let json = serde_json::to_string(¬ification).unwrap();
880 assert!(json.contains("\"taskId\":\"task-999\""));
881 assert!(json.contains("\"status\":\"completed\""));
882 assert!(json.contains("\"statusMessage\":\"Task finished successfully\""));
883 }
884}