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")]
83 pub resume_reason: Option<String>,
84
85 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub local_command: Option<String>,
90
91 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub queued_turn_count: Option<u64>,
98
99 pub num_turns: i32,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub result: Option<String>,
103
104 #[serde(alias = "sessionId")]
105 pub session_id: String,
106 pub total_cost_usd: f64,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub usage: Option<UsageInfo>,
110
111 #[serde(default)]
113 pub permission_denials: Vec<PermissionDenial>,
114
115 #[serde(default)]
120 pub errors: Vec<String>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub uuid: Option<String>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub api_error_status: Option<u16>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub stop_reason: Option<String>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub terminal_reason: Option<String>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub result_index: Option<u64>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub fast_mode_state: Option<String>,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
155 pub fast_mode_disabled_reason: Option<FastModeDisabledReason>,
156
157 #[serde(skip_serializing_if = "Option::is_none", rename = "modelUsage")]
159 pub model_usage: Option<std::collections::BTreeMap<String, ModelUsageEntry>>,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
165 pub subagent_stats: Option<SubagentStats>,
166
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub structured_output: Option<Value>,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub deferred_tool_use: Option<DeferredToolUse>,
174
175 #[serde(skip_serializing_if = "Option::is_none")]
177 pub origin: Option<super::message_types::MessageOrigin>,
178
179 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub runner_exit: Option<RunnerExit>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct RunnerExit {
191 pub phase: RunnerExitPhase,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub exit_code: Option<i64>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub signal: Option<String>,
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Hash)]
207pub enum RunnerExitPhase {
208 Setup,
210 Run,
212 Unknown(String),
214}
215
216impl RunnerExitPhase {
217 pub fn as_str(&self) -> &str {
218 match self {
219 Self::Setup => "setup",
220 Self::Run => "run",
221 Self::Unknown(s) => s.as_str(),
222 }
223 }
224}
225
226impl std::fmt::Display for RunnerExitPhase {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 f.write_str(self.as_str())
229 }
230}
231
232impl From<&str> for RunnerExitPhase {
233 fn from(s: &str) -> Self {
234 match s {
235 "setup" => Self::Setup,
236 "run" => Self::Run,
237 other => Self::Unknown(other.to_string()),
238 }
239 }
240}
241
242impl Serialize for RunnerExitPhase {
243 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
244 serializer.serialize_str(self.as_str())
245 }
246}
247
248impl<'de> Deserialize<'de> for RunnerExitPhase {
249 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
250 let s = String::deserialize(deserializer)?;
251 Ok(Self::from(s.as_str()))
252 }
253}
254
255#[derive(Debug, Clone, Default, Serialize, Deserialize)]
261#[serde(rename_all = "camelCase")]
262pub struct ModelUsageEntry {
263 #[serde(default)]
264 pub input_tokens: u64,
265 #[serde(default)]
266 pub output_tokens: u64,
267 #[serde(default)]
268 pub cache_read_input_tokens: u64,
269 #[serde(default)]
270 pub cache_creation_input_tokens: u64,
271 #[serde(default, rename = "costUSD")]
272 pub cost_usd: f64,
273 #[serde(default)]
274 pub web_search_requests: u32,
275 #[serde(default)]
276 pub context_window: u64,
277 #[serde(default)]
278 pub max_output_tokens: u64,
279 #[serde(flatten)]
280 pub extra: serde_json::Map<String, serde_json::Value>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
285pub struct DeferredToolUse {
286 pub id: String,
287 pub name: String,
288 pub input: Value,
289}
290
291#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
300pub struct SubagentStats {
301 pub spawned: u64,
303 pub requested: SubagentSpawnRequests,
306 pub started_in_background: u64,
309 #[serde(default)]
311 pub by_type: std::collections::BTreeMap<String, u64>,
312 pub max_depth: u64,
315 pub spawned_by_subagents: u64,
317 pub completed: u64,
318 pub failed: u64,
319 pub killed: SubagentKillCounts,
321 pub refused: SubagentRefusalCounts,
324}
325
326#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
329pub struct SubagentSpawnRequests {
330 pub background: u64,
331 pub foreground: u64,
332 pub unset: u64,
333}
334
335#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
339pub struct SubagentKillCounts {
340 pub parent: u64,
341 pub user: u64,
342 pub system: u64,
343}
344
345#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
347pub struct SubagentRefusalCounts {
348 pub depth_limit: u64,
349 pub concurrency_limit: u64,
350 pub budget: u64,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
358pub struct PermissionDenial {
359 pub tool_name: String,
361
362 pub tool_input: Value,
364
365 pub tool_use_id: String,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq, Hash)]
372pub enum FastModeDisabledReason {
373 Free,
375 Preference,
377 ExtraUsageDisabled,
379 NetworkError,
381 UnknownReason,
383 NotFirstParty,
385 DisabledByEnv,
387 ModelNotAllowed,
389 SdkOptInRequired,
391 Pending,
393 Unknown(String),
395}
396
397impl FastModeDisabledReason {
398 pub fn as_str(&self) -> &str {
399 match self {
400 Self::Free => "free",
401 Self::Preference => "preference",
402 Self::ExtraUsageDisabled => "extra_usage_disabled",
403 Self::NetworkError => "network_error",
404 Self::UnknownReason => "unknown",
405 Self::NotFirstParty => "not_first_party",
406 Self::DisabledByEnv => "disabled_by_env",
407 Self::ModelNotAllowed => "model_not_allowed",
408 Self::SdkOptInRequired => "sdk_opt_in_required",
409 Self::Pending => "pending",
410 Self::Unknown(s) => s.as_str(),
411 }
412 }
413}
414
415impl fmt::Display for FastModeDisabledReason {
416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417 f.write_str(self.as_str())
418 }
419}
420
421impl From<&str> for FastModeDisabledReason {
422 fn from(s: &str) -> Self {
423 match s {
424 "free" => Self::Free,
425 "preference" => Self::Preference,
426 "extra_usage_disabled" => Self::ExtraUsageDisabled,
427 "network_error" => Self::NetworkError,
428 "unknown" => Self::UnknownReason,
429 "not_first_party" => Self::NotFirstParty,
430 "disabled_by_env" => Self::DisabledByEnv,
431 "model_not_allowed" => Self::ModelNotAllowed,
432 "sdk_opt_in_required" => Self::SdkOptInRequired,
433 "pending" => Self::Pending,
434 other => Self::Unknown(other.to_string()),
435 }
436 }
437}
438
439impl Serialize for FastModeDisabledReason {
440 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
441 serializer.serialize_str(self.as_str())
442 }
443}
444
445impl<'de> Deserialize<'de> for FastModeDisabledReason {
446 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
447 let s = String::deserialize(deserializer)?;
448 Ok(Self::from(s.as_str()))
449 }
450}
451
452#[derive(Debug, Clone, PartialEq, Eq, Hash)]
454pub enum ResultSubtype {
455 Success,
456 ErrorMaxTurns,
457 ErrorDuringExecution,
458 ErrorMaxBudgetUsd,
459 ErrorMaxStructuredOutputRetries,
460 Unknown(String),
461}
462
463impl ResultSubtype {
464 pub fn as_str(&self) -> &str {
465 match self {
466 Self::Success => "success",
467 Self::ErrorMaxTurns => "error_max_turns",
468 Self::ErrorDuringExecution => "error_during_execution",
469 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
470 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
471 Self::Unknown(s) => s.as_str(),
472 }
473 }
474}
475
476impl fmt::Display for ResultSubtype {
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 f.write_str(self.as_str())
479 }
480}
481
482impl From<&str> for ResultSubtype {
483 fn from(s: &str) -> Self {
484 match s {
485 "success" => Self::Success,
486 "error_max_turns" => Self::ErrorMaxTurns,
487 "error_during_execution" => Self::ErrorDuringExecution,
488 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
489 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
490 other => Self::Unknown(other.to_string()),
491 }
492 }
493}
494
495impl Serialize for ResultSubtype {
496 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
497 serializer.serialize_str(self.as_str())
498 }
499}
500
501impl<'de> Deserialize<'de> for ResultSubtype {
502 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
503 let s = String::deserialize(deserializer)?;
504 Ok(Self::from(s.as_str()))
505 }
506}
507
508#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct UsageInfo {
527 #[serde(default)]
529 pub input_tokens: u32,
530 #[serde(default)]
532 pub cache_creation_input_tokens: u32,
533 #[serde(default)]
537 pub cache_read_input_tokens: u32,
538 #[serde(default)]
540 pub output_tokens: u32,
541 #[serde(default)]
542 pub server_tool_use: ServerToolUse,
543 #[serde(default)]
544 pub service_tier: String,
545
546 #[serde(skip_serializing_if = "Option::is_none")]
548 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
549
550 #[serde(skip_serializing_if = "Option::is_none")]
552 pub inference_geo: Option<String>,
553
554 #[serde(default, skip_serializing_if = "Vec::is_empty")]
558 pub iterations: Vec<UsageIteration>,
559
560 #[serde(skip_serializing_if = "Option::is_none")]
562 pub speed: Option<String>,
563
564 #[serde(skip_serializing_if = "Option::is_none")]
567 pub output_tokens_details: Option<OutputTokensDetails>,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize, Default)]
573pub struct OutputTokensDetails {
574 #[serde(skip_serializing_if = "Option::is_none")]
576 pub thinking_tokens: Option<u64>,
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct UsageIteration {
589 #[serde(default)]
590 pub input_tokens: u32,
591 #[serde(default)]
592 pub output_tokens: u32,
593 #[serde(default, skip_serializing_if = "Option::is_none")]
595 pub cache_read_input_tokens: Option<u32>,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
598 pub cache_creation_input_tokens: Option<u32>,
599 #[serde(default, skip_serializing_if = "Option::is_none")]
601 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
602 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
604 pub kind: Option<String>,
605}
606
607#[derive(Debug, Clone, Default, Serialize, Deserialize)]
609pub struct ServerToolUse {
610 #[serde(default)]
611 pub web_search_requests: u32,
612 #[serde(default)]
614 pub web_fetch_requests: u32,
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use crate::io::ClaudeOutput;
621
622 #[test]
623 fn test_deserialize_result_message() {
624 let json = r#"{
625 "type": "result",
626 "subtype": "success",
627 "is_error": false,
628 "duration_ms": 100,
629 "duration_api_ms": 200,
630 "num_turns": 1,
631 "result": "Done",
632 "session_id": "123",
633 "total_cost_usd": 0.01,
634 "permission_denials": []
635 }"#;
636
637 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
638 assert!(!output.is_error());
639 }
640
641 #[test]
642 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
643 let json = r#"{
644 "type": "result",
645 "subtype": "error_max_budget_usd",
646 "is_error": true,
647 "duration_ms": 100,
648 "duration_api_ms": 200,
649 "num_turns": 1,
650 "session_id": "123",
651 "total_cost_usd": 0.01
652 }"#;
653
654 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
655 let ClaudeOutput::Result(result) = output else {
656 panic!("Expected Result");
657 };
658 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
659
660 let json = r#"{
661 "type": "result",
662 "subtype": "future_result_subtype",
663 "is_error": true,
664 "duration_ms": 100,
665 "duration_api_ms": 200,
666 "num_turns": 1,
667 "session_id": "123",
668 "total_cost_usd": 0.01
669 }"#;
670
671 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
672 let ClaudeOutput::Result(result) = output else {
673 panic!("Expected Result");
674 };
675 assert_eq!(
676 result.subtype,
677 ResultSubtype::Unknown("future_result_subtype".to_string())
678 );
679 }
680
681 #[test]
682 fn test_deserialize_result_with_permission_denials() {
683 let json = r#"{
684 "type": "result",
685 "subtype": "success",
686 "is_error": false,
687 "duration_ms": 100,
688 "duration_api_ms": 200,
689 "num_turns": 2,
690 "result": "Done",
691 "session_id": "123",
692 "total_cost_usd": 0.01,
693 "permission_denials": [
694 {
695 "tool_name": "Bash",
696 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
697 "tool_use_id": "toolu_123"
698 }
699 ]
700 }"#;
701
702 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
703 if let ClaudeOutput::Result(result) = output {
704 assert_eq!(result.permission_denials.len(), 1);
705 assert_eq!(result.permission_denials[0].tool_name, "Bash");
706 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
707 assert_eq!(
708 result.permission_denials[0]
709 .tool_input
710 .get("command")
711 .unwrap(),
712 "rm -rf /"
713 );
714 } else {
715 panic!("Expected Result");
716 }
717 }
718
719 #[test]
720 fn test_permission_denial_roundtrip() {
721 let denial = PermissionDenial {
722 tool_name: "Write".to_string(),
723 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
724 tool_use_id: "toolu_456".to_string(),
725 };
726
727 let json = serde_json::to_string(&denial).unwrap();
728 assert!(json.contains("\"tool_name\":\"Write\""));
729 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
730 assert!(json.contains("/etc/passwd"));
731
732 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
733 assert_eq!(parsed, denial);
734 }
735
736 #[test]
737 fn test_deserialize_result_message_with_errors() {
738 let json = r#"{
739 "type": "result",
740 "subtype": "error_during_execution",
741 "duration_ms": 0,
742 "duration_api_ms": 0,
743 "is_error": true,
744 "num_turns": 0,
745 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
746 "total_cost_usd": 0,
747 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
748 "permission_denials": []
749 }"#;
750
751 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
752 assert!(output.is_error());
753
754 if let ClaudeOutput::Result(res) = output {
755 assert!(res.is_error);
756 assert_eq!(res.errors.len(), 1);
757 assert!(res.errors[0].contains("No conversation found"));
758 } else {
759 panic!("Expected Result message");
760 }
761 }
762
763 #[test]
764 fn test_deserialize_result_message_errors_defaults_empty() {
765 let json = r#"{
766 "type": "result",
767 "subtype": "success",
768 "is_error": false,
769 "duration_ms": 100,
770 "duration_api_ms": 200,
771 "num_turns": 1,
772 "session_id": "123",
773 "total_cost_usd": 0.01
774 }"#;
775
776 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
777 if let ClaudeOutput::Result(res) = output {
778 assert!(res.errors.is_empty());
779 } else {
780 panic!("Expected Result message");
781 }
782 }
783
784 #[test]
785 fn test_result_message_errors_roundtrip() {
786 let json = r#"{
787 "type": "result",
788 "subtype": "error_during_execution",
789 "is_error": true,
790 "duration_ms": 0,
791 "duration_api_ms": 0,
792 "num_turns": 0,
793 "session_id": "test-session",
794 "total_cost_usd": 0.0,
795 "errors": ["Error 1", "Error 2"]
796 }"#;
797
798 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
799 let reserialized = serde_json::to_string(&output).unwrap();
800
801 assert!(reserialized.contains("Error 1"));
802 assert!(reserialized.contains("Error 2"));
803 }
804
805 #[test]
806 fn test_result_with_new_fields() {
807 let json = r#"{
808 "type": "result",
809 "subtype": "success",
810 "is_error": false,
811 "duration_ms": 5000,
812 "duration_api_ms": 4500,
813 "num_turns": 1,
814 "result": "Done",
815 "session_id": "abc",
816 "total_cost_usd": 0.06,
817 "api_error_status": null,
818 "stop_reason": "end_turn",
819 "terminal_reason": "completed",
820 "fast_mode_state": "off",
821 "modelUsage": {
822 "claude-opus-4-7[1m]": {
823 "inputTokens": 3817,
824 "outputTokens": 14,
825 "costUSD": 0.06
826 }
827 },
828 "usage": {
829 "input_tokens": 3817,
830 "output_tokens": 14,
831 "cache_creation_input_tokens": 3540,
832 "cache_read_input_tokens": 0,
833 "server_tool_use": {
834 "web_search_requests": 0,
835 "web_fetch_requests": 2
836 },
837 "service_tier": "standard",
838 "inference_geo": "not_available",
839 "speed": "standard",
840 "iterations": [
841 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
842 ]
843 }
844 }"#;
845
846 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
847 if let ClaudeOutput::Result(res) = output {
848 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
849 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
850 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
851 let model_usage = res.model_usage.as_ref().unwrap();
852 let entry = model_usage
853 .get("claude-opus-4-7[1m]")
854 .expect("per-model entry present");
855 assert_eq!(entry.input_tokens, 3817);
856 assert_eq!(entry.output_tokens, 14);
857 assert_eq!(entry.cost_usd, 0.06);
858 assert!(res.api_error_status.is_none());
859
860 let usage = res.usage.unwrap();
861 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
862 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
863 assert_eq!(usage.speed.as_deref(), Some("standard"));
864 assert_eq!(usage.iterations.len(), 1);
865 assert_eq!(usage.iterations[0].input_tokens, 3817);
866 assert_eq!(usage.iterations[0].output_tokens, 14);
867 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
868 } else {
869 panic!("Expected Result");
870 }
871 }
872
873 #[test]
874 fn test_result_backwards_compatible_without_new_fields() {
875 let json = r#"{
877 "type": "result",
878 "subtype": "success",
879 "is_error": false,
880 "duration_ms": 100,
881 "duration_api_ms": 200,
882 "num_turns": 1,
883 "session_id": "abc",
884 "total_cost_usd": 0.01
885 }"#;
886
887 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
888 if let ClaudeOutput::Result(res) = output {
889 assert!(res.api_error_status.is_none());
890 assert!(res.stop_reason.is_none());
891 assert!(res.terminal_reason.is_none());
892 assert!(res.fast_mode_state.is_none());
893 assert!(res.model_usage.is_none());
894 } else {
895 panic!("Expected Result");
896 }
897 }
898
899 #[test]
900 fn test_result_fast_mode_disabled_reason() {
901 let json = r#"{
902 "type":"result","subtype":"success","is_error":false,
903 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
904 "session_id":"s1","total_cost_usd":0.01,
905 "fast_mode_state":"off",
906 "fast_mode_disabled_reason":"sdk_opt_in_required"
907 }"#;
908 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
909 let crate::ClaudeOutput::Result(res) = &output else {
910 panic!("expected Result");
911 };
912 assert_eq!(
913 res.fast_mode_disabled_reason,
914 Some(FastModeDisabledReason::SdkOptInRequired)
915 );
916 assert!(serde_json::to_string(&output)
917 .unwrap()
918 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
919
920 assert_eq!(
923 FastModeDisabledReason::from("unknown"),
924 FastModeDisabledReason::UnknownReason
925 );
926 let novel = FastModeDisabledReason::from("solar_flare");
927 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
928 assert_eq!(novel.as_str(), "solar_flare");
929 }
930
931 #[test]
932 fn test_result_timing_and_user_message_uuid_fields() {
933 let json = r#"{
934 "type":"result","subtype":"success","is_error":false,
935 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
936 "session_id":"s1","total_cost_usd":0.01,
937 "request_sent_wall_ms":1753212345678.25,
938 "user_message_uuid":"um-1"
939 }"#;
940 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
941 let crate::ClaudeOutput::Result(res) = &output else {
942 panic!("expected Result");
943 };
944 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
945 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
946
947 let reserialized = serde_json::to_string(&output).unwrap();
948 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
949 }
950
951 #[test]
952 fn test_result_queued_turn_count_and_user_message_uuids() {
953 let json = r#"{
955 "type":"result","subtype":"success","is_error":false,
956 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
957 "session_id":"s1","total_cost_usd":0.01,
958 "user_message_uuid":"um-2",
959 "user_message_uuids":["um-1","um-2"],
960 "queued_turn_count":3
961 }"#;
962 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
963 let crate::ClaudeOutput::Result(res) = &output else {
964 panic!("expected Result");
965 };
966 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
967 assert_eq!(res.queued_turn_count, Some(3));
968
969 let reserialized = serde_json::to_string(&output).unwrap();
970 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
971 assert!(reserialized.contains("\"queued_turn_count\":3"));
972
973 let old = r#"{
975 "type":"result","subtype":"success","is_error":false,
976 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
977 "session_id":"s1","total_cost_usd":0.01
978 }"#;
979 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
980 let crate::ClaudeOutput::Result(res) = &output else {
981 panic!("expected Result");
982 };
983 assert!(res.user_message_uuids.is_empty());
984 assert_eq!(res.queued_turn_count, None);
985 let reserialized = serde_json::to_string(&output).unwrap();
986 assert!(!reserialized.contains("user_message_uuids"));
987 assert!(!reserialized.contains("queued_turn_count"));
988 }
989}