1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use crate::types::{Artifact, Message};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum TaskState {
11 #[serde(rename = "TASK_STATE_UNSPECIFIED")]
13 Unspecified,
14 #[serde(rename = "TASK_STATE_SUBMITTED")]
16 Submitted,
17 #[serde(rename = "TASK_STATE_WORKING")]
19 Working,
20 #[serde(rename = "TASK_STATE_COMPLETED")]
22 Completed,
23 #[serde(rename = "TASK_STATE_FAILED")]
25 Failed,
26 #[serde(rename = "TASK_STATE_CANCELED")]
28 Canceled,
29 #[serde(rename = "TASK_STATE_REJECTED")]
31 Rejected,
32 #[serde(rename = "TASK_STATE_INPUT_REQUIRED")]
34 InputRequired,
35 #[serde(rename = "TASK_STATE_AUTH_REQUIRED")]
37 AuthRequired,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct TaskStatus {
43 pub state: TaskState,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub message: Option<Message>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub timestamp: Option<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55pub struct Task {
56 pub id: String,
58 #[serde(rename = "contextId", skip_serializing_if = "Option::is_none")]
60 pub context_id: Option<String>,
61 pub status: TaskStatus,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub artifacts: Option<Vec<Artifact>>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub history: Option<Vec<Message>>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub metadata: Option<HashMap<String, serde_json::Value>>,
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 fn submitted_task(id: &str) -> Task {
79 Task {
80 id: id.to_string(),
81 context_id: None,
82 status: TaskStatus {
83 state: TaskState::Submitted,
84 message: None,
85 timestamp: None,
86 },
87 artifacts: None,
88 history: None,
89 metadata: None,
90 }
91 }
92
93 #[test]
96 fn task_state_serializes_to_uppercase_snake() {
97 assert_eq!(
98 serde_json::to_string(&TaskState::Submitted).unwrap(),
99 r#""TASK_STATE_SUBMITTED""#
100 );
101 assert_eq!(
102 serde_json::to_string(&TaskState::Completed).unwrap(),
103 r#""TASK_STATE_COMPLETED""#
104 );
105 assert_eq!(
106 serde_json::to_string(&TaskState::Failed).unwrap(),
107 r#""TASK_STATE_FAILED""#
108 );
109 }
110
111 #[test]
112 fn all_task_states_roundtrip() {
113 let states = [
114 TaskState::Unspecified,
115 TaskState::Submitted,
116 TaskState::Working,
117 TaskState::Completed,
118 TaskState::Failed,
119 TaskState::Canceled,
120 TaskState::Rejected,
121 TaskState::InputRequired,
122 TaskState::AuthRequired,
123 ];
124 for state in states {
125 let json = serde_json::to_string(&state).unwrap();
126 let back: TaskState = serde_json::from_str(&json).unwrap();
127 assert_eq!(back, state);
128 }
129 }
130
131 #[test]
134 fn task_status_roundtrip() {
135 let status = TaskStatus {
136 state: TaskState::Working,
137 message: None,
138 timestamp: Some("2024-01-01T00:00:00Z".to_string()),
139 };
140 let json = serde_json::to_string(&status).unwrap();
141 let back: TaskStatus = serde_json::from_str(&json).unwrap();
142 assert_eq!(back, status);
143 }
144
145 #[test]
146 fn task_status_omits_optional_fields_when_none() {
147 let status = TaskStatus {
148 state: TaskState::Submitted,
149 message: None,
150 timestamp: None,
151 };
152 let json = serde_json::to_string(&status).unwrap();
153 assert!(!json.contains("message"));
154 assert!(!json.contains("timestamp"));
155 }
156
157 #[test]
160 fn task_roundtrip_minimal() {
161 let task = submitted_task("task-001");
162 let json = serde_json::to_string(&task).unwrap();
163 let back: Task = serde_json::from_str(&json).unwrap();
164 assert_eq!(back, task);
165 }
166
167 #[test]
168 fn task_optional_fields_omitted_when_none() {
169 let task = submitted_task("t1");
170 let json = serde_json::to_string(&task).unwrap();
171 assert!(!json.contains("contextId"));
172 assert!(!json.contains("artifacts"));
173 assert!(!json.contains("history"));
174 assert!(!json.contains("metadata"));
175 }
176
177 #[test]
178 fn task_json_uses_camel_case_context_id() {
179 let mut task = submitted_task("t2");
180 task.context_id = Some("ctx-1".to_string());
181 let json = serde_json::to_string(&task).unwrap();
182 assert!(json.contains("contextId"));
183 assert!(!json.contains("context_id"));
184 }
185
186 #[test]
187 fn task_with_history_roundtrip() {
188 use crate::types::Message;
189 let mut task = submitted_task("t3");
190 task.history = Some(vec![Message::user_text("hello")]);
191 let json = serde_json::to_string(&task).unwrap();
192 let back: Task = serde_json::from_str(&json).unwrap();
193 assert_eq!(back.history.as_ref().unwrap().len(), 1);
194 }
195}