1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ResultMessage {
7 pub subtype: ResultSubtype,
8 pub is_error: bool,
9 pub duration_ms: u64,
10 pub duration_api_ms: u64,
11 pub num_turns: i32,
12
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub result: Option<String>,
15
16 pub session_id: String,
17 pub total_cost_usd: f64,
18
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub usage: Option<UsageInfo>,
21
22 #[serde(default)]
24 pub permission_denials: Vec<PermissionDenial>,
25
26 #[serde(default)]
31 pub errors: Vec<String>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub uuid: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct PermissionDenial {
43 pub tool_name: String,
45
46 pub tool_input: Value,
48
49 pub tool_use_id: String,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum ResultSubtype {
57 Success,
58 ErrorMaxTurns,
59 ErrorDuringExecution,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct UsageInfo {
65 pub input_tokens: u32,
66 pub cache_creation_input_tokens: u32,
67 pub cache_read_input_tokens: u32,
68 pub output_tokens: u32,
69 pub server_tool_use: ServerToolUse,
70 pub service_tier: String,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ServerToolUse {
76 pub web_search_requests: u32,
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::io::ClaudeOutput;
83
84 #[test]
85 fn test_deserialize_result_message() {
86 let json = r#"{
87 "type": "result",
88 "subtype": "success",
89 "is_error": false,
90 "duration_ms": 100,
91 "duration_api_ms": 200,
92 "num_turns": 1,
93 "result": "Done",
94 "session_id": "123",
95 "total_cost_usd": 0.01,
96 "permission_denials": []
97 }"#;
98
99 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
100 assert!(!output.is_error());
101 }
102
103 #[test]
104 fn test_deserialize_result_with_permission_denials() {
105 let json = r#"{
106 "type": "result",
107 "subtype": "success",
108 "is_error": false,
109 "duration_ms": 100,
110 "duration_api_ms": 200,
111 "num_turns": 2,
112 "result": "Done",
113 "session_id": "123",
114 "total_cost_usd": 0.01,
115 "permission_denials": [
116 {
117 "tool_name": "Bash",
118 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
119 "tool_use_id": "toolu_123"
120 }
121 ]
122 }"#;
123
124 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
125 if let ClaudeOutput::Result(result) = output {
126 assert_eq!(result.permission_denials.len(), 1);
127 assert_eq!(result.permission_denials[0].tool_name, "Bash");
128 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
129 assert_eq!(
130 result.permission_denials[0]
131 .tool_input
132 .get("command")
133 .unwrap(),
134 "rm -rf /"
135 );
136 } else {
137 panic!("Expected Result");
138 }
139 }
140
141 #[test]
142 fn test_permission_denial_roundtrip() {
143 let denial = PermissionDenial {
144 tool_name: "Write".to_string(),
145 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
146 tool_use_id: "toolu_456".to_string(),
147 };
148
149 let json = serde_json::to_string(&denial).unwrap();
150 assert!(json.contains("\"tool_name\":\"Write\""));
151 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
152 assert!(json.contains("/etc/passwd"));
153
154 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
155 assert_eq!(parsed, denial);
156 }
157
158 #[test]
159 fn test_deserialize_result_message_with_errors() {
160 let json = r#"{
161 "type": "result",
162 "subtype": "error_during_execution",
163 "duration_ms": 0,
164 "duration_api_ms": 0,
165 "is_error": true,
166 "num_turns": 0,
167 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
168 "total_cost_usd": 0,
169 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
170 "permission_denials": []
171 }"#;
172
173 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
174 assert!(output.is_error());
175
176 if let ClaudeOutput::Result(res) = output {
177 assert!(res.is_error);
178 assert_eq!(res.errors.len(), 1);
179 assert!(res.errors[0].contains("No conversation found"));
180 } else {
181 panic!("Expected Result message");
182 }
183 }
184
185 #[test]
186 fn test_deserialize_result_message_errors_defaults_empty() {
187 let json = r#"{
188 "type": "result",
189 "subtype": "success",
190 "is_error": false,
191 "duration_ms": 100,
192 "duration_api_ms": 200,
193 "num_turns": 1,
194 "session_id": "123",
195 "total_cost_usd": 0.01
196 }"#;
197
198 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
199 if let ClaudeOutput::Result(res) = output {
200 assert!(res.errors.is_empty());
201 } else {
202 panic!("Expected Result message");
203 }
204 }
205
206 #[test]
207 fn test_result_message_errors_roundtrip() {
208 let json = r#"{
209 "type": "result",
210 "subtype": "error_during_execution",
211 "is_error": true,
212 "duration_ms": 0,
213 "duration_api_ms": 0,
214 "num_turns": 0,
215 "session_id": "test-session",
216 "total_cost_usd": 0.0,
217 "errors": ["Error 1", "Error 2"]
218 }"#;
219
220 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
221 let reserialized = serde_json::to_string(&output).unwrap();
222
223 assert!(reserialized.contains("Error 1"));
224 assert!(reserialized.contains("Error 2"));
225 }
226}