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(¬ification).unwrap();
854 assert!(json.contains("\"taskId\":\"task-999\""));
855 assert!(json.contains("\"status\":\"completed\""));
856 assert!(json.contains("\"statusMessage\":\"Task finished successfully\""));
857 }
858}