1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ResultMessage {
8 pub subtype: ResultSubtype,
9 pub is_error: bool,
10 pub duration_ms: u64,
11 pub duration_api_ms: u64,
12
13 #[serde(skip_serializing_if = "Option::is_none")]
15 pub ttft_ms: Option<u64>,
16
17 #[serde(skip_serializing_if = "Option::is_none")]
19 pub ttft_stream_ms: Option<u64>,
20
21 #[serde(skip_serializing_if = "Option::is_none")]
23 pub time_to_request_ms: Option<u64>,
24
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub time_to_request_from_spawn_ms: Option<u64>,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub warm_spare_claimed: Option<bool>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub time_origin_ms: Option<u64>,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
40 pub request_sent_wall_ms: Option<f64>,
41
42 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub first_content_frame_ms: Option<u64>,
46
47 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub first_stream_post_ms: Option<u64>,
51
52 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub first_stream_post_ack_ms: Option<u64>,
56
57 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub first_stream_post_wall_ms: Option<f64>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub user_message_uuid: Option<String>,
65
66 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub user_message_uuids: Vec<String>,
74
75 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub queued_turn_count: Option<u64>,
82
83 pub num_turns: i32,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub result: Option<String>,
87
88 #[serde(alias = "sessionId")]
89 pub session_id: String,
90 pub total_cost_usd: f64,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub usage: Option<UsageInfo>,
94
95 #[serde(default)]
97 pub permission_denials: Vec<PermissionDenial>,
98
99 #[serde(default)]
104 pub errors: Vec<String>,
105
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub uuid: Option<String>,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
111 pub api_error_status: Option<u16>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub stop_reason: Option<String>,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub terminal_reason: Option<String>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub fast_mode_state: Option<String>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
129 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
130
131 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
133 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
139 pub subagent_stats: Option<SubagentStats>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub structured_output: Option<Value>,
144
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub deferred_tool_use: Option<DeferredToolUse>,
148
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub origin: Option<super::message_types::MessageOrigin>,
152}
153
154#[derive(Debug, Clone, Default, Serialize, Deserialize)]
160#[serde(rename_all = "camelCase")]
161pub struct ModelUsageEntry {
162 #[serde(default)]
163 pub input_tokens: u64,
164 #[serde(default)]
165 pub output_tokens: u64,
166 #[serde(default)]
167 pub cache_read_input_tokens: u64,
168 #[serde(default)]
169 pub cache_creation_input_tokens: u64,
170 #[serde(default, rename = "costUSD")]
171 pub cost_usd: f64,
172 #[serde(default)]
173 pub web_search_requests: u32,
174 #[serde(default)]
175 pub context_window: u64,
176 #[serde(default)]
177 pub max_output_tokens: u64,
178 #[serde(flatten)]
179 pub extra: serde_json::Map<String, serde_json::Value>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184pub struct DeferredToolUse {
185 pub id: String,
186 pub name: String,
187 pub input: Value,
188}
189
190#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
199pub struct SubagentStats {
200 pub spawned: u64,
202 pub requested: SubagentSpawnRequests,
205 pub started_in_background: u64,
208 #[serde(default)]
210 pub by_type: std::collections::BTreeMap<String, u64>,
211 pub max_depth: u64,
214 pub spawned_by_subagents: u64,
216 pub completed: u64,
217 pub failed: u64,
218 pub killed: SubagentKillCounts,
220 pub refused: SubagentRefusalCounts,
223}
224
225#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
228pub struct SubagentSpawnRequests {
229 pub background: u64,
230 pub foreground: u64,
231 pub unset: u64,
232}
233
234#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
238pub struct SubagentKillCounts {
239 pub parent: u64,
240 pub user: u64,
241 pub system: u64,
242}
243
244#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
246pub struct SubagentRefusalCounts {
247 pub depth_limit: u64,
248 pub concurrency_limit: u64,
249 pub budget: u64,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
257pub struct PermissionDenial {
258 pub tool_name: String,
260
261 pub tool_input: Value,
263
264 pub tool_use_id: String,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq, Hash)]
271pub enum FastModeDisabledReason {
272 Free,
274 Preference,
276 ExtraUsageDisabled,
278 NetworkError,
280 UnknownReason,
282 NotFirstParty,
284 DisabledByEnv,
286 ModelNotAllowed,
288 SdkOptInRequired,
290 Pending,
292 Unknown(String),
294}
295
296impl FastModeDisabledReason {
297 pub fn as_str(&self) -> &str {
298 match self {
299 Self::Free => "free",
300 Self::Preference => "preference",
301 Self::ExtraUsageDisabled => "extra_usage_disabled",
302 Self::NetworkError => "network_error",
303 Self::UnknownReason => "unknown",
304 Self::NotFirstParty => "not_first_party",
305 Self::DisabledByEnv => "disabled_by_env",
306 Self::ModelNotAllowed => "model_not_allowed",
307 Self::SdkOptInRequired => "sdk_opt_in_required",
308 Self::Pending => "pending",
309 Self::Unknown(s) => s.as_str(),
310 }
311 }
312}
313
314impl fmt::Display for FastModeDisabledReason {
315 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
316 f.write_str(self.as_str())
317 }
318}
319
320impl From<&str> for FastModeDisabledReason {
321 fn from(s: &str) -> Self {
322 match s {
323 "free" => Self::Free,
324 "preference" => Self::Preference,
325 "extra_usage_disabled" => Self::ExtraUsageDisabled,
326 "network_error" => Self::NetworkError,
327 "unknown" => Self::UnknownReason,
328 "not_first_party" => Self::NotFirstParty,
329 "disabled_by_env" => Self::DisabledByEnv,
330 "model_not_allowed" => Self::ModelNotAllowed,
331 "sdk_opt_in_required" => Self::SdkOptInRequired,
332 "pending" => Self::Pending,
333 other => Self::Unknown(other.to_string()),
334 }
335 }
336}
337
338impl Serialize for FastModeDisabledReason {
339 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
340 serializer.serialize_str(self.as_str())
341 }
342}
343
344impl<'de> Deserialize<'de> for FastModeDisabledReason {
345 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
346 let s = String::deserialize(deserializer)?;
347 Ok(Self::from(s.as_str()))
348 }
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Hash)]
353pub enum ResultSubtype {
354 Success,
355 ErrorMaxTurns,
356 ErrorDuringExecution,
357 ErrorMaxBudgetUsd,
358 ErrorMaxStructuredOutputRetries,
359 Unknown(String),
360}
361
362impl ResultSubtype {
363 pub fn as_str(&self) -> &str {
364 match self {
365 Self::Success => "success",
366 Self::ErrorMaxTurns => "error_max_turns",
367 Self::ErrorDuringExecution => "error_during_execution",
368 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
369 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
370 Self::Unknown(s) => s.as_str(),
371 }
372 }
373}
374
375impl fmt::Display for ResultSubtype {
376 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377 f.write_str(self.as_str())
378 }
379}
380
381impl From<&str> for ResultSubtype {
382 fn from(s: &str) -> Self {
383 match s {
384 "success" => Self::Success,
385 "error_max_turns" => Self::ErrorMaxTurns,
386 "error_during_execution" => Self::ErrorDuringExecution,
387 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
388 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
389 other => Self::Unknown(other.to_string()),
390 }
391 }
392}
393
394impl Serialize for ResultSubtype {
395 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
396 serializer.serialize_str(self.as_str())
397 }
398}
399
400impl<'de> Deserialize<'de> for ResultSubtype {
401 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
402 let s = String::deserialize(deserializer)?;
403 Ok(Self::from(s.as_str()))
404 }
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
425pub struct UsageInfo {
426 #[serde(default)]
428 pub input_tokens: u32,
429 #[serde(default)]
431 pub cache_creation_input_tokens: u32,
432 #[serde(default)]
436 pub cache_read_input_tokens: u32,
437 #[serde(default)]
439 pub output_tokens: u32,
440 #[serde(default)]
441 pub server_tool_use: ServerToolUse,
442 #[serde(default)]
443 pub service_tier: String,
444
445 #[serde(skip_serializing_if = "Option::is_none")]
447 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
448
449 #[serde(skip_serializing_if = "Option::is_none")]
451 pub inference_geo: Option<String>,
452
453 #[serde(default, skip_serializing_if = "Vec::is_empty")]
457 pub iterations: Vec<UsageIteration>,
458
459 #[serde(skip_serializing_if = "Option::is_none")]
461 pub speed: Option<String>,
462
463 #[serde(skip_serializing_if = "Option::is_none")]
466 pub output_tokens_details: Option<OutputTokensDetails>,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize, Default)]
472pub struct OutputTokensDetails {
473 #[serde(skip_serializing_if = "Option::is_none")]
475 pub thinking_tokens: Option<u64>,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
487pub struct UsageIteration {
488 #[serde(default)]
489 pub input_tokens: u32,
490 #[serde(default)]
491 pub output_tokens: u32,
492 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub cache_read_input_tokens: Option<u32>,
495 #[serde(default, skip_serializing_if = "Option::is_none")]
497 pub cache_creation_input_tokens: Option<u32>,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
501 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
503 pub kind: Option<String>,
504}
505
506#[derive(Debug, Clone, Default, Serialize, Deserialize)]
508pub struct ServerToolUse {
509 #[serde(default)]
510 pub web_search_requests: u32,
511 #[serde(default)]
513 pub web_fetch_requests: u32,
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519 use crate::io::ClaudeOutput;
520
521 #[test]
522 fn test_deserialize_result_message() {
523 let json = r#"{
524 "type": "result",
525 "subtype": "success",
526 "is_error": false,
527 "duration_ms": 100,
528 "duration_api_ms": 200,
529 "num_turns": 1,
530 "result": "Done",
531 "session_id": "123",
532 "total_cost_usd": 0.01,
533 "permission_denials": []
534 }"#;
535
536 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
537 assert!(!output.is_error());
538 }
539
540 #[test]
541 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
542 let json = r#"{
543 "type": "result",
544 "subtype": "error_max_budget_usd",
545 "is_error": true,
546 "duration_ms": 100,
547 "duration_api_ms": 200,
548 "num_turns": 1,
549 "session_id": "123",
550 "total_cost_usd": 0.01
551 }"#;
552
553 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
554 let ClaudeOutput::Result(result) = output else {
555 panic!("Expected Result");
556 };
557 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
558
559 let json = r#"{
560 "type": "result",
561 "subtype": "future_result_subtype",
562 "is_error": true,
563 "duration_ms": 100,
564 "duration_api_ms": 200,
565 "num_turns": 1,
566 "session_id": "123",
567 "total_cost_usd": 0.01
568 }"#;
569
570 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
571 let ClaudeOutput::Result(result) = output else {
572 panic!("Expected Result");
573 };
574 assert_eq!(
575 result.subtype,
576 ResultSubtype::Unknown("future_result_subtype".to_string())
577 );
578 }
579
580 #[test]
581 fn test_deserialize_result_with_permission_denials() {
582 let json = r#"{
583 "type": "result",
584 "subtype": "success",
585 "is_error": false,
586 "duration_ms": 100,
587 "duration_api_ms": 200,
588 "num_turns": 2,
589 "result": "Done",
590 "session_id": "123",
591 "total_cost_usd": 0.01,
592 "permission_denials": [
593 {
594 "tool_name": "Bash",
595 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
596 "tool_use_id": "toolu_123"
597 }
598 ]
599 }"#;
600
601 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
602 if let ClaudeOutput::Result(result) = output {
603 assert_eq!(result.permission_denials.len(), 1);
604 assert_eq!(result.permission_denials[0].tool_name, "Bash");
605 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
606 assert_eq!(
607 result.permission_denials[0]
608 .tool_input
609 .get("command")
610 .unwrap(),
611 "rm -rf /"
612 );
613 } else {
614 panic!("Expected Result");
615 }
616 }
617
618 #[test]
619 fn test_permission_denial_roundtrip() {
620 let denial = PermissionDenial {
621 tool_name: "Write".to_string(),
622 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
623 tool_use_id: "toolu_456".to_string(),
624 };
625
626 let json = serde_json::to_string(&denial).unwrap();
627 assert!(json.contains("\"tool_name\":\"Write\""));
628 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
629 assert!(json.contains("/etc/passwd"));
630
631 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
632 assert_eq!(parsed, denial);
633 }
634
635 #[test]
636 fn test_deserialize_result_message_with_errors() {
637 let json = r#"{
638 "type": "result",
639 "subtype": "error_during_execution",
640 "duration_ms": 0,
641 "duration_api_ms": 0,
642 "is_error": true,
643 "num_turns": 0,
644 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
645 "total_cost_usd": 0,
646 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
647 "permission_denials": []
648 }"#;
649
650 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
651 assert!(output.is_error());
652
653 if let ClaudeOutput::Result(res) = output {
654 assert!(res.is_error);
655 assert_eq!(res.errors.len(), 1);
656 assert!(res.errors[0].contains("No conversation found"));
657 } else {
658 panic!("Expected Result message");
659 }
660 }
661
662 #[test]
663 fn test_deserialize_result_message_errors_defaults_empty() {
664 let json = r#"{
665 "type": "result",
666 "subtype": "success",
667 "is_error": false,
668 "duration_ms": 100,
669 "duration_api_ms": 200,
670 "num_turns": 1,
671 "session_id": "123",
672 "total_cost_usd": 0.01
673 }"#;
674
675 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
676 if let ClaudeOutput::Result(res) = output {
677 assert!(res.errors.is_empty());
678 } else {
679 panic!("Expected Result message");
680 }
681 }
682
683 #[test]
684 fn test_result_message_errors_roundtrip() {
685 let json = r#"{
686 "type": "result",
687 "subtype": "error_during_execution",
688 "is_error": true,
689 "duration_ms": 0,
690 "duration_api_ms": 0,
691 "num_turns": 0,
692 "session_id": "test-session",
693 "total_cost_usd": 0.0,
694 "errors": ["Error 1", "Error 2"]
695 }"#;
696
697 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
698 let reserialized = serde_json::to_string(&output).unwrap();
699
700 assert!(reserialized.contains("Error 1"));
701 assert!(reserialized.contains("Error 2"));
702 }
703
704 #[test]
705 fn test_result_with_new_fields() {
706 let json = r#"{
707 "type": "result",
708 "subtype": "success",
709 "is_error": false,
710 "duration_ms": 5000,
711 "duration_api_ms": 4500,
712 "num_turns": 1,
713 "result": "Done",
714 "session_id": "abc",
715 "total_cost_usd": 0.06,
716 "api_error_status": null,
717 "stop_reason": "end_turn",
718 "terminal_reason": "completed",
719 "fast_mode_state": "off",
720 "modelUsage": {
721 "claude-opus-4-7[1m]": {
722 "inputTokens": 3817,
723 "outputTokens": 14,
724 "costUSD": 0.06
725 }
726 },
727 "usage": {
728 "input_tokens": 3817,
729 "output_tokens": 14,
730 "cache_creation_input_tokens": 3540,
731 "cache_read_input_tokens": 0,
732 "server_tool_use": {
733 "web_search_requests": 0,
734 "web_fetch_requests": 2
735 },
736 "service_tier": "standard",
737 "inference_geo": "not_available",
738 "speed": "standard",
739 "iterations": [
740 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
741 ]
742 }
743 }"#;
744
745 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
746 if let ClaudeOutput::Result(res) = output {
747 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
748 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
749 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
750 let model_usage = res.model_usage.as_ref().unwrap();
751 let entry = model_usage
752 .get("claude-opus-4-7[1m]")
753 .expect("per-model entry present");
754 assert_eq!(entry.input_tokens, 3817);
755 assert_eq!(entry.output_tokens, 14);
756 assert_eq!(entry.cost_usd, 0.06);
757 assert!(res.api_error_status.is_none());
758
759 let usage = res.usage.unwrap();
760 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
761 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
762 assert_eq!(usage.speed.as_deref(), Some("standard"));
763 assert_eq!(usage.iterations.len(), 1);
764 assert_eq!(usage.iterations[0].input_tokens, 3817);
765 assert_eq!(usage.iterations[0].output_tokens, 14);
766 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
767 } else {
768 panic!("Expected Result");
769 }
770 }
771
772 #[test]
773 fn test_result_backwards_compatible_without_new_fields() {
774 let json = r#"{
776 "type": "result",
777 "subtype": "success",
778 "is_error": false,
779 "duration_ms": 100,
780 "duration_api_ms": 200,
781 "num_turns": 1,
782 "session_id": "abc",
783 "total_cost_usd": 0.01
784 }"#;
785
786 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
787 if let ClaudeOutput::Result(res) = output {
788 assert!(res.api_error_status.is_none());
789 assert!(res.stop_reason.is_none());
790 assert!(res.terminal_reason.is_none());
791 assert!(res.fast_mode_state.is_none());
792 assert!(res.model_usage.is_none());
793 } else {
794 panic!("Expected Result");
795 }
796 }
797
798 #[test]
799 fn test_result_fast_mode_disabled_reason() {
800 let json = r#"{
801 "type":"result","subtype":"success","is_error":false,
802 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
803 "session_id":"s1","total_cost_usd":0.01,
804 "fast_mode_state":"off",
805 "fast_mode_disabled_reason":"sdk_opt_in_required"
806 }"#;
807 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
808 let crate::ClaudeOutput::Result(res) = &output else {
809 panic!("expected Result");
810 };
811 assert_eq!(
812 res.fast_mode_disabled_reason,
813 Some(FastModeDisabledReason::SdkOptInRequired)
814 );
815 assert!(serde_json::to_string(&output)
816 .unwrap()
817 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
818
819 assert_eq!(
822 FastModeDisabledReason::from("unknown"),
823 FastModeDisabledReason::UnknownReason
824 );
825 let novel = FastModeDisabledReason::from("solar_flare");
826 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
827 assert_eq!(novel.as_str(), "solar_flare");
828 }
829
830 #[test]
831 fn test_result_timing_and_user_message_uuid_fields() {
832 let json = r#"{
833 "type":"result","subtype":"success","is_error":false,
834 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
835 "session_id":"s1","total_cost_usd":0.01,
836 "request_sent_wall_ms":1753212345678.25,
837 "user_message_uuid":"um-1"
838 }"#;
839 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
840 let crate::ClaudeOutput::Result(res) = &output else {
841 panic!("expected Result");
842 };
843 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
844 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
845
846 let reserialized = serde_json::to_string(&output).unwrap();
847 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
848 }
849
850 #[test]
851 fn test_result_queued_turn_count_and_user_message_uuids() {
852 let json = r#"{
854 "type":"result","subtype":"success","is_error":false,
855 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
856 "session_id":"s1","total_cost_usd":0.01,
857 "user_message_uuid":"um-2",
858 "user_message_uuids":["um-1","um-2"],
859 "queued_turn_count":3
860 }"#;
861 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
862 let crate::ClaudeOutput::Result(res) = &output else {
863 panic!("expected Result");
864 };
865 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
866 assert_eq!(res.queued_turn_count, Some(3));
867
868 let reserialized = serde_json::to_string(&output).unwrap();
869 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
870 assert!(reserialized.contains("\"queued_turn_count\":3"));
871
872 let old = r#"{
874 "type":"result","subtype":"success","is_error":false,
875 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
876 "session_id":"s1","total_cost_usd":0.01
877 }"#;
878 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
879 let crate::ClaudeOutput::Result(res) = &output else {
880 panic!("expected Result");
881 };
882 assert!(res.user_message_uuids.is_empty());
883 assert_eq!(res.queued_turn_count, None);
884 let reserialized = serde_json::to_string(&output).unwrap();
885 assert!(!reserialized.contains("user_message_uuids"));
886 assert!(!reserialized.contains("queued_turn_count"));
887 }
888}