1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct ClaudeUsage {
16 #[serde(default)]
18 pub input_tokens: u64,
19 #[serde(default)]
21 pub output_tokens: u64,
22 #[serde(default)]
24 pub cache_read_tokens: u64,
25 #[serde(default)]
27 pub cache_creation_tokens: u64,
28 #[serde(default)]
30 pub thinking_tokens: u64,
31 #[serde(default)]
33 pub model: Option<String>,
34}
35
36impl ClaudeUsage {
37 pub fn total_tokens(&self) -> u64 {
39 self.input_tokens + self.output_tokens
40 }
41
42 pub fn add(&mut self, other: &ClaudeUsage) {
47 self.input_tokens += other.input_tokens;
48 self.output_tokens += other.output_tokens;
49 self.cache_read_tokens += other.cache_read_tokens;
50 self.cache_creation_tokens += other.cache_creation_tokens;
51 self.thinking_tokens += other.thinking_tokens;
52 if self.model.is_none() {
54 self.model = other.model.clone();
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub struct ClaudeErrorInfo {
62 pub message: String,
64 pub exit_code: Option<i32>,
66 pub stderr: Option<String>,
68}
69
70impl ClaudeErrorInfo {
71 pub fn new(message: impl Into<String>) -> Self {
73 Self {
74 message: message.into(),
75 exit_code: None,
76 stderr: None,
77 }
78 }
79
80 pub fn from_process_failure(status: std::process::ExitStatus, stderr: Option<String>) -> Self {
82 let exit_code = status.code();
83 let stderr_trimmed = stderr.as_ref().map(|s| s.trim().to_string());
84
85 let message = match (&stderr_trimmed, exit_code) {
86 (Some(err), Some(code)) if !err.is_empty() => {
87 format!("Claude exited with status {}: {}", code, err)
88 }
89 (Some(err), None) if !err.is_empty() => {
90 format!("Claude exited with error: {}", err)
91 }
92 (_, Some(code)) => {
93 format!("Claude exited with status: {}", code)
94 }
95 (_, None) => {
96 format!("Claude exited with status: {}", status)
97 }
98 };
99
100 Self {
101 message,
102 exit_code,
103 stderr: stderr_trimmed.filter(|s| !s.is_empty()),
104 }
105 }
106}
107
108impl std::fmt::Display for ClaudeErrorInfo {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 write!(f, "{}", self.message)
111 }
112}
113
114impl From<ClaudeErrorInfo> for String {
115 fn from(info: ClaudeErrorInfo) -> Self {
116 info.message
117 }
118}
119
120#[derive(Debug, Clone, PartialEq)]
122pub struct ClaudeStoryResult {
123 pub outcome: ClaudeOutcome,
124 pub work_summary: Option<String>,
126 pub full_output: String,
128 pub usage: Option<ClaudeUsage>,
130}
131
132#[derive(Debug, Clone, PartialEq)]
133pub enum ClaudeOutcome {
134 IterationComplete,
135 AllStoriesComplete,
136 Error(ClaudeErrorInfo),
137}
138
139#[derive(Debug, Clone, PartialEq)]
141pub enum ClaudeResult {
142 IterationComplete,
143 AllStoriesComplete,
144 Error(ClaudeErrorInfo),
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
154 fn test_claude_usage_default() {
155 let usage = ClaudeUsage::default();
156 assert_eq!(usage.input_tokens, 0);
157 assert_eq!(usage.output_tokens, 0);
158 assert_eq!(usage.cache_read_tokens, 0);
159 assert_eq!(usage.cache_creation_tokens, 0);
160 assert_eq!(usage.thinking_tokens, 0);
161 assert_eq!(usage.model, None);
162 }
163
164 #[test]
165 fn test_claude_usage_total_tokens() {
166 let usage = ClaudeUsage {
167 input_tokens: 100,
168 output_tokens: 50,
169 ..Default::default()
170 };
171 assert_eq!(usage.total_tokens(), 150);
172 }
173
174 #[test]
175 fn test_claude_usage_total_tokens_zero() {
176 let usage = ClaudeUsage::default();
177 assert_eq!(usage.total_tokens(), 0);
178 }
179
180 #[test]
181 fn test_claude_usage_add_basic() {
182 let mut usage1 = ClaudeUsage {
183 input_tokens: 100,
184 output_tokens: 50,
185 cache_read_tokens: 25,
186 cache_creation_tokens: 10,
187 thinking_tokens: 5,
188 model: None,
189 };
190 let usage2 = ClaudeUsage {
191 input_tokens: 200,
192 output_tokens: 100,
193 cache_read_tokens: 50,
194 cache_creation_tokens: 20,
195 thinking_tokens: 10,
196 model: Some("claude-sonnet-4-20250514".to_string()),
197 };
198
199 usage1.add(&usage2);
200
201 assert_eq!(usage1.input_tokens, 300);
202 assert_eq!(usage1.output_tokens, 150);
203 assert_eq!(usage1.cache_read_tokens, 75);
204 assert_eq!(usage1.cache_creation_tokens, 30);
205 assert_eq!(usage1.thinking_tokens, 15);
206 assert_eq!(usage1.model, Some("claude-sonnet-4-20250514".to_string()));
207 }
208
209 #[test]
210 fn test_claude_usage_add_preserves_existing_model() {
211 let mut usage1 = ClaudeUsage {
212 model: Some("existing-model".to_string()),
213 ..Default::default()
214 };
215 let usage2 = ClaudeUsage {
216 model: Some("other-model".to_string()),
217 ..Default::default()
218 };
219
220 usage1.add(&usage2);
221
222 assert_eq!(usage1.model, Some("existing-model".to_string()));
224 }
225
226 #[test]
227 fn test_claude_usage_add_takes_model_when_none() {
228 let mut usage1 = ClaudeUsage::default();
229 let usage2 = ClaudeUsage {
230 model: Some("new-model".to_string()),
231 ..Default::default()
232 };
233
234 usage1.add(&usage2);
235
236 assert_eq!(usage1.model, Some("new-model".to_string()));
237 }
238
239 #[test]
240 fn test_claude_usage_clone() {
241 let usage = ClaudeUsage {
242 input_tokens: 100,
243 output_tokens: 50,
244 cache_read_tokens: 25,
245 cache_creation_tokens: 10,
246 thinking_tokens: 5,
247 model: Some("test-model".to_string()),
248 };
249 let cloned = usage.clone();
250 assert_eq!(usage.input_tokens, cloned.input_tokens);
251 assert_eq!(usage.output_tokens, cloned.output_tokens);
252 assert_eq!(usage.cache_read_tokens, cloned.cache_read_tokens);
253 assert_eq!(usage.cache_creation_tokens, cloned.cache_creation_tokens);
254 assert_eq!(usage.thinking_tokens, cloned.thinking_tokens);
255 assert_eq!(usage.model, cloned.model);
256 }
257
258 #[test]
259 fn test_claude_usage_serialize_deserialize() {
260 let usage = ClaudeUsage {
261 input_tokens: 100,
262 output_tokens: 50,
263 cache_read_tokens: 25,
264 cache_creation_tokens: 10,
265 thinking_tokens: 5,
266 model: Some("test-model".to_string()),
267 };
268
269 let json = serde_json::to_string(&usage).unwrap();
270 let deserialized: ClaudeUsage = serde_json::from_str(&json).unwrap();
271
272 assert_eq!(usage.input_tokens, deserialized.input_tokens);
273 assert_eq!(usage.output_tokens, deserialized.output_tokens);
274 assert_eq!(usage.cache_read_tokens, deserialized.cache_read_tokens);
275 assert_eq!(
276 usage.cache_creation_tokens,
277 deserialized.cache_creation_tokens
278 );
279 assert_eq!(usage.thinking_tokens, deserialized.thinking_tokens);
280 assert_eq!(usage.model, deserialized.model);
281 }
282
283 #[test]
284 fn test_claude_usage_deserialize_partial() {
285 let json = r#"{"inputTokens": 100, "outputTokens": 50}"#;
287 let usage: ClaudeUsage = serde_json::from_str(json).unwrap();
288
289 assert_eq!(usage.input_tokens, 100);
290 assert_eq!(usage.output_tokens, 50);
291 assert_eq!(usage.cache_read_tokens, 0);
292 assert_eq!(usage.cache_creation_tokens, 0);
293 assert_eq!(usage.thinking_tokens, 0);
294 assert_eq!(usage.model, None);
295 }
296
297 #[test]
298 fn test_claude_usage_deserialize_empty() {
299 let json = r#"{}"#;
301 let usage: ClaudeUsage = serde_json::from_str(json).unwrap();
302
303 assert_eq!(usage.input_tokens, 0);
304 assert_eq!(usage.output_tokens, 0);
305 assert_eq!(usage.cache_read_tokens, 0);
306 assert_eq!(usage.cache_creation_tokens, 0);
307 assert_eq!(usage.thinking_tokens, 0);
308 assert_eq!(usage.model, None);
309 }
310
311 #[test]
314 fn test_claude_error_info_new() {
315 let info = ClaudeErrorInfo::new("test error message");
316 assert_eq!(info.message, "test error message");
317 assert_eq!(info.exit_code, None);
318 assert_eq!(info.stderr, None);
319 }
320
321 #[test]
322 fn test_claude_error_info_display() {
323 let info = ClaudeErrorInfo::new("test error");
324 assert_eq!(format!("{}", info), "test error");
325 }
326
327 #[test]
328 fn test_claude_error_info_into_string() {
329 let info = ClaudeErrorInfo::new("convertible error");
330 let s: String = info.into();
331 assert_eq!(s, "convertible error");
332 }
333
334 #[test]
335 fn test_claude_error_info_clone() {
336 let info = ClaudeErrorInfo {
337 message: "cloned error".to_string(),
338 exit_code: Some(42),
339 stderr: Some("stderr content".to_string()),
340 };
341 let cloned = info.clone();
342 assert_eq!(info, cloned);
343 }
344
345 #[test]
346 fn test_claude_error_info_equality() {
347 let info1 = ClaudeErrorInfo::new("error");
348 let info2 = ClaudeErrorInfo::new("error");
349 let info3 = ClaudeErrorInfo::new("different");
350 assert_eq!(info1, info2);
351 assert_ne!(info1, info3);
352 }
353}