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 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub runner_exit: Option<RunnerExit>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
164pub struct RunnerExit {
165 pub phase: RunnerExitPhase,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub exit_code: Option<i64>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub signal: Option<String>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Hash)]
181pub enum RunnerExitPhase {
182 Setup,
184 Run,
186 Unknown(String),
188}
189
190impl RunnerExitPhase {
191 pub fn as_str(&self) -> &str {
192 match self {
193 Self::Setup => "setup",
194 Self::Run => "run",
195 Self::Unknown(s) => s.as_str(),
196 }
197 }
198}
199
200impl std::fmt::Display for RunnerExitPhase {
201 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202 f.write_str(self.as_str())
203 }
204}
205
206impl From<&str> for RunnerExitPhase {
207 fn from(s: &str) -> Self {
208 match s {
209 "setup" => Self::Setup,
210 "run" => Self::Run,
211 other => Self::Unknown(other.to_string()),
212 }
213 }
214}
215
216impl Serialize for RunnerExitPhase {
217 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
218 serializer.serialize_str(self.as_str())
219 }
220}
221
222impl<'de> Deserialize<'de> for RunnerExitPhase {
223 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
224 let s = String::deserialize(deserializer)?;
225 Ok(Self::from(s.as_str()))
226 }
227}
228
229#[derive(Debug, Clone, Default, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct ModelUsageEntry {
237 #[serde(default)]
238 pub input_tokens: u64,
239 #[serde(default)]
240 pub output_tokens: u64,
241 #[serde(default)]
242 pub cache_read_input_tokens: u64,
243 #[serde(default)]
244 pub cache_creation_input_tokens: u64,
245 #[serde(default, rename = "costUSD")]
246 pub cost_usd: f64,
247 #[serde(default)]
248 pub web_search_requests: u32,
249 #[serde(default)]
250 pub context_window: u64,
251 #[serde(default)]
252 pub max_output_tokens: u64,
253 #[serde(flatten)]
254 pub extra: serde_json::Map<String, serde_json::Value>,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
259pub struct DeferredToolUse {
260 pub id: String,
261 pub name: String,
262 pub input: Value,
263}
264
265#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
274pub struct SubagentStats {
275 pub spawned: u64,
277 pub requested: SubagentSpawnRequests,
280 pub started_in_background: u64,
283 #[serde(default)]
285 pub by_type: std::collections::BTreeMap<String, u64>,
286 pub max_depth: u64,
289 pub spawned_by_subagents: u64,
291 pub completed: u64,
292 pub failed: u64,
293 pub killed: SubagentKillCounts,
295 pub refused: SubagentRefusalCounts,
298}
299
300#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
303pub struct SubagentSpawnRequests {
304 pub background: u64,
305 pub foreground: u64,
306 pub unset: u64,
307}
308
309#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
313pub struct SubagentKillCounts {
314 pub parent: u64,
315 pub user: u64,
316 pub system: u64,
317}
318
319#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
321pub struct SubagentRefusalCounts {
322 pub depth_limit: u64,
323 pub concurrency_limit: u64,
324 pub budget: u64,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
332pub struct PermissionDenial {
333 pub tool_name: String,
335
336 pub tool_input: Value,
338
339 pub tool_use_id: String,
341}
342
343#[derive(Debug, Clone, PartialEq, Eq, Hash)]
346pub enum FastModeDisabledReason {
347 Free,
349 Preference,
351 ExtraUsageDisabled,
353 NetworkError,
355 UnknownReason,
357 NotFirstParty,
359 DisabledByEnv,
361 ModelNotAllowed,
363 SdkOptInRequired,
365 Pending,
367 Unknown(String),
369}
370
371impl FastModeDisabledReason {
372 pub fn as_str(&self) -> &str {
373 match self {
374 Self::Free => "free",
375 Self::Preference => "preference",
376 Self::ExtraUsageDisabled => "extra_usage_disabled",
377 Self::NetworkError => "network_error",
378 Self::UnknownReason => "unknown",
379 Self::NotFirstParty => "not_first_party",
380 Self::DisabledByEnv => "disabled_by_env",
381 Self::ModelNotAllowed => "model_not_allowed",
382 Self::SdkOptInRequired => "sdk_opt_in_required",
383 Self::Pending => "pending",
384 Self::Unknown(s) => s.as_str(),
385 }
386 }
387}
388
389impl fmt::Display for FastModeDisabledReason {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 f.write_str(self.as_str())
392 }
393}
394
395impl From<&str> for FastModeDisabledReason {
396 fn from(s: &str) -> Self {
397 match s {
398 "free" => Self::Free,
399 "preference" => Self::Preference,
400 "extra_usage_disabled" => Self::ExtraUsageDisabled,
401 "network_error" => Self::NetworkError,
402 "unknown" => Self::UnknownReason,
403 "not_first_party" => Self::NotFirstParty,
404 "disabled_by_env" => Self::DisabledByEnv,
405 "model_not_allowed" => Self::ModelNotAllowed,
406 "sdk_opt_in_required" => Self::SdkOptInRequired,
407 "pending" => Self::Pending,
408 other => Self::Unknown(other.to_string()),
409 }
410 }
411}
412
413impl Serialize for FastModeDisabledReason {
414 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
415 serializer.serialize_str(self.as_str())
416 }
417}
418
419impl<'de> Deserialize<'de> for FastModeDisabledReason {
420 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
421 let s = String::deserialize(deserializer)?;
422 Ok(Self::from(s.as_str()))
423 }
424}
425
426#[derive(Debug, Clone, PartialEq, Eq, Hash)]
428pub enum ResultSubtype {
429 Success,
430 ErrorMaxTurns,
431 ErrorDuringExecution,
432 ErrorMaxBudgetUsd,
433 ErrorMaxStructuredOutputRetries,
434 Unknown(String),
435}
436
437impl ResultSubtype {
438 pub fn as_str(&self) -> &str {
439 match self {
440 Self::Success => "success",
441 Self::ErrorMaxTurns => "error_max_turns",
442 Self::ErrorDuringExecution => "error_during_execution",
443 Self::ErrorMaxBudgetUsd => "error_max_budget_usd",
444 Self::ErrorMaxStructuredOutputRetries => "error_max_structured_output_retries",
445 Self::Unknown(s) => s.as_str(),
446 }
447 }
448}
449
450impl fmt::Display for ResultSubtype {
451 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452 f.write_str(self.as_str())
453 }
454}
455
456impl From<&str> for ResultSubtype {
457 fn from(s: &str) -> Self {
458 match s {
459 "success" => Self::Success,
460 "error_max_turns" => Self::ErrorMaxTurns,
461 "error_during_execution" => Self::ErrorDuringExecution,
462 "error_max_budget_usd" => Self::ErrorMaxBudgetUsd,
463 "error_max_structured_output_retries" => Self::ErrorMaxStructuredOutputRetries,
464 other => Self::Unknown(other.to_string()),
465 }
466 }
467}
468
469impl Serialize for ResultSubtype {
470 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
471 serializer.serialize_str(self.as_str())
472 }
473}
474
475impl<'de> Deserialize<'de> for ResultSubtype {
476 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
477 let s = String::deserialize(deserializer)?;
478 Ok(Self::from(s.as_str()))
479 }
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct UsageInfo {
501 #[serde(default)]
503 pub input_tokens: u32,
504 #[serde(default)]
506 pub cache_creation_input_tokens: u32,
507 #[serde(default)]
511 pub cache_read_input_tokens: u32,
512 #[serde(default)]
514 pub output_tokens: u32,
515 #[serde(default)]
516 pub server_tool_use: ServerToolUse,
517 #[serde(default)]
518 pub service_tier: String,
519
520 #[serde(skip_serializing_if = "Option::is_none")]
522 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
523
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub inference_geo: Option<String>,
527
528 #[serde(default, skip_serializing_if = "Vec::is_empty")]
532 pub iterations: Vec<UsageIteration>,
533
534 #[serde(skip_serializing_if = "Option::is_none")]
536 pub speed: Option<String>,
537
538 #[serde(skip_serializing_if = "Option::is_none")]
541 pub output_tokens_details: Option<OutputTokensDetails>,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize, Default)]
547pub struct OutputTokensDetails {
548 #[serde(skip_serializing_if = "Option::is_none")]
550 pub thinking_tokens: Option<u64>,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
562pub struct UsageIteration {
563 #[serde(default)]
564 pub input_tokens: u32,
565 #[serde(default)]
566 pub output_tokens: u32,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
569 pub cache_read_input_tokens: Option<u32>,
570 #[serde(default, skip_serializing_if = "Option::is_none")]
572 pub cache_creation_input_tokens: Option<u32>,
573 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub cache_creation: Option<super::message_types::CacheCreationDetails>,
576 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
578 pub kind: Option<String>,
579}
580
581#[derive(Debug, Clone, Default, Serialize, Deserialize)]
583pub struct ServerToolUse {
584 #[serde(default)]
585 pub web_search_requests: u32,
586 #[serde(default)]
588 pub web_fetch_requests: u32,
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594 use crate::io::ClaudeOutput;
595
596 #[test]
597 fn test_deserialize_result_message() {
598 let json = r#"{
599 "type": "result",
600 "subtype": "success",
601 "is_error": false,
602 "duration_ms": 100,
603 "duration_api_ms": 200,
604 "num_turns": 1,
605 "result": "Done",
606 "session_id": "123",
607 "total_cost_usd": 0.01,
608 "permission_denials": []
609 }"#;
610
611 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
612 assert!(!output.is_error());
613 }
614
615 #[test]
616 fn test_result_subtype_new_and_unknown_values_do_not_fail() {
617 let json = r#"{
618 "type": "result",
619 "subtype": "error_max_budget_usd",
620 "is_error": true,
621 "duration_ms": 100,
622 "duration_api_ms": 200,
623 "num_turns": 1,
624 "session_id": "123",
625 "total_cost_usd": 0.01
626 }"#;
627
628 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
629 let ClaudeOutput::Result(result) = output else {
630 panic!("Expected Result");
631 };
632 assert_eq!(result.subtype, ResultSubtype::ErrorMaxBudgetUsd);
633
634 let json = r#"{
635 "type": "result",
636 "subtype": "future_result_subtype",
637 "is_error": true,
638 "duration_ms": 100,
639 "duration_api_ms": 200,
640 "num_turns": 1,
641 "session_id": "123",
642 "total_cost_usd": 0.01
643 }"#;
644
645 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
646 let ClaudeOutput::Result(result) = output else {
647 panic!("Expected Result");
648 };
649 assert_eq!(
650 result.subtype,
651 ResultSubtype::Unknown("future_result_subtype".to_string())
652 );
653 }
654
655 #[test]
656 fn test_deserialize_result_with_permission_denials() {
657 let json = r#"{
658 "type": "result",
659 "subtype": "success",
660 "is_error": false,
661 "duration_ms": 100,
662 "duration_api_ms": 200,
663 "num_turns": 2,
664 "result": "Done",
665 "session_id": "123",
666 "total_cost_usd": 0.01,
667 "permission_denials": [
668 {
669 "tool_name": "Bash",
670 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
671 "tool_use_id": "toolu_123"
672 }
673 ]
674 }"#;
675
676 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
677 if let ClaudeOutput::Result(result) = output {
678 assert_eq!(result.permission_denials.len(), 1);
679 assert_eq!(result.permission_denials[0].tool_name, "Bash");
680 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
681 assert_eq!(
682 result.permission_denials[0]
683 .tool_input
684 .get("command")
685 .unwrap(),
686 "rm -rf /"
687 );
688 } else {
689 panic!("Expected Result");
690 }
691 }
692
693 #[test]
694 fn test_permission_denial_roundtrip() {
695 let denial = PermissionDenial {
696 tool_name: "Write".to_string(),
697 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
698 tool_use_id: "toolu_456".to_string(),
699 };
700
701 let json = serde_json::to_string(&denial).unwrap();
702 assert!(json.contains("\"tool_name\":\"Write\""));
703 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
704 assert!(json.contains("/etc/passwd"));
705
706 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
707 assert_eq!(parsed, denial);
708 }
709
710 #[test]
711 fn test_deserialize_result_message_with_errors() {
712 let json = r#"{
713 "type": "result",
714 "subtype": "error_during_execution",
715 "duration_ms": 0,
716 "duration_api_ms": 0,
717 "is_error": true,
718 "num_turns": 0,
719 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
720 "total_cost_usd": 0,
721 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
722 "permission_denials": []
723 }"#;
724
725 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
726 assert!(output.is_error());
727
728 if let ClaudeOutput::Result(res) = output {
729 assert!(res.is_error);
730 assert_eq!(res.errors.len(), 1);
731 assert!(res.errors[0].contains("No conversation found"));
732 } else {
733 panic!("Expected Result message");
734 }
735 }
736
737 #[test]
738 fn test_deserialize_result_message_errors_defaults_empty() {
739 let json = r#"{
740 "type": "result",
741 "subtype": "success",
742 "is_error": false,
743 "duration_ms": 100,
744 "duration_api_ms": 200,
745 "num_turns": 1,
746 "session_id": "123",
747 "total_cost_usd": 0.01
748 }"#;
749
750 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
751 if let ClaudeOutput::Result(res) = output {
752 assert!(res.errors.is_empty());
753 } else {
754 panic!("Expected Result message");
755 }
756 }
757
758 #[test]
759 fn test_result_message_errors_roundtrip() {
760 let json = r#"{
761 "type": "result",
762 "subtype": "error_during_execution",
763 "is_error": true,
764 "duration_ms": 0,
765 "duration_api_ms": 0,
766 "num_turns": 0,
767 "session_id": "test-session",
768 "total_cost_usd": 0.0,
769 "errors": ["Error 1", "Error 2"]
770 }"#;
771
772 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
773 let reserialized = serde_json::to_string(&output).unwrap();
774
775 assert!(reserialized.contains("Error 1"));
776 assert!(reserialized.contains("Error 2"));
777 }
778
779 #[test]
780 fn test_result_with_new_fields() {
781 let json = r#"{
782 "type": "result",
783 "subtype": "success",
784 "is_error": false,
785 "duration_ms": 5000,
786 "duration_api_ms": 4500,
787 "num_turns": 1,
788 "result": "Done",
789 "session_id": "abc",
790 "total_cost_usd": 0.06,
791 "api_error_status": null,
792 "stop_reason": "end_turn",
793 "terminal_reason": "completed",
794 "fast_mode_state": "off",
795 "modelUsage": {
796 "claude-opus-4-7[1m]": {
797 "inputTokens": 3817,
798 "outputTokens": 14,
799 "costUSD": 0.06
800 }
801 },
802 "usage": {
803 "input_tokens": 3817,
804 "output_tokens": 14,
805 "cache_creation_input_tokens": 3540,
806 "cache_read_input_tokens": 0,
807 "server_tool_use": {
808 "web_search_requests": 0,
809 "web_fetch_requests": 2
810 },
811 "service_tier": "standard",
812 "inference_geo": "not_available",
813 "speed": "standard",
814 "iterations": [
815 {"input_tokens": 3817, "output_tokens": 14, "type": "turn"}
816 ]
817 }
818 }"#;
819
820 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
821 if let ClaudeOutput::Result(res) = output {
822 assert_eq!(res.stop_reason.as_deref(), Some("end_turn"));
823 assert_eq!(res.terminal_reason.as_deref(), Some("completed"));
824 assert_eq!(res.fast_mode_state.as_deref(), Some("off"));
825 let model_usage = res.model_usage.as_ref().unwrap();
826 let entry = model_usage
827 .get("claude-opus-4-7[1m]")
828 .expect("per-model entry present");
829 assert_eq!(entry.input_tokens, 3817);
830 assert_eq!(entry.output_tokens, 14);
831 assert_eq!(entry.cost_usd, 0.06);
832 assert!(res.api_error_status.is_none());
833
834 let usage = res.usage.unwrap();
835 assert_eq!(usage.server_tool_use.web_fetch_requests, 2);
836 assert_eq!(usage.inference_geo.as_deref(), Some("not_available"));
837 assert_eq!(usage.speed.as_deref(), Some("standard"));
838 assert_eq!(usage.iterations.len(), 1);
839 assert_eq!(usage.iterations[0].input_tokens, 3817);
840 assert_eq!(usage.iterations[0].output_tokens, 14);
841 assert_eq!(usage.iterations[0].kind.as_deref(), Some("turn"));
842 } else {
843 panic!("Expected Result");
844 }
845 }
846
847 #[test]
848 fn test_result_backwards_compatible_without_new_fields() {
849 let json = r#"{
851 "type": "result",
852 "subtype": "success",
853 "is_error": false,
854 "duration_ms": 100,
855 "duration_api_ms": 200,
856 "num_turns": 1,
857 "session_id": "abc",
858 "total_cost_usd": 0.01
859 }"#;
860
861 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
862 if let ClaudeOutput::Result(res) = output {
863 assert!(res.api_error_status.is_none());
864 assert!(res.stop_reason.is_none());
865 assert!(res.terminal_reason.is_none());
866 assert!(res.fast_mode_state.is_none());
867 assert!(res.model_usage.is_none());
868 } else {
869 panic!("Expected Result");
870 }
871 }
872
873 #[test]
874 fn test_result_fast_mode_disabled_reason() {
875 let json = r#"{
876 "type":"result","subtype":"success","is_error":false,
877 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
878 "session_id":"s1","total_cost_usd":0.01,
879 "fast_mode_state":"off",
880 "fast_mode_disabled_reason":"sdk_opt_in_required"
881 }"#;
882 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
883 let crate::ClaudeOutput::Result(res) = &output else {
884 panic!("expected Result");
885 };
886 assert_eq!(
887 res.fast_mode_disabled_reason,
888 Some(FastModeDisabledReason::SdkOptInRequired)
889 );
890 assert!(serde_json::to_string(&output)
891 .unwrap()
892 .contains("\"fast_mode_disabled_reason\":\"sdk_opt_in_required\""));
893
894 assert_eq!(
897 FastModeDisabledReason::from("unknown"),
898 FastModeDisabledReason::UnknownReason
899 );
900 let novel = FastModeDisabledReason::from("solar_flare");
901 assert_eq!(novel, FastModeDisabledReason::Unknown("solar_flare".into()));
902 assert_eq!(novel.as_str(), "solar_flare");
903 }
904
905 #[test]
906 fn test_result_timing_and_user_message_uuid_fields() {
907 let json = r#"{
908 "type":"result","subtype":"success","is_error":false,
909 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
910 "session_id":"s1","total_cost_usd":0.01,
911 "request_sent_wall_ms":1753212345678.25,
912 "user_message_uuid":"um-1"
913 }"#;
914 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
915 let crate::ClaudeOutput::Result(res) = &output else {
916 panic!("expected Result");
917 };
918 assert_eq!(res.request_sent_wall_ms, Some(1753212345678.25));
919 assert_eq!(res.user_message_uuid.as_deref(), Some("um-1"));
920
921 let reserialized = serde_json::to_string(&output).unwrap();
922 assert!(reserialized.contains("\"user_message_uuid\":\"um-1\""));
923 }
924
925 #[test]
926 fn test_result_queued_turn_count_and_user_message_uuids() {
927 let json = r#"{
929 "type":"result","subtype":"success","is_error":false,
930 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
931 "session_id":"s1","total_cost_usd":0.01,
932 "user_message_uuid":"um-2",
933 "user_message_uuids":["um-1","um-2"],
934 "queued_turn_count":3
935 }"#;
936 let output: crate::ClaudeOutput = serde_json::from_str(json).unwrap();
937 let crate::ClaudeOutput::Result(res) = &output else {
938 panic!("expected Result");
939 };
940 assert_eq!(res.user_message_uuids, vec!["um-1", "um-2"]);
941 assert_eq!(res.queued_turn_count, Some(3));
942
943 let reserialized = serde_json::to_string(&output).unwrap();
944 assert!(reserialized.contains("\"user_message_uuids\":[\"um-1\",\"um-2\"]"));
945 assert!(reserialized.contains("\"queued_turn_count\":3"));
946
947 let old = r#"{
949 "type":"result","subtype":"success","is_error":false,
950 "duration_ms":100,"duration_api_ms":80,"num_turns":1,
951 "session_id":"s1","total_cost_usd":0.01
952 }"#;
953 let output: crate::ClaudeOutput = serde_json::from_str(old).unwrap();
954 let crate::ClaudeOutput::Result(res) = &output else {
955 panic!("expected Result");
956 };
957 assert!(res.user_message_uuids.is_empty());
958 assert_eq!(res.queued_turn_count, None);
959 let reserialized = serde_json::to_string(&output).unwrap();
960 assert!(!reserialized.contains("user_message_uuids"));
961 assert!(!reserialized.contains("queued_turn_count"));
962 }
963}