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(skip_serializing_if = "Option::is_none")]
44 pub user_message_uuid: Option<String>,
45
46 #[serde(default, skip_serializing_if = "Vec::is_empty")]
53 pub user_message_uuids: Vec<String>,
54
55 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub queued_turn_count: Option<u64>,
62
63 pub num_turns: i32,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub result: Option<String>,
67
68 #[serde(alias = "sessionId")]
69 pub session_id: String,
70 pub total_cost_usd: f64,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub usage: Option<UsageInfo>,
74
75 #[serde(default)]
77 pub permission_denials: Vec<PermissionDenial>,
78
79 #[serde(default)]
84 pub errors: Vec<String>,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub uuid: Option<String>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub api_error_status: Option<u16>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub stop_reason: Option<String>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub terminal_reason: Option<String>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub fast_mode_state: Option<String>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
109 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
110
111 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
113 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
119 pub subagent_stats: Option<SubagentStats>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub structured_output: Option<Value>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub deferred_tool_use: Option<DeferredToolUse>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub origin: Option<super::message_types::MessageOrigin>,
132}
133
134#[derive(Debug, Clone, Default, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct ModelUsageEntry {
142 #[serde(default)]
143 pub input_tokens: u64,
144 #[serde(default)]
145 pub output_tokens: u64,
146 #[serde(default)]
147 pub cache_read_input_tokens: u64,
148 #[serde(default)]
149 pub cache_creation_input_tokens: u64,
150 #[serde(default, rename = "costUSD")]
151 pub cost_usd: f64,
152 #[serde(default)]
153 pub web_search_requests: u32,
154 #[serde(default)]
155 pub context_window: u64,
156 #[serde(default)]
157 pub max_output_tokens: u64,
158 #[serde(flatten)]
159 pub extra: serde_json::Map<String, serde_json::Value>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
164pub struct DeferredToolUse {
165 pub id: String,
166 pub name: String,
167 pub input: Value,
168}
169
170#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
179pub struct SubagentStats {
180 pub spawned: u64,
182 pub requested: SubagentSpawnRequests,
185 pub started_in_background: u64,
188 #[serde(default)]
190 pub by_type: std::collections::BTreeMap<String, u64>,
191 pub max_depth: u64,
194 pub spawned_by_subagents: u64,
196 pub completed: u64,
197 pub failed: u64,
198 pub killed: SubagentKillCounts,
200 pub refused: SubagentRefusalCounts,
203}
204
205#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
208pub struct SubagentSpawnRequests {
209 pub background: u64,
210 pub foreground: u64,
211 pub unset: u64,
212}
213
214#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
218pub struct SubagentKillCounts {
219 pub parent: u64,
220 pub user: u64,
221 pub system: u64,
222}
223
224#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
226pub struct SubagentRefusalCounts {
227 pub depth_limit: u64,
228 pub concurrency_limit: u64,
229 pub budget: u64,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
237pub struct PermissionDenial {
238 pub tool_name: String,
240
241 pub tool_input: Value,
243
244 pub tool_use_id: String,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Hash)]
251pub enum FastModeDisabledReason {
252 Free,
254 Preference,
256 ExtraUsageDisabled,
258 NetworkError,
260 UnknownReason,
262 NotFirstParty,
264 DisabledByEnv,
266 ModelNotAllowed,
268 SdkOptInRequired,
270 Pending,
272 Unknown(String),
274}
275
276impl FastModeDisabledReason {
277 pub fn as_str(&self) -> &str {
278 match self {
279 Self::Free => "free",
280 Self::Preference => "preference",
281 Self::ExtraUsageDisabled => "extra_usage_disabled",
282 Self::NetworkError => "network_error",
283 Self::UnknownReason => "unknown",
284 Self::NotFirstParty => "not_first_party",
285 Self::DisabledByEnv => "disabled_by_env",
286 Self::ModelNotAllowed => "model_not_allowed",
287 Self::SdkOptInRequired => "sdk_opt_in_required",
288 Self::Pending => "pending",
289 Self::Unknown(s) => s.as_str(),
290 }
291 }
292}
293
294impl fmt::Display for FastModeDisabledReason {
295 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296 f.write_str(self.as_str())
297 }
298}
299
300impl From<&str> for FastModeDisabledReason {
301 fn from(s: &str) -> Self {
302 match s {
303 "free" => Self::Free,
304 "preference" => Self::Preference,
305 "extra_usage_disabled" => Self::ExtraUsageDisabled,
306 "network_error" => Self::NetworkError,
307 "unknown" => Self::UnknownReason,
308 "not_first_party" => Self::NotFirstParty,
309 "disabled_by_env" => Self::DisabledByEnv,
310 "model_not_allowed" => Self::ModelNotAllowed,
311 "sdk_opt_in_required" => Self::SdkOptInRequired,
312 "pending" => Self::Pending,
313 other => Self::Unknown(other.to_string()),
314 }
315 }
316}
317
318impl Serialize for FastModeDisabledReason {
319 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
320 serializer.serialize_str(self.as_str())
321 }
322}
323
324impl<'de> Deserialize<'de> for FastModeDisabledReason {
325 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
326 let s = String::deserialize(deserializer)?;
327 Ok(Self::from(s.as_str()))
328 }
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Hash)]
333pub enum ResultSubtype {
334 Success,
335 ErrorMaxTurns,
336 ErrorDuringExecution,
337 ErrorMaxBudgetUsd,
338 ErrorMaxStructuredOutputRetries,
339 Unknown(String),
340}
341
342impl ResultSubtype {
343 pub fn as_str(&self) -> &str {
344 match self {
345 Self::Success => "success",
346 Self::ErrorMaxTurns => "error_max_turns",
347 Self::ErrorDuringExecution => "error_during_execution",
348 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
349 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
350 Self::Unknown(s) => s.as_str(),
351 }
352 }
353}
354
355impl fmt::Display for ResultSubtype {
356 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
357 f.write_str(self.as_str())
358 }
359}
360
361impl From<&str> for ResultSubtype {
362 fn from(s: &str) -> Self {
363 match s {
364 "success" => Self::Success,
365 "error_max_turns" => Self::ErrorMaxTurns,
366 "error_during_execution" => Self::ErrorDuringExecution,
367 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
368 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
369 other => Self::Unknown(other.to_string()),
370 }
371 }
372}
373
374impl Serialize for ResultSubtype {
375 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
376 serializer.serialize_str(self.as_str())
377 }
378}
379
380impl<'de> Deserialize<'de> for ResultSubtype {
381 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
382 let s = String::deserialize(deserializer)?;
383 Ok(Self::from(s.as_str()))
384 }
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct UsageInfo {
406 #[serde(default)]
408 pub input_tokens: u32,
409 #[serde(default)]
411 pub cache_creation_input_tokens: u32,
412 #[serde(default)]
416 pub cache_read_input_tokens: u32,
417 #[serde(default)]
419 pub output_tokens: u32,
420 #[serde(default)]
421 pub server_tool_use: ServerToolUse,
422 #[serde(default)]
423 pub service_tier: String,
424
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
428
429 #[serde(skip_serializing_if = "Option::is_none")]
431 pub inference_geo: Option<String>,
432
433 #[serde(default, skip_serializing_if = "Vec::is_empty")]
437 pub iterations: Vec<UsageIteration>,
438
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub speed: Option<String>,
442
443 #[serde(skip_serializing_if = "Option::is_none")]
446 pub output_tokens_details: Option<OutputTokensDetails>,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize, Default)]
452pub struct OutputTokensDetails {
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub thinking_tokens: Option<u64>,
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
467pub struct UsageIteration {
468 #[serde(default)]
469 pub input_tokens: u32,
470 #[serde(default)]
471 pub output_tokens: u32,
472 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub cache_read_input_tokens: Option<u32>,
475 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub cache_creation_input_tokens: Option<u32>,
478 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
481 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
483 pub kind: Option<String>,
484}
485
486#[derive(Debug, Clone, Default, Serialize, Deserialize)]
488pub struct ServerToolUse {
489 #[serde(default)]
490 pub web_search_requests: u32,
491 #[serde(default)]
493 pub web_fetch_requests: u32,
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499 use crate::io::ClaudeOutput;
500
501 #[test]
502 fn test_deserialize_result_message() {
503 let json = r#"{
504 "type": "result",
505 "subtype": "success",
506 "is_error": false,
507 "duration_ms": 100,
508 "duration_api_ms": 200,
509 "num_turns": 1,
510 "result": "Done",
511 "session_id": "123",
512 "total_cost_usd": 0.01,
513 "permission_denials": []
514 }"#;
515
516 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
517 assert!(!output.is_error());
518 }
519
520 #[test]
521 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
522 let json = r#"{
523 "type": "result",
524 "subtype": "error_max_budget_usd",
525 "is_error": true,
526 "duration_ms": 100,
527 "duration_api_ms": 200,
528 "num_turns": 1,
529 "session_id": "123",
530 "total_cost_usd": 0.01
531 }"#;
532
533 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
534 let ClaudeOutput::Result(result) = output else {
535 panic!("Expected Result");
536 };
537 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
538
539 let json = r#"{
540 "type": "result",
541 "subtype": "future_result_subtype",
542 "is_error": true,
543 "duration_ms": 100,
544 "duration_api_ms": 200,
545 "num_turns": 1,
546 "session_id": "123",
547 "total_cost_usd": 0.01
548 }"#;
549
550 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
551 let ClaudeOutput::Result(result) = output else {
552 panic!("Expected Result");
553 };
554 assert_eq!(
555 result.subtype,
556 ResultSubtype::Unknown("future_result_subtype".to_string())
557 );
558 }
559
560 #[test]
561 fn test_deserialize_result_with_permission_denials() {
562 let json = r#"{
563 "type": "result",
564 "subtype": "success",
565 "is_error": false,
566 "duration_ms": 100,
567 "duration_api_ms": 200,
568 "num_turns": 2,
569 "result": "Done",
570 "session_id": "123",
571 "total_cost_usd": 0.01,
572 "permission_denials": [
573 {
574 "tool_name": "Bash",
575 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
576 "tool_use_id": "toolu_123"
577 }
578 ]
579 }"#;
580
581 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
582 if let ClaudeOutput::Result(result) = output {
583 assert_eq!(result.permission_denials.len(), 1);
584 assert_eq!(result.permission_denials[0].tool_name, "Bash");
585 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
586 assert_eq!(
587 result.permission_denials[0]
588 .tool_input
589 .get("command")
590 .unwrap(),
591 "rm -rf /"
592 );
593 } else {
594 panic!("Expected Result");
595 }
596 }
597
598 #[test]
599 fn test_permission_denial_roundtrip() {
600 let denial = PermissionDenial {
601 tool_name: "Write".to_string(),
602 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
603 tool_use_id: "toolu_456".to_string(),
604 };
605
606 let json = serde_json::to_string(&denial).unwrap();
607 assert!(json.contains("\"tool_name\":\"Write\""));
608 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
609 assert!(json.contains("/etc/passwd"));
610
611 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
612 assert_eq!(parsed, denial);
613 }
614
615 #[test]
616 fn test_deserialize_result_message_with_errors() {
617 let json = r#"{
618 "type": "result",
619 "subtype": "error_during_execution",
620 "duration_ms": 0,
621 "duration_api_ms": 0,
622 "is_error": true,
623 "num_turns": 0,
624 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
625 "total_cost_usd": 0,
626 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
627 "permission_denials": []
628 }"#;
629
630 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
631 assert!(output.is_error());
632
633 if let ClaudeOutput::Result(res) = output {
634 assert!(res.is_error);
635 assert_eq!(res.errors.len(), 1);
636 assert!(res.errors[0].contains("No conversation found"));
637 } else {
638 panic!("Expected Result message");
639 }
640 }
641
642 #[test]
643 fn test_deserialize_result_message_errors_defaults_empty() {
644 let json = r#"{
645 "type": "result",
646 "subtype": "success",
647 "is_error": false,
648 "duration_ms": 100,
649 "duration_api_ms": 200,
650 "num_turns": 1,
651 "session_id": "123",
652 "total_cost_usd": 0.01
653 }"#;
654
655 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
656 if let ClaudeOutput::Result(res) = output {
657 assert!(res.errors.is_empty());
658 } else {
659 panic!("Expected Result message");
660 }
661 }
662
663 #[test]
664 fn test_result_message_errors_roundtrip() {
665 let json = r#"{
666 "type": "result",
667 "subtype": "error_during_execution",
668 "is_error": true,
669 "duration_ms": 0,
670 "duration_api_ms": 0,
671 "num_turns": 0,
672 "session_id": "test-session",
673 "total_cost_usd": 0.0,
674 "errors": ["Error 1", "Error 2"]
675 }"#;
676
677 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
678 let reserialized = serde_json::to_string(&output).unwrap();
679
680 assert!(reserialized.contains("Error 1"));
681 assert!(reserialized.contains("Error 2"));
682 }
683
684 #[test]
685 fn test_result_with_new_fields() {
686 let json = r#"{
687 "type": "result",
688 "subtype": "success",
689 "is_error": false,
690 "duration_ms": 5000,
691 "duration_api_ms": 4500,
692 "num_turns": 1,
693 "result": "Done",
694 "session_id": "abc",
695 "total_cost_usd": 0.06,
696 "api_error_status": null,
697 "stop_reason": "end_turn",
698 "terminal_reason": "completed",
699 "fast_mode_state": "off",
700 "modelUsage": {
701 "claude-opus-4-7[1m]": {
702 "inputTokens": 3817,
703 "outputTokens": 14,
704 "costUSD": 0.06
705 }
706 },
707 "usage": {
708 "input_tokens": 3817,
709 "output_tokens": 14,
710 "cache_creation_input_tokens": 3540,
711 "cache_read_input_tokens": 0,
712 "server_tool_use": {
713 "web_search_requests": 0,
714 "web_fetch_requests": 2
715 },
716 "service_tier": "standard",
717 "inference_geo": "not_available",
718 "speed": "standard",
719 "iterations": [
720 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
721 ]
722 }
723 }"#;
724
725 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
726 if let ClaudeOutput::Result(res) = output {
727 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
728 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
729 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
730 let model_usage = res.model_usage.as_ref().unwrap();
731 let entry = model_usage
732 .get("claude-opus-4-7[1m]")
733 .expect("per-model entry present");
734 assert_eq!(entry.input_tokens, 3817);
735 assert_eq!(entry.output_tokens, 14);
736 assert_eq!(entry.cost_usd, 0.06);
737 assert!(res.api_error_status.is_none());
738
739 let usage = res.usage.unwrap();
740 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
741 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
742 assert_eq!(usage.speed.as_deref(), Some("standard"));
743 assert_eq!(usage.iterations.len(), 1);
744 assert_eq!(usage.iterations[0].input_tokens, 3817);
745 assert_eq!(usage.iterations[0].output_tokens, 14);
746 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
747 } else {
748 panic!("Expected Result");
749 }
750 }
751
752 #[test]
753 fn test_result_backwards_compatible_without_new_fields() {
754 let json = r#"{
756 "type": "result",
757 "subtype": "success",
758 "is_error": false,
759 "duration_ms": 100,
760 "duration_api_ms": 200,
761 "num_turns": 1,
762 "session_id": "abc",
763 "total_cost_usd": 0.01
764 }"#;
765
766 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
767 if let ClaudeOutput::Result(res) = output {
768 assert!(res.api_error_status.is_none());
769 assert!(res.stop_reason.is_none());
770 assert!(res.terminal_reason.is_none());
771 assert!(res.fast_mode_state.is_none());
772 assert!(res.model_usage.is_none());
773 } else {
774 panic!("Expected Result");
775 }
776 }
777
778 #[test]
779 fn test_result_fast_mode_disabled_reason() {
780 let json = r#"{
781 "type":"result","subtype":"success","is_error":false,
782 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
783 "session_id":"s1","total_cost_usd":0.01,
784 "fast_mode_state":"off",
785 "fast_mode_disabled_reason":"sdk_opt_in_required"
786 }"#;
787 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
788 let crate::ClaudeOutput::Result(res) = &output else {
789 panic!("expected Result");
790 };
791 assert_eq!(
792 res.fast_mode_disabled_reason,
793 Some(FastModeDisabledReason::SdkOptInRequired)
794 );
795 assert!(serde_json::to_string(&output)
796 .unwrap()
797 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
798
799 assert_eq!(
802 FastModeDisabledReason::from("unknown"),
803 FastModeDisabledReason::UnknownReason
804 );
805 let novel = FastModeDisabledReason::from("solar_flare");
806 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
807 assert_eq!(novel.as_str(), "solar_flare");
808 }
809
810 #[test]
811 fn test_result_timing_and_user_message_uuid_fields() {
812 let json = r#"{
813 "type":"result","subtype":"success","is_error":false,
814 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
815 "session_id":"s1","total_cost_usd":0.01,
816 "request_sent_wall_ms":1753212345678.25,
817 "user_message_uuid":"um-1"
818 }"#;
819 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
820 let crate::ClaudeOutput::Result(res) = &output else {
821 panic!("expected Result");
822 };
823 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
824 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
825
826 let reserialized = serde_json::to_string(&output).unwrap();
827 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
828 }
829
830 #[test]
831 fn test_result_queued_turn_count_and_user_message_uuids() {
832 let json = r#"{
834 "type":"result","subtype":"success","is_error":false,
835 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
836 "session_id":"s1","total_cost_usd":0.01,
837 "user_message_uuid":"um-2",
838 "user_message_uuids":["um-1","um-2"],
839 "queued_turn_count":3
840 }"#;
841 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
842 let crate::ClaudeOutput::Result(res) = &output else {
843 panic!("expected Result");
844 };
845 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
846 assert_eq!(res.queued_turn_count, Some(3));
847
848 let reserialized = serde_json::to_string(&output).unwrap();
849 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
850 assert!(reserialized.contains("\"queued_turn_count\":3"));
851
852 let old = 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 }"#;
858 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
859 let crate::ClaudeOutput::Result(res) = &output else {
860 panic!("expected Result");
861 };
862 assert!(res.user_message_uuids.is_empty());
863 assert_eq!(res.queued_turn_count, None);
864 let reserialized = serde_json::to_string(&output).unwrap();
865 assert!(!reserialized.contains("user_message_uuids"));
866 assert!(!reserialized.contains("queued_turn_count"));
867 }
868}