1use serde::{Deserialize, Deserializer, Serialize, Serializer};
33use serde_json::Value;
34use std::fmt;
35use uuid::Uuid;
36
37fn serialize_optional_uuid<S>(uuid: &Option<Uuid>, serializer: S) -> Result<S::Ok, S::Error>
39where
40 S: Serializer,
41{
42 match uuid {
43 Some(id) => serializer.serialize_str(&id.to_string()),
44 None => serializer.serialize_none(),
45 }
46}
47
48fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
50where
51 D: Deserializer<'de>,
52{
53 let opt_str: Option<String> = Option::deserialize(deserializer)?;
54 match opt_str {
55 Some(s) => Uuid::parse_str(&s)
56 .map(Some)
57 .map_err(serde::de::Error::custom),
58 None => Ok(None),
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(tag = "type", rename_all = "snake_case")]
65pub enum ClaudeInput {
66 User(UserMessage),
68
69 ControlRequest(ControlRequest),
71
72 ControlResponse(ControlResponse),
74
75 #[serde(untagged)]
77 Raw(Value),
78}
79
80#[derive(Debug, Clone)]
82pub struct ParseError {
83 pub raw_json: Value,
85 pub error_message: String,
87}
88
89impl fmt::Display for ParseError {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 write!(f, "Failed to parse ClaudeOutput: {}", self.error_message)
92 }
93}
94
95impl std::error::Error for ParseError {}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(tag = "type", rename_all = "snake_case")]
100pub enum ClaudeOutput {
101 System(SystemMessage),
103
104 User(UserMessage),
106
107 Assistant(AssistantMessage),
109
110 Result(ResultMessage),
112
113 ControlRequest(ControlRequest),
115
116 ControlResponse(ControlResponse),
118
119 Error(AnthropicError),
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
155pub struct AnthropicError {
156 pub error: AnthropicErrorDetails,
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub request_id: Option<String>,
161}
162
163impl AnthropicError {
164 pub fn is_overloaded(&self) -> bool {
166 self.error.error_type == "overloaded_error"
167 }
168
169 pub fn is_server_error(&self) -> bool {
171 self.error.error_type == "api_error"
172 }
173
174 pub fn is_invalid_request(&self) -> bool {
176 self.error.error_type == "invalid_request_error"
177 }
178
179 pub fn is_authentication_error(&self) -> bool {
181 self.error.error_type == "authentication_error"
182 }
183
184 pub fn is_rate_limited(&self) -> bool {
186 self.error.error_type == "rate_limit_error"
187 }
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
192pub struct AnthropicErrorDetails {
193 #[serde(rename = "type")]
195 pub error_type: String,
196 pub message: String,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct UserMessage {
203 pub message: MessageContent,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 #[serde(
206 serialize_with = "serialize_optional_uuid",
207 deserialize_with = "deserialize_optional_uuid"
208 )]
209 pub session_id: Option<Uuid>,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct MessageContent {
215 pub role: String,
216 #[serde(deserialize_with = "deserialize_content_blocks")]
217 pub content: Vec<ContentBlock>,
218}
219
220fn deserialize_content_blocks<'de, D>(deserializer: D) -> Result<Vec<ContentBlock>, D::Error>
222where
223 D: Deserializer<'de>,
224{
225 let value: Value = Value::deserialize(deserializer)?;
226 match value {
227 Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock { text: s })]),
228 Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
229 _ => Err(serde::de::Error::custom(
230 "content must be a string or array",
231 )),
232 }
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct SystemMessage {
238 pub subtype: String,
239 #[serde(flatten)]
240 pub data: Value, }
242
243impl SystemMessage {
244 pub fn is_init(&self) -> bool {
246 self.subtype == "init"
247 }
248
249 pub fn is_status(&self) -> bool {
251 self.subtype == "status"
252 }
253
254 pub fn is_compact_boundary(&self) -> bool {
256 self.subtype == "compact_boundary"
257 }
258
259 pub fn as_init(&self) -> Option<InitMessage> {
261 if self.subtype != "init" {
262 return None;
263 }
264 serde_json::from_value(self.data.clone()).ok()
265 }
266
267 pub fn as_status(&self) -> Option<StatusMessage> {
269 if self.subtype != "status" {
270 return None;
271 }
272 serde_json::from_value(self.data.clone()).ok()
273 }
274
275 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
277 if self.subtype != "compact_boundary" {
278 return None;
279 }
280 serde_json::from_value(self.data.clone()).ok()
281 }
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct InitMessage {
287 pub session_id: String,
289 #[serde(skip_serializing_if = "Option::is_none")]
291 pub cwd: Option<String>,
292 #[serde(skip_serializing_if = "Option::is_none")]
294 pub model: Option<String>,
295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
297 pub tools: Vec<String>,
298 #[serde(default, skip_serializing_if = "Vec::is_empty")]
300 pub mcp_servers: Vec<Value>,
301}
302
303#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct StatusMessage {
306 pub session_id: String,
308 pub status: Option<String>,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub uuid: Option<String>,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct CompactBoundaryMessage {
318 pub session_id: String,
320 pub compact_metadata: CompactMetadata,
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub uuid: Option<String>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct CompactMetadata {
330 pub pre_tokens: u64,
332 pub trigger: String,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct AssistantMessage {
339 pub message: AssistantMessageContent,
340 pub session_id: String,
341 #[serde(skip_serializing_if = "Option::is_none")]
342 pub uuid: Option<String>,
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub parent_tool_use_id: Option<String>,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct AssistantMessageContent {
350 pub id: String,
351 pub role: String,
352 pub model: String,
353 pub content: Vec<ContentBlock>,
354 #[serde(skip_serializing_if = "Option::is_none")]
355 pub stop_reason: Option<String>,
356 #[serde(skip_serializing_if = "Option::is_none")]
357 pub stop_sequence: Option<String>,
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub usage: Option<AssistantUsage>,
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct AssistantUsage {
365 #[serde(default)]
367 pub input_tokens: u32,
368
369 #[serde(default)]
371 pub output_tokens: u32,
372
373 #[serde(default)]
375 pub cache_creation_input_tokens: u32,
376
377 #[serde(default)]
379 pub cache_read_input_tokens: u32,
380
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub service_tier: Option<String>,
384
385 #[serde(skip_serializing_if = "Option::is_none")]
387 pub cache_creation: Option<CacheCreationDetails>,
388}
389
390#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct CacheCreationDetails {
393 #[serde(default)]
395 pub ephemeral_1h_input_tokens: u32,
396
397 #[serde(default)]
399 pub ephemeral_5m_input_tokens: u32,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize)]
404#[serde(tag = "type", rename_all = "snake_case")]
405pub enum ContentBlock {
406 Text(TextBlock),
407 Image(ImageBlock),
408 Thinking(ThinkingBlock),
409 ToolUse(ToolUseBlock),
410 ToolResult(ToolResultBlock),
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize)]
415pub struct TextBlock {
416 pub text: String,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize)]
421pub struct ImageBlock {
422 pub source: ImageSource,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct ImageSource {
428 #[serde(rename = "type")]
429 pub source_type: String, pub media_type: String, pub data: String, }
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436pub struct ThinkingBlock {
437 pub thinking: String,
438 pub signature: String,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct ToolUseBlock {
444 pub id: String,
445 pub name: String,
446 pub input: Value,
447}
448
449impl ToolUseBlock {
450 pub fn typed_input(&self) -> Option<crate::tool_inputs::ToolInput> {
472 serde_json::from_value(self.input.clone()).ok()
473 }
474
475 pub fn try_typed_input(&self) -> Result<crate::tool_inputs::ToolInput, serde_json::Error> {
479 serde_json::from_value(self.input.clone())
480 }
481}
482
483#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct ToolResultBlock {
486 pub tool_use_id: String,
487 #[serde(skip_serializing_if = "Option::is_none")]
488 pub content: Option<ToolResultContent>,
489 #[serde(skip_serializing_if = "Option::is_none")]
490 pub is_error: Option<bool>,
491}
492
493#[derive(Debug, Clone, Serialize, Deserialize)]
495#[serde(untagged)]
496pub enum ToolResultContent {
497 Text(String),
498 Structured(Vec<Value>),
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct ResultMessage {
504 pub subtype: ResultSubtype,
505 pub is_error: bool,
506 pub duration_ms: u64,
507 pub duration_api_ms: u64,
508 pub num_turns: i32,
509
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub result: Option<String>,
512
513 pub session_id: String,
514 pub total_cost_usd: f64,
515
516 #[serde(skip_serializing_if = "Option::is_none")]
517 pub usage: Option<UsageInfo>,
518
519 #[serde(default)]
521 pub permission_denials: Vec<PermissionDenial>,
522
523 #[serde(default)]
528 pub errors: Vec<String>,
529
530 #[serde(skip_serializing_if = "Option::is_none")]
531 pub uuid: Option<String>,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
539pub struct PermissionDenial {
540 pub tool_name: String,
542
543 pub tool_input: Value,
545
546 pub tool_use_id: String,
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize)]
552#[serde(rename_all = "snake_case")]
553pub enum ResultSubtype {
554 Success,
555 ErrorMaxTurns,
556 ErrorDuringExecution,
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize)]
561#[serde(tag = "type", rename_all = "snake_case")]
562pub enum McpServerConfig {
563 Stdio(McpStdioServerConfig),
564 Sse(McpSseServerConfig),
565 Http(McpHttpServerConfig),
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize)]
570pub struct McpStdioServerConfig {
571 pub command: String,
572 #[serde(skip_serializing_if = "Option::is_none")]
573 pub args: Option<Vec<String>>,
574 #[serde(skip_serializing_if = "Option::is_none")]
575 pub env: Option<std::collections::HashMap<String, String>>,
576}
577
578#[derive(Debug, Clone, Serialize, Deserialize)]
580pub struct McpSseServerConfig {
581 pub url: String,
582 #[serde(skip_serializing_if = "Option::is_none")]
583 pub headers: Option<std::collections::HashMap<String, String>>,
584}
585
586#[derive(Debug, Clone, Serialize, Deserialize)]
588pub struct McpHttpServerConfig {
589 pub url: String,
590 #[serde(skip_serializing_if = "Option::is_none")]
591 pub headers: Option<std::collections::HashMap<String, String>>,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize)]
596#[serde(rename_all = "camelCase")]
597pub enum PermissionMode {
598 Default,
599 AcceptEdits,
600 BypassPermissions,
601 Plan,
602}
603
604#[derive(Debug, Clone, Serialize, Deserialize)]
614pub struct ControlRequest {
615 pub request_id: String,
617 pub request: ControlRequestPayload,
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize)]
623#[serde(tag = "subtype", rename_all = "snake_case")]
624pub enum ControlRequestPayload {
625 CanUseTool(ToolPermissionRequest),
627 HookCallback(HookCallbackRequest),
629 McpMessage(McpMessageRequest),
631 Initialize(InitializeRequest),
633}
634
635#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
652pub struct Permission {
653 #[serde(rename = "type")]
655 pub permission_type: String,
656 pub destination: String,
658 #[serde(skip_serializing_if = "Option::is_none")]
660 pub mode: Option<String>,
661 #[serde(skip_serializing_if = "Option::is_none")]
663 pub behavior: Option<String>,
664 #[serde(skip_serializing_if = "Option::is_none")]
666 pub rules: Option<Vec<PermissionRule>>,
667}
668
669#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
671pub struct PermissionRule {
672 #[serde(rename = "toolName")]
674 pub tool_name: String,
675 #[serde(rename = "ruleContent")]
677 pub rule_content: String,
678}
679
680impl Permission {
681 pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
694 Permission {
695 permission_type: "addRules".to_string(),
696 destination: "session".to_string(),
697 mode: None,
698 behavior: Some("allow".to_string()),
699 rules: Some(vec![PermissionRule {
700 tool_name: tool_name.into(),
701 rule_content: rule_content.into(),
702 }]),
703 }
704 }
705
706 pub fn allow_tool_with_destination(
716 tool_name: impl Into<String>,
717 rule_content: impl Into<String>,
718 destination: impl Into<String>,
719 ) -> Self {
720 Permission {
721 permission_type: "addRules".to_string(),
722 destination: destination.into(),
723 mode: None,
724 behavior: Some("allow".to_string()),
725 rules: Some(vec![PermissionRule {
726 tool_name: tool_name.into(),
727 rule_content: rule_content.into(),
728 }]),
729 }
730 }
731
732 pub fn set_mode(mode: impl Into<String>, destination: impl Into<String>) -> Self {
742 Permission {
743 permission_type: "setMode".to_string(),
744 destination: destination.into(),
745 mode: Some(mode.into()),
746 behavior: None,
747 rules: None,
748 }
749 }
750
751 pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
770 Permission {
771 permission_type: suggestion.suggestion_type.clone(),
772 destination: suggestion.destination.clone(),
773 mode: suggestion.mode.clone(),
774 behavior: suggestion.behavior.clone(),
775 rules: suggestion.rules.as_ref().map(|rules| {
776 rules
777 .iter()
778 .filter_map(|v| {
779 Some(PermissionRule {
780 tool_name: v.get("toolName")?.as_str()?.to_string(),
781 rule_content: v.get("ruleContent")?.as_str()?.to_string(),
782 })
783 })
784 .collect()
785 }),
786 }
787 }
788}
789
790#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
801pub struct PermissionSuggestion {
802 #[serde(rename = "type")]
804 pub suggestion_type: String,
805 pub destination: String,
807 #[serde(skip_serializing_if = "Option::is_none")]
809 pub mode: Option<String>,
810 #[serde(skip_serializing_if = "Option::is_none")]
812 pub behavior: Option<String>,
813 #[serde(skip_serializing_if = "Option::is_none")]
815 pub rules: Option<Vec<Value>>,
816}
817
818#[derive(Debug, Clone, Serialize, Deserialize)]
844pub struct ToolPermissionRequest {
845 pub tool_name: String,
847 pub input: Value,
849 #[serde(default)]
851 pub permission_suggestions: Vec<PermissionSuggestion>,
852 #[serde(skip_serializing_if = "Option::is_none")]
854 pub blocked_path: Option<String>,
855 #[serde(skip_serializing_if = "Option::is_none")]
857 pub decision_reason: Option<String>,
858 #[serde(skip_serializing_if = "Option::is_none")]
860 pub tool_use_id: Option<String>,
861}
862
863impl ToolPermissionRequest {
864 pub fn allow(&self, request_id: &str) -> ControlResponse {
881 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
882 }
883
884 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
906 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
907 }
908
909 pub fn allow_with_permissions(
913 &self,
914 modified_input: Value,
915 permissions: Vec<Value>,
916 request_id: &str,
917 ) -> ControlResponse {
918 ControlResponse::from_result(
919 request_id,
920 PermissionResult::allow_with_permissions(modified_input, permissions),
921 )
922 }
923
924 pub fn allow_and_remember(
950 &self,
951 permissions: Vec<Permission>,
952 request_id: &str,
953 ) -> ControlResponse {
954 ControlResponse::from_result(
955 request_id,
956 PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
957 )
958 }
959
960 pub fn allow_with_and_remember(
964 &self,
965 modified_input: Value,
966 permissions: Vec<Permission>,
967 request_id: &str,
968 ) -> ControlResponse {
969 ControlResponse::from_result(
970 request_id,
971 PermissionResult::allow_with_typed_permissions(modified_input, permissions),
972 )
973 }
974
975 pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
1001 self.permission_suggestions.first().map(|suggestion| {
1002 let perm = Permission::from_suggestion(suggestion);
1003 self.allow_and_remember(vec![perm], request_id)
1004 })
1005 }
1006
1007 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
1026 ControlResponse::from_result(request_id, PermissionResult::deny(message))
1027 }
1028
1029 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
1033 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
1034 }
1035}
1036
1037#[derive(Debug, Clone, Serialize, Deserialize)]
1042#[serde(tag = "behavior", rename_all = "snake_case")]
1043pub enum PermissionResult {
1044 Allow {
1046 #[serde(rename = "updatedInput")]
1048 updated_input: Value,
1049 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
1051 updated_permissions: Option<Vec<Value>>,
1052 },
1053 Deny {
1055 message: String,
1057 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1059 interrupt: bool,
1060 },
1061}
1062
1063impl PermissionResult {
1064 pub fn allow(input: Value) -> Self {
1066 PermissionResult::Allow {
1067 updated_input: input,
1068 updated_permissions: None,
1069 }
1070 }
1071
1072 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
1076 PermissionResult::Allow {
1077 updated_input: input,
1078 updated_permissions: Some(permissions),
1079 }
1080 }
1081
1082 pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
1098 let permission_values: Vec<Value> = permissions
1099 .into_iter()
1100 .filter_map(|p| serde_json::to_value(p).ok())
1101 .collect();
1102 PermissionResult::Allow {
1103 updated_input: input,
1104 updated_permissions: Some(permission_values),
1105 }
1106 }
1107
1108 pub fn deny(message: impl Into<String>) -> Self {
1110 PermissionResult::Deny {
1111 message: message.into(),
1112 interrupt: false,
1113 }
1114 }
1115
1116 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
1118 PermissionResult::Deny {
1119 message: message.into(),
1120 interrupt: true,
1121 }
1122 }
1123}
1124
1125#[derive(Debug, Clone, Serialize, Deserialize)]
1127pub struct HookCallbackRequest {
1128 pub callback_id: String,
1129 pub input: Value,
1130 #[serde(skip_serializing_if = "Option::is_none")]
1131 pub tool_use_id: Option<String>,
1132}
1133
1134#[derive(Debug, Clone, Serialize, Deserialize)]
1136pub struct McpMessageRequest {
1137 pub server_name: String,
1138 pub message: Value,
1139}
1140
1141#[derive(Debug, Clone, Serialize, Deserialize)]
1143pub struct InitializeRequest {
1144 #[serde(skip_serializing_if = "Option::is_none")]
1145 pub hooks: Option<Value>,
1146}
1147
1148#[derive(Debug, Clone, Serialize, Deserialize)]
1153pub struct ControlResponse {
1154 pub response: ControlResponsePayload,
1156}
1157
1158impl ControlResponse {
1159 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
1163 let response_value = serde_json::to_value(&result)
1165 .expect("PermissionResult serialization should never fail");
1166 ControlResponse {
1167 response: ControlResponsePayload::Success {
1168 request_id: request_id.to_string(),
1169 response: Some(response_value),
1170 },
1171 }
1172 }
1173
1174 pub fn success(request_id: &str, response_data: Value) -> Self {
1176 ControlResponse {
1177 response: ControlResponsePayload::Success {
1178 request_id: request_id.to_string(),
1179 response: Some(response_data),
1180 },
1181 }
1182 }
1183
1184 pub fn success_empty(request_id: &str) -> Self {
1186 ControlResponse {
1187 response: ControlResponsePayload::Success {
1188 request_id: request_id.to_string(),
1189 response: None,
1190 },
1191 }
1192 }
1193
1194 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
1196 ControlResponse {
1197 response: ControlResponsePayload::Error {
1198 request_id: request_id.to_string(),
1199 error: error_message.into(),
1200 },
1201 }
1202 }
1203}
1204
1205#[derive(Debug, Clone, Serialize, Deserialize)]
1207#[serde(tag = "subtype", rename_all = "snake_case")]
1208pub enum ControlResponsePayload {
1209 Success {
1210 request_id: String,
1211 #[serde(skip_serializing_if = "Option::is_none")]
1212 response: Option<Value>,
1213 },
1214 Error {
1215 request_id: String,
1216 error: String,
1217 },
1218}
1219
1220#[derive(Debug, Clone, Serialize, Deserialize)]
1222pub struct ControlResponseMessage {
1223 #[serde(rename = "type")]
1224 pub message_type: String,
1225 pub response: ControlResponsePayload,
1226}
1227
1228impl From<ControlResponse> for ControlResponseMessage {
1229 fn from(resp: ControlResponse) -> Self {
1230 ControlResponseMessage {
1231 message_type: "control_response".to_string(),
1232 response: resp.response,
1233 }
1234 }
1235}
1236
1237#[derive(Debug, Clone, Serialize, Deserialize)]
1239pub struct ControlRequestMessage {
1240 #[serde(rename = "type")]
1241 pub message_type: String,
1242 pub request_id: String,
1243 pub request: ControlRequestPayload,
1244}
1245
1246impl ControlRequestMessage {
1247 pub fn initialize(request_id: impl Into<String>) -> Self {
1249 ControlRequestMessage {
1250 message_type: "control_request".to_string(),
1251 request_id: request_id.into(),
1252 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
1253 }
1254 }
1255
1256 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
1258 ControlRequestMessage {
1259 message_type: "control_request".to_string(),
1260 request_id: request_id.into(),
1261 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
1262 }
1263 }
1264}
1265
1266#[derive(Debug, Clone, Serialize, Deserialize)]
1268pub struct UsageInfo {
1269 pub input_tokens: u32,
1270 pub cache_creation_input_tokens: u32,
1271 pub cache_read_input_tokens: u32,
1272 pub output_tokens: u32,
1273 pub server_tool_use: ServerToolUse,
1274 pub service_tier: String,
1275}
1276
1277#[derive(Debug, Clone, Serialize, Deserialize)]
1279pub struct ServerToolUse {
1280 pub web_search_requests: u32,
1281}
1282
1283impl ClaudeInput {
1284 pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
1286 ClaudeInput::User(UserMessage {
1287 message: MessageContent {
1288 role: "user".to_string(),
1289 content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
1290 },
1291 session_id: Some(session_id),
1292 })
1293 }
1294
1295 pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
1297 ClaudeInput::User(UserMessage {
1298 message: MessageContent {
1299 role: "user".to_string(),
1300 content: blocks,
1301 },
1302 session_id: Some(session_id),
1303 })
1304 }
1305
1306 pub fn user_message_with_image(
1309 image_data: String,
1310 media_type: String,
1311 text: Option<String>,
1312 session_id: Uuid,
1313 ) -> Result<Self, String> {
1314 let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
1316
1317 if !valid_types.contains(&media_type.as_str()) {
1318 return Err(format!(
1319 "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
1320 media_type
1321 ));
1322 }
1323
1324 let mut blocks = vec![ContentBlock::Image(ImageBlock {
1325 source: ImageSource {
1326 source_type: "base64".to_string(),
1327 media_type,
1328 data: image_data,
1329 },
1330 })];
1331
1332 if let Some(text_content) = text {
1333 blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
1334 }
1335
1336 Ok(Self::user_message_blocks(blocks, session_id))
1337 }
1338}
1339
1340impl ClaudeOutput {
1341 pub fn message_type(&self) -> String {
1343 match self {
1344 ClaudeOutput::System(_) => "system".to_string(),
1345 ClaudeOutput::User(_) => "user".to_string(),
1346 ClaudeOutput::Assistant(_) => "assistant".to_string(),
1347 ClaudeOutput::Result(_) => "result".to_string(),
1348 ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
1349 ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
1350 ClaudeOutput::Error(_) => "error".to_string(),
1351 }
1352 }
1353
1354 pub fn is_control_request(&self) -> bool {
1356 matches!(self, ClaudeOutput::ControlRequest(_))
1357 }
1358
1359 pub fn is_control_response(&self) -> bool {
1361 matches!(self, ClaudeOutput::ControlResponse(_))
1362 }
1363
1364 pub fn is_api_error(&self) -> bool {
1366 matches!(self, ClaudeOutput::Error(_))
1367 }
1368
1369 pub fn as_control_request(&self) -> Option<&ControlRequest> {
1371 match self {
1372 ClaudeOutput::ControlRequest(req) => Some(req),
1373 _ => None,
1374 }
1375 }
1376
1377 pub fn as_anthropic_error(&self) -> Option<&AnthropicError> {
1393 match self {
1394 ClaudeOutput::Error(err) => Some(err),
1395 _ => None,
1396 }
1397 }
1398
1399 pub fn is_error(&self) -> bool {
1401 matches!(self, ClaudeOutput::Result(r) if r.is_error)
1402 }
1403
1404 pub fn is_assistant_message(&self) -> bool {
1406 matches!(self, ClaudeOutput::Assistant(_))
1407 }
1408
1409 pub fn is_system_message(&self) -> bool {
1411 matches!(self, ClaudeOutput::System(_))
1412 }
1413
1414 pub fn is_system_init(&self) -> bool {
1425 matches!(self, ClaudeOutput::System(sys) if sys.is_init())
1426 }
1427
1428 pub fn session_id(&self) -> Option<&str> {
1444 match self {
1445 ClaudeOutput::System(sys) => sys.data.get("session_id").and_then(|v| v.as_str()),
1446 ClaudeOutput::Assistant(ass) => Some(&ass.session_id),
1447 ClaudeOutput::Result(res) => Some(&res.session_id),
1448 ClaudeOutput::User(_) => None,
1449 ClaudeOutput::ControlRequest(_) => None,
1450 ClaudeOutput::ControlResponse(_) => None,
1451 ClaudeOutput::Error(_) => None,
1452 }
1453 }
1454
1455 pub fn as_tool_use(&self, tool_name: &str) -> Option<&ToolUseBlock> {
1474 match self {
1475 ClaudeOutput::Assistant(ass) => {
1476 ass.message.content.iter().find_map(|block| match block {
1477 ContentBlock::ToolUse(tu) if tu.name == tool_name => Some(tu),
1478 _ => None,
1479 })
1480 }
1481 _ => None,
1482 }
1483 }
1484
1485 pub fn tool_uses(&self) -> impl Iterator<Item = &ToolUseBlock> {
1505 let content = match self {
1506 ClaudeOutput::Assistant(ass) => Some(&ass.message.content),
1507 _ => None,
1508 };
1509
1510 content
1511 .into_iter()
1512 .flat_map(|c| c.iter())
1513 .filter_map(|block| match block {
1514 ContentBlock::ToolUse(tu) => Some(tu),
1515 _ => None,
1516 })
1517 }
1518
1519 pub fn text_content(&self) -> Option<String> {
1535 match self {
1536 ClaudeOutput::Assistant(ass) => {
1537 let texts: Vec<&str> = ass
1538 .message
1539 .content
1540 .iter()
1541 .filter_map(|block| match block {
1542 ContentBlock::Text(t) => Some(t.text.as_str()),
1543 _ => None,
1544 })
1545 .collect();
1546
1547 if texts.is_empty() {
1548 None
1549 } else {
1550 Some(texts.join(""))
1551 }
1552 }
1553 _ => None,
1554 }
1555 }
1556
1557 pub fn as_assistant(&self) -> Option<&AssistantMessage> {
1572 match self {
1573 ClaudeOutput::Assistant(ass) => Some(ass),
1574 _ => None,
1575 }
1576 }
1577
1578 pub fn as_result(&self) -> Option<&ResultMessage> {
1594 match self {
1595 ClaudeOutput::Result(res) => Some(res),
1596 _ => None,
1597 }
1598 }
1599
1600 pub fn as_system(&self) -> Option<&SystemMessage> {
1602 match self {
1603 ClaudeOutput::System(sys) => Some(sys),
1604 _ => None,
1605 }
1606 }
1607
1608 pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
1613 match Self::parse_json(s) {
1615 Ok(output) => Ok(output),
1616 Err(first_error) => {
1617 if let Some(json_start) = s.find('{') {
1619 let trimmed = &s[json_start..];
1620 match Self::parse_json(trimmed) {
1621 Ok(output) => Ok(output),
1622 Err(_) => {
1623 Err(first_error)
1625 }
1626 }
1627 } else {
1628 Err(first_error)
1629 }
1630 }
1631 }
1632 }
1633
1634 pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
1636 let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
1638 raw_json: Value::String(s.to_string()),
1639 error_message: format!("Invalid JSON: {}", e),
1640 })?;
1641
1642 serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
1644 raw_json: value,
1645 error_message: e.to_string(),
1646 })
1647 }
1648}
1649
1650#[cfg(test)]
1651mod tests {
1652 use super::*;
1653
1654 #[test]
1655 fn test_serialize_user_message() {
1656 let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
1657 let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
1658 let json = serde_json::to_string(&input).unwrap();
1659 assert!(json.contains("\"type\":\"user\""));
1660 assert!(json.contains("\"role\":\"user\""));
1661 assert!(json.contains("\"text\":\"Hello, Claude!\""));
1662 assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
1663 }
1664
1665 #[test]
1666 fn test_deserialize_assistant_message() {
1667 let json = r#"{
1668 "type": "assistant",
1669 "message": {
1670 "id": "msg_123",
1671 "role": "assistant",
1672 "model": "claude-3-sonnet",
1673 "content": [{"type": "text", "text": "Hello! How can I help you?"}]
1674 },
1675 "session_id": "123"
1676 }"#;
1677
1678 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1679 assert!(output.is_assistant_message());
1680 }
1681
1682 #[test]
1683 fn test_deserialize_result_message() {
1684 let json = r#"{
1685 "type": "result",
1686 "subtype": "success",
1687 "is_error": false,
1688 "duration_ms": 100,
1689 "duration_api_ms": 200,
1690 "num_turns": 1,
1691 "result": "Done",
1692 "session_id": "123",
1693 "total_cost_usd": 0.01,
1694 "permission_denials": []
1695 }"#;
1696
1697 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1698 assert!(!output.is_error());
1699 }
1700
1701 #[test]
1702 fn test_deserialize_result_with_permission_denials() {
1703 let json = r#"{
1704 "type": "result",
1705 "subtype": "success",
1706 "is_error": false,
1707 "duration_ms": 100,
1708 "duration_api_ms": 200,
1709 "num_turns": 2,
1710 "result": "Done",
1711 "session_id": "123",
1712 "total_cost_usd": 0.01,
1713 "permission_denials": [
1714 {
1715 "tool_name": "Bash",
1716 "tool_input": {"command": "rm -rf /", "description": "Delete everything"},
1717 "tool_use_id": "toolu_123"
1718 }
1719 ]
1720 }"#;
1721
1722 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1723 if let ClaudeOutput::Result(result) = output {
1724 assert_eq!(result.permission_denials.len(), 1);
1725 assert_eq!(result.permission_denials[0].tool_name, "Bash");
1726 assert_eq!(result.permission_denials[0].tool_use_id, "toolu_123");
1727 assert_eq!(
1728 result.permission_denials[0]
1729 .tool_input
1730 .get("command")
1731 .unwrap(),
1732 "rm -rf /"
1733 );
1734 } else {
1735 panic!("Expected Result");
1736 }
1737 }
1738
1739 #[test]
1740 fn test_permission_denial_roundtrip() {
1741 let denial = PermissionDenial {
1742 tool_name: "Write".to_string(),
1743 tool_input: serde_json::json!({"file_path": "/etc/passwd", "content": "bad"}),
1744 tool_use_id: "toolu_456".to_string(),
1745 };
1746
1747 let json = serde_json::to_string(&denial).unwrap();
1748 assert!(json.contains("\"tool_name\":\"Write\""));
1749 assert!(json.contains("\"tool_use_id\":\"toolu_456\""));
1750 assert!(json.contains("/etc/passwd"));
1751
1752 let parsed: PermissionDenial = serde_json::from_str(&json).unwrap();
1753 assert_eq!(parsed, denial);
1754 }
1755
1756 #[test]
1761 fn test_deserialize_control_request_can_use_tool() {
1762 let json = r#"{
1763 "type": "control_request",
1764 "request_id": "perm-abc123",
1765 "request": {
1766 "subtype": "can_use_tool",
1767 "tool_name": "Write",
1768 "input": {
1769 "file_path": "/home/user/hello.py",
1770 "content": "print('hello')"
1771 },
1772 "permission_suggestions": [],
1773 "blocked_path": null
1774 }
1775 }"#;
1776
1777 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1778 assert!(output.is_control_request());
1779
1780 if let ClaudeOutput::ControlRequest(req) = output {
1781 assert_eq!(req.request_id, "perm-abc123");
1782 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1783 assert_eq!(perm_req.tool_name, "Write");
1784 assert_eq!(
1785 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1786 "/home/user/hello.py"
1787 );
1788 } else {
1789 panic!("Expected CanUseTool payload");
1790 }
1791 } else {
1792 panic!("Expected ControlRequest");
1793 }
1794 }
1795
1796 #[test]
1797 fn test_deserialize_control_request_edit_tool_real() {
1798 let json = r#"{"type":"control_request","request_id":"f3cf357c-17d6-4eca-b498-dd17c7ac43dd","request":{"subtype":"can_use_tool","tool_name":"Edit","input":{"file_path":"/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs","old_string":"/// Print hint to re-authenticate\npub fn print_reauth_hint() {\n println!(\n \" {} Run: {} to re-authenticate\",\n \"→\".bright_blue(),\n \"claude-portal logout && claude-portal login\".bright_cyan()\n );\n}","new_string":"/// Print hint to re-authenticate\npub fn print_reauth_hint() {\n println!(\n \" {} Run: {} to re-authenticate\",\n \"→\".bright_blue(),\n \"claude-portal --reauth\".bright_cyan()\n );\n}","replace_all":false},"permission_suggestions":[{"type":"setMode","mode":"acceptEdits","destination":"session"}],"tool_use_id":"toolu_015BDGtNiqNrRSJSDrWXNckW"}}"#;
1800
1801 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1802 assert!(output.is_control_request());
1803 assert_eq!(output.message_type(), "control_request");
1804
1805 if let ClaudeOutput::ControlRequest(req) = output {
1806 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1807 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1808 assert_eq!(perm_req.tool_name, "Edit");
1809 assert_eq!(
1811 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1812 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1813 );
1814 assert!(perm_req.input.get("old_string").is_some());
1815 assert!(perm_req.input.get("new_string").is_some());
1816 assert!(!perm_req
1817 .input
1818 .get("replace_all")
1819 .unwrap()
1820 .as_bool()
1821 .unwrap());
1822 } else {
1823 panic!("Expected CanUseTool payload");
1824 }
1825 } else {
1826 panic!("Expected ControlRequest");
1827 }
1828 }
1829
1830 #[test]
1831 fn test_tool_permission_request_allow() {
1832 let req = ToolPermissionRequest {
1833 tool_name: "Read".to_string(),
1834 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1835 permission_suggestions: vec![],
1836 blocked_path: None,
1837 decision_reason: None,
1838 tool_use_id: None,
1839 };
1840
1841 let response = req.allow("req-123");
1842 let message: ControlResponseMessage = response.into();
1843
1844 let json = serde_json::to_string(&message).unwrap();
1845 assert!(json.contains("\"type\":\"control_response\""));
1846 assert!(json.contains("\"subtype\":\"success\""));
1847 assert!(json.contains("\"request_id\":\"req-123\""));
1848 assert!(json.contains("\"behavior\":\"allow\""));
1849 assert!(json.contains("\"updatedInput\""));
1850 }
1851
1852 #[test]
1853 fn test_tool_permission_request_allow_with_modified_input() {
1854 let req = ToolPermissionRequest {
1855 tool_name: "Write".to_string(),
1856 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1857 permission_suggestions: vec![],
1858 blocked_path: None,
1859 decision_reason: None,
1860 tool_use_id: None,
1861 };
1862
1863 let modified_input = serde_json::json!({
1864 "file_path": "/tmp/safe/passwd",
1865 "content": "test"
1866 });
1867 let response = req.allow_with(modified_input, "req-456");
1868 let message: ControlResponseMessage = response.into();
1869
1870 let json = serde_json::to_string(&message).unwrap();
1871 assert!(json.contains("/tmp/safe/passwd"));
1872 assert!(!json.contains("/etc/passwd"));
1873 }
1874
1875 #[test]
1876 fn test_tool_permission_request_deny() {
1877 let req = ToolPermissionRequest {
1878 tool_name: "Bash".to_string(),
1879 input: serde_json::json!({"command": "sudo rm -rf /"}),
1880 permission_suggestions: vec![],
1881 blocked_path: None,
1882 decision_reason: None,
1883 tool_use_id: None,
1884 };
1885
1886 let response = req.deny("Dangerous command blocked", "req-789");
1887 let message: ControlResponseMessage = response.into();
1888
1889 let json = serde_json::to_string(&message).unwrap();
1890 assert!(json.contains("\"behavior\":\"deny\""));
1891 assert!(json.contains("Dangerous command blocked"));
1892 assert!(!json.contains("\"interrupt\":true"));
1893 }
1894
1895 #[test]
1896 fn test_tool_permission_request_deny_and_stop() {
1897 let req = ToolPermissionRequest {
1898 tool_name: "Bash".to_string(),
1899 input: serde_json::json!({"command": "rm -rf /"}),
1900 permission_suggestions: vec![],
1901 blocked_path: None,
1902 decision_reason: None,
1903 tool_use_id: None,
1904 };
1905
1906 let response = req.deny_and_stop("Security violation", "req-000");
1907 let message: ControlResponseMessage = response.into();
1908
1909 let json = serde_json::to_string(&message).unwrap();
1910 assert!(json.contains("\"behavior\":\"deny\""));
1911 assert!(json.contains("\"interrupt\":true"));
1912 }
1913
1914 #[test]
1915 fn test_permission_result_serialization() {
1916 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1918 let json = serde_json::to_string(&allow).unwrap();
1919 assert!(json.contains("\"behavior\":\"allow\""));
1920 assert!(json.contains("\"updatedInput\""));
1921
1922 let deny = PermissionResult::deny("Not allowed");
1924 let json = serde_json::to_string(&deny).unwrap();
1925 assert!(json.contains("\"behavior\":\"deny\""));
1926 assert!(json.contains("\"message\":\"Not allowed\""));
1927 assert!(!json.contains("\"interrupt\""));
1928
1929 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1931 let json = serde_json::to_string(&deny_stop).unwrap();
1932 assert!(json.contains("\"interrupt\":true"));
1933 }
1934
1935 #[test]
1936 fn test_control_request_message_initialize() {
1937 let init = ControlRequestMessage::initialize("init-1");
1938
1939 let json = serde_json::to_string(&init).unwrap();
1940 assert!(json.contains("\"type\":\"control_request\""));
1941 assert!(json.contains("\"request_id\":\"init-1\""));
1942 assert!(json.contains("\"subtype\":\"initialize\""));
1943 }
1944
1945 #[test]
1946 fn test_control_response_error() {
1947 let response = ControlResponse::error("req-err", "Something went wrong");
1948 let message: ControlResponseMessage = response.into();
1949
1950 let json = serde_json::to_string(&message).unwrap();
1951 assert!(json.contains("\"subtype\":\"error\""));
1952 assert!(json.contains("\"error\":\"Something went wrong\""));
1953 }
1954
1955 #[test]
1956 fn test_roundtrip_control_request() {
1957 let original_json = r#"{
1959 "type": "control_request",
1960 "request_id": "test-123",
1961 "request": {
1962 "subtype": "can_use_tool",
1963 "tool_name": "Bash",
1964 "input": {"command": "ls -la"},
1965 "permission_suggestions": []
1966 }
1967 }"#;
1968
1969 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1971
1972 let reserialized = serde_json::to_string(&output).unwrap();
1974 assert!(reserialized.contains("control_request"));
1975 assert!(reserialized.contains("test-123"));
1976 assert!(reserialized.contains("Bash"));
1977 }
1978
1979 #[test]
1980 fn test_permission_suggestions_parsing() {
1981 let json = r#"{
1983 "type": "control_request",
1984 "request_id": "perm-456",
1985 "request": {
1986 "subtype": "can_use_tool",
1987 "tool_name": "Bash",
1988 "input": {"command": "npm test"},
1989 "permission_suggestions": [
1990 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1991 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1992 ]
1993 }
1994 }"#;
1995
1996 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1997 if let ClaudeOutput::ControlRequest(req) = output {
1998 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1999 assert_eq!(perm_req.permission_suggestions.len(), 2);
2000 assert_eq!(
2001 perm_req.permission_suggestions[0].suggestion_type,
2002 "setMode"
2003 );
2004 assert_eq!(
2005 perm_req.permission_suggestions[0].mode,
2006 Some("acceptEdits".to_string())
2007 );
2008 assert_eq!(perm_req.permission_suggestions[0].destination, "session");
2009 assert_eq!(
2010 perm_req.permission_suggestions[1].suggestion_type,
2011 "setMode"
2012 );
2013 assert_eq!(
2014 perm_req.permission_suggestions[1].mode,
2015 Some("bypassPermissions".to_string())
2016 );
2017 assert_eq!(perm_req.permission_suggestions[1].destination, "project");
2018 } else {
2019 panic!("Expected CanUseTool payload");
2020 }
2021 } else {
2022 panic!("Expected ControlRequest");
2023 }
2024 }
2025
2026 #[test]
2027 fn test_permission_suggestion_set_mode_roundtrip() {
2028 let suggestion = PermissionSuggestion {
2029 suggestion_type: "setMode".to_string(),
2030 destination: "session".to_string(),
2031 mode: Some("acceptEdits".to_string()),
2032 behavior: None,
2033 rules: None,
2034 };
2035
2036 let json = serde_json::to_string(&suggestion).unwrap();
2037 assert!(json.contains("\"type\":\"setMode\""));
2038 assert!(json.contains("\"mode\":\"acceptEdits\""));
2039 assert!(json.contains("\"destination\":\"session\""));
2040 assert!(!json.contains("\"behavior\""));
2041 assert!(!json.contains("\"rules\""));
2042
2043 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
2044 assert_eq!(parsed, suggestion);
2045 }
2046
2047 #[test]
2048 fn test_permission_suggestion_add_rules_roundtrip() {
2049 let suggestion = PermissionSuggestion {
2050 suggestion_type: "addRules".to_string(),
2051 destination: "session".to_string(),
2052 mode: None,
2053 behavior: Some("allow".to_string()),
2054 rules: Some(vec![serde_json::json!({
2055 "toolName": "Read",
2056 "ruleContent": "//tmp/**"
2057 })]),
2058 };
2059
2060 let json = serde_json::to_string(&suggestion).unwrap();
2061 assert!(json.contains("\"type\":\"addRules\""));
2062 assert!(json.contains("\"behavior\":\"allow\""));
2063 assert!(json.contains("\"destination\":\"session\""));
2064 assert!(json.contains("\"rules\""));
2065 assert!(json.contains("\"toolName\":\"Read\""));
2066 assert!(!json.contains("\"mode\""));
2067
2068 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
2069 assert_eq!(parsed, suggestion);
2070 }
2071
2072 #[test]
2073 fn test_permission_suggestion_add_rules_from_real_json() {
2074 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
2076
2077 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
2078 assert_eq!(parsed.suggestion_type, "addRules");
2079 assert_eq!(parsed.destination, "session");
2080 assert_eq!(parsed.behavior, Some("allow".to_string()));
2081 assert!(parsed.rules.is_some());
2082 assert!(parsed.mode.is_none());
2083 }
2084
2085 #[test]
2090 fn test_system_message_init() {
2091 let json = r#"{
2092 "type": "system",
2093 "subtype": "init",
2094 "session_id": "test-session-123",
2095 "cwd": "/home/user/project",
2096 "model": "claude-sonnet-4",
2097 "tools": ["Bash", "Read", "Write"],
2098 "mcp_servers": []
2099 }"#;
2100
2101 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2102 if let ClaudeOutput::System(sys) = output {
2103 assert!(sys.is_init());
2104 assert!(!sys.is_status());
2105 assert!(!sys.is_compact_boundary());
2106
2107 let init = sys.as_init().expect("Should parse as init");
2108 assert_eq!(init.session_id, "test-session-123");
2109 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
2110 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
2111 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
2112 } else {
2113 panic!("Expected System message");
2114 }
2115 }
2116
2117 #[test]
2118 fn test_system_message_status() {
2119 let json = r#"{
2120 "type": "system",
2121 "subtype": "status",
2122 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2123 "status": "compacting",
2124 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
2125 }"#;
2126
2127 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2128 if let ClaudeOutput::System(sys) = output {
2129 assert!(sys.is_status());
2130 assert!(!sys.is_init());
2131
2132 let status = sys.as_status().expect("Should parse as status");
2133 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2134 assert_eq!(status.status, Some("compacting".to_string()));
2135 assert_eq!(
2136 status.uuid,
2137 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
2138 );
2139 } else {
2140 panic!("Expected System message");
2141 }
2142 }
2143
2144 #[test]
2145 fn test_system_message_status_null() {
2146 let json = r#"{
2147 "type": "system",
2148 "subtype": "status",
2149 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2150 "status": null,
2151 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
2152 }"#;
2153
2154 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2155 if let ClaudeOutput::System(sys) = output {
2156 let status = sys.as_status().expect("Should parse as status");
2157 assert_eq!(status.status, None);
2158 } else {
2159 panic!("Expected System message");
2160 }
2161 }
2162
2163 #[test]
2164 fn test_system_message_compact_boundary() {
2165 let json = r#"{
2166 "type": "system",
2167 "subtype": "compact_boundary",
2168 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
2169 "compact_metadata": {
2170 "pre_tokens": 155285,
2171 "trigger": "auto"
2172 },
2173 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
2174 }"#;
2175
2176 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2177 if let ClaudeOutput::System(sys) = output {
2178 assert!(sys.is_compact_boundary());
2179 assert!(!sys.is_init());
2180 assert!(!sys.is_status());
2181
2182 let compact = sys
2183 .as_compact_boundary()
2184 .expect("Should parse as compact_boundary");
2185 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
2186 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
2187 assert_eq!(compact.compact_metadata.trigger, "auto");
2188 } else {
2189 panic!("Expected System message");
2190 }
2191 }
2192
2193 #[test]
2198 fn test_is_system_init() {
2199 let init_json = r#"{
2200 "type": "system",
2201 "subtype": "init",
2202 "session_id": "test-session"
2203 }"#;
2204 let output: ClaudeOutput = serde_json::from_str(init_json).unwrap();
2205 assert!(output.is_system_init());
2206
2207 let status_json = r#"{
2208 "type": "system",
2209 "subtype": "status",
2210 "session_id": "test-session"
2211 }"#;
2212 let output: ClaudeOutput = serde_json::from_str(status_json).unwrap();
2213 assert!(!output.is_system_init());
2214 }
2215
2216 #[test]
2217 fn test_session_id() {
2218 let result_json = r#"{
2220 "type": "result",
2221 "subtype": "success",
2222 "is_error": false,
2223 "duration_ms": 100,
2224 "duration_api_ms": 200,
2225 "num_turns": 1,
2226 "session_id": "result-session",
2227 "total_cost_usd": 0.01
2228 }"#;
2229 let output: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2230 assert_eq!(output.session_id(), Some("result-session"));
2231
2232 let assistant_json = r#"{
2234 "type": "assistant",
2235 "message": {
2236 "id": "msg_1",
2237 "role": "assistant",
2238 "model": "claude-3",
2239 "content": []
2240 },
2241 "session_id": "assistant-session"
2242 }"#;
2243 let output: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
2244 assert_eq!(output.session_id(), Some("assistant-session"));
2245
2246 let system_json = r#"{
2248 "type": "system",
2249 "subtype": "init",
2250 "session_id": "system-session"
2251 }"#;
2252 let output: ClaudeOutput = serde_json::from_str(system_json).unwrap();
2253 assert_eq!(output.session_id(), Some("system-session"));
2254 }
2255
2256 #[test]
2257 fn test_as_tool_use() {
2258 let json = r#"{
2259 "type": "assistant",
2260 "message": {
2261 "id": "msg_1",
2262 "role": "assistant",
2263 "model": "claude-3",
2264 "content": [
2265 {"type": "text", "text": "Let me run that command."},
2266 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls -la"}},
2267 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/test"}}
2268 ]
2269 },
2270 "session_id": "abc"
2271 }"#;
2272 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2273
2274 let bash = output.as_tool_use("Bash");
2276 assert!(bash.is_some());
2277 assert_eq!(bash.unwrap().id, "tu_1");
2278
2279 let read = output.as_tool_use("Read");
2281 assert!(read.is_some());
2282 assert_eq!(read.unwrap().id, "tu_2");
2283
2284 assert!(output.as_tool_use("Write").is_none());
2286
2287 let result_json = r#"{
2289 "type": "result",
2290 "subtype": "success",
2291 "is_error": false,
2292 "duration_ms": 100,
2293 "duration_api_ms": 200,
2294 "num_turns": 1,
2295 "session_id": "abc",
2296 "total_cost_usd": 0.01
2297 }"#;
2298 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2299 assert!(result.as_tool_use("Bash").is_none());
2300 }
2301
2302 #[test]
2303 fn test_tool_uses() {
2304 let json = r#"{
2305 "type": "assistant",
2306 "message": {
2307 "id": "msg_1",
2308 "role": "assistant",
2309 "model": "claude-3",
2310 "content": [
2311 {"type": "text", "text": "Running commands..."},
2312 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}},
2313 {"type": "tool_use", "id": "tu_2", "name": "Read", "input": {"file_path": "/tmp/a"}},
2314 {"type": "tool_use", "id": "tu_3", "name": "Write", "input": {"file_path": "/tmp/b", "content": "x"}}
2315 ]
2316 },
2317 "session_id": "abc"
2318 }"#;
2319 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2320
2321 let tools: Vec<_> = output.tool_uses().collect();
2322 assert_eq!(tools.len(), 3);
2323 assert_eq!(tools[0].name, "Bash");
2324 assert_eq!(tools[1].name, "Read");
2325 assert_eq!(tools[2].name, "Write");
2326 }
2327
2328 #[test]
2329 fn test_text_content() {
2330 let json = r#"{
2332 "type": "assistant",
2333 "message": {
2334 "id": "msg_1",
2335 "role": "assistant",
2336 "model": "claude-3",
2337 "content": [{"type": "text", "text": "Hello, world!"}]
2338 },
2339 "session_id": "abc"
2340 }"#;
2341 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2342 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
2343
2344 let json = r#"{
2346 "type": "assistant",
2347 "message": {
2348 "id": "msg_1",
2349 "role": "assistant",
2350 "model": "claude-3",
2351 "content": [
2352 {"type": "text", "text": "Hello, "},
2353 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}},
2354 {"type": "text", "text": "world!"}
2355 ]
2356 },
2357 "session_id": "abc"
2358 }"#;
2359 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2360 assert_eq!(output.text_content(), Some("Hello, world!".to_string()));
2361
2362 let json = r#"{
2364 "type": "assistant",
2365 "message": {
2366 "id": "msg_1",
2367 "role": "assistant",
2368 "model": "claude-3",
2369 "content": [{"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {}}]
2370 },
2371 "session_id": "abc"
2372 }"#;
2373 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2374 assert_eq!(output.text_content(), None);
2375
2376 let json = r#"{
2378 "type": "result",
2379 "subtype": "success",
2380 "is_error": false,
2381 "duration_ms": 100,
2382 "duration_api_ms": 200,
2383 "num_turns": 1,
2384 "session_id": "abc",
2385 "total_cost_usd": 0.01
2386 }"#;
2387 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2388 assert_eq!(output.text_content(), None);
2389 }
2390
2391 #[test]
2392 fn test_as_assistant() {
2393 let json = r#"{
2394 "type": "assistant",
2395 "message": {
2396 "id": "msg_1",
2397 "role": "assistant",
2398 "model": "claude-sonnet-4",
2399 "content": []
2400 },
2401 "session_id": "abc"
2402 }"#;
2403 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2404
2405 let assistant = output.as_assistant();
2406 assert!(assistant.is_some());
2407 assert_eq!(assistant.unwrap().message.model, "claude-sonnet-4");
2408
2409 let result_json = r#"{
2411 "type": "result",
2412 "subtype": "success",
2413 "is_error": false,
2414 "duration_ms": 100,
2415 "duration_api_ms": 200,
2416 "num_turns": 1,
2417 "session_id": "abc",
2418 "total_cost_usd": 0.01
2419 }"#;
2420 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2421 assert!(result.as_assistant().is_none());
2422 }
2423
2424 #[test]
2425 fn test_as_result() {
2426 let json = r#"{
2427 "type": "result",
2428 "subtype": "success",
2429 "is_error": false,
2430 "duration_ms": 100,
2431 "duration_api_ms": 200,
2432 "num_turns": 5,
2433 "session_id": "abc",
2434 "total_cost_usd": 0.05
2435 }"#;
2436 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2437
2438 let result = output.as_result();
2439 assert!(result.is_some());
2440 assert_eq!(result.unwrap().num_turns, 5);
2441 assert_eq!(result.unwrap().total_cost_usd, 0.05);
2442
2443 let assistant_json = r#"{
2445 "type": "assistant",
2446 "message": {
2447 "id": "msg_1",
2448 "role": "assistant",
2449 "model": "claude-3",
2450 "content": []
2451 },
2452 "session_id": "abc"
2453 }"#;
2454 let assistant: ClaudeOutput = serde_json::from_str(assistant_json).unwrap();
2455 assert!(assistant.as_result().is_none());
2456 }
2457
2458 #[test]
2459 fn test_as_system() {
2460 let json = r#"{
2461 "type": "system",
2462 "subtype": "init",
2463 "session_id": "abc",
2464 "model": "claude-3"
2465 }"#;
2466 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2467
2468 let system = output.as_system();
2469 assert!(system.is_some());
2470 assert!(system.unwrap().is_init());
2471
2472 let result_json = r#"{
2474 "type": "result",
2475 "subtype": "success",
2476 "is_error": false,
2477 "duration_ms": 100,
2478 "duration_api_ms": 200,
2479 "num_turns": 1,
2480 "session_id": "abc",
2481 "total_cost_usd": 0.01
2482 }"#;
2483 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2484 assert!(result.as_system().is_none());
2485 }
2486
2487 #[test]
2492 fn test_deserialize_result_message_with_errors() {
2493 let json = r#"{
2494 "type": "result",
2495 "subtype": "error_during_execution",
2496 "duration_ms": 0,
2497 "duration_api_ms": 0,
2498 "is_error": true,
2499 "num_turns": 0,
2500 "session_id": "27934753-425a-4182-892c-6b1c15050c3f",
2501 "total_cost_usd": 0,
2502 "errors": ["No conversation found with session ID: d56965c9-c855-4042-a8f5-f12bbb14d6f6"],
2503 "permission_denials": []
2504 }"#;
2505
2506 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2507 assert!(output.is_error());
2508
2509 if let ClaudeOutput::Result(res) = output {
2510 assert!(res.is_error);
2511 assert_eq!(res.errors.len(), 1);
2512 assert!(res.errors[0].contains("No conversation found"));
2513 } else {
2514 panic!("Expected Result message");
2515 }
2516 }
2517
2518 #[test]
2519 fn test_deserialize_result_message_errors_defaults_empty() {
2520 let json = r#"{
2522 "type": "result",
2523 "subtype": "success",
2524 "is_error": false,
2525 "duration_ms": 100,
2526 "duration_api_ms": 200,
2527 "num_turns": 1,
2528 "session_id": "123",
2529 "total_cost_usd": 0.01
2530 }"#;
2531
2532 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2533 if let ClaudeOutput::Result(res) = output {
2534 assert!(res.errors.is_empty());
2535 } else {
2536 panic!("Expected Result message");
2537 }
2538 }
2539
2540 #[test]
2541 fn test_result_message_errors_roundtrip() {
2542 let json = r#"{
2543 "type": "result",
2544 "subtype": "error_during_execution",
2545 "is_error": true,
2546 "duration_ms": 0,
2547 "duration_api_ms": 0,
2548 "num_turns": 0,
2549 "session_id": "test-session",
2550 "total_cost_usd": 0.0,
2551 "errors": ["Error 1", "Error 2"]
2552 }"#;
2553
2554 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2555 let reserialized = serde_json::to_string(&output).unwrap();
2556
2557 assert!(reserialized.contains("Error 1"));
2559 assert!(reserialized.contains("Error 2"));
2560 }
2561
2562 #[test]
2567 fn test_permission_allow_tool() {
2568 let perm = Permission::allow_tool("Bash", "npm test");
2569
2570 assert_eq!(perm.permission_type, "addRules");
2571 assert_eq!(perm.destination, "session");
2572 assert_eq!(perm.behavior, Some("allow".to_string()));
2573 assert!(perm.mode.is_none());
2574
2575 let rules = perm.rules.unwrap();
2576 assert_eq!(rules.len(), 1);
2577 assert_eq!(rules[0].tool_name, "Bash");
2578 assert_eq!(rules[0].rule_content, "npm test");
2579 }
2580
2581 #[test]
2582 fn test_permission_allow_tool_with_destination() {
2583 let perm = Permission::allow_tool_with_destination("Read", "/tmp/**", "project");
2584
2585 assert_eq!(perm.permission_type, "addRules");
2586 assert_eq!(perm.destination, "project");
2587 assert_eq!(perm.behavior, Some("allow".to_string()));
2588
2589 let rules = perm.rules.unwrap();
2590 assert_eq!(rules[0].tool_name, "Read");
2591 assert_eq!(rules[0].rule_content, "/tmp/**");
2592 }
2593
2594 #[test]
2595 fn test_permission_set_mode() {
2596 let perm = Permission::set_mode("acceptEdits", "session");
2597
2598 assert_eq!(perm.permission_type, "setMode");
2599 assert_eq!(perm.destination, "session");
2600 assert_eq!(perm.mode, Some("acceptEdits".to_string()));
2601 assert!(perm.behavior.is_none());
2602 assert!(perm.rules.is_none());
2603 }
2604
2605 #[test]
2606 fn test_permission_serialization() {
2607 let perm = Permission::allow_tool("Bash", "npm test");
2608 let json = serde_json::to_string(&perm).unwrap();
2609
2610 assert!(json.contains("\"type\":\"addRules\""));
2611 assert!(json.contains("\"destination\":\"session\""));
2612 assert!(json.contains("\"behavior\":\"allow\""));
2613 assert!(json.contains("\"toolName\":\"Bash\""));
2614 assert!(json.contains("\"ruleContent\":\"npm test\""));
2615 }
2616
2617 #[test]
2618 fn test_permission_from_suggestion_set_mode() {
2619 let suggestion = PermissionSuggestion {
2620 suggestion_type: "setMode".to_string(),
2621 destination: "session".to_string(),
2622 mode: Some("acceptEdits".to_string()),
2623 behavior: None,
2624 rules: None,
2625 };
2626
2627 let perm = Permission::from_suggestion(&suggestion);
2628
2629 assert_eq!(perm.permission_type, "setMode");
2630 assert_eq!(perm.destination, "session");
2631 assert_eq!(perm.mode, Some("acceptEdits".to_string()));
2632 }
2633
2634 #[test]
2635 fn test_permission_from_suggestion_add_rules() {
2636 let suggestion = PermissionSuggestion {
2637 suggestion_type: "addRules".to_string(),
2638 destination: "session".to_string(),
2639 mode: None,
2640 behavior: Some("allow".to_string()),
2641 rules: Some(vec![serde_json::json!({
2642 "toolName": "Read",
2643 "ruleContent": "/tmp/**"
2644 })]),
2645 };
2646
2647 let perm = Permission::from_suggestion(&suggestion);
2648
2649 assert_eq!(perm.permission_type, "addRules");
2650 assert_eq!(perm.behavior, Some("allow".to_string()));
2651
2652 let rules = perm.rules.unwrap();
2653 assert_eq!(rules.len(), 1);
2654 assert_eq!(rules[0].tool_name, "Read");
2655 assert_eq!(rules[0].rule_content, "/tmp/**");
2656 }
2657
2658 #[test]
2659 fn test_permission_result_allow_with_typed_permissions() {
2660 let result = PermissionResult::allow_with_typed_permissions(
2661 serde_json::json!({"command": "npm test"}),
2662 vec![Permission::allow_tool("Bash", "npm test")],
2663 );
2664
2665 let json = serde_json::to_string(&result).unwrap();
2666 assert!(json.contains("\"behavior\":\"allow\""));
2667 assert!(json.contains("\"updatedPermissions\""));
2668 assert!(json.contains("\"toolName\":\"Bash\""));
2669 }
2670
2671 #[test]
2672 fn test_tool_permission_request_allow_and_remember() {
2673 let req = ToolPermissionRequest {
2674 tool_name: "Bash".to_string(),
2675 input: serde_json::json!({"command": "npm test"}),
2676 permission_suggestions: vec![],
2677 blocked_path: None,
2678 decision_reason: None,
2679 tool_use_id: None,
2680 };
2681
2682 let response =
2683 req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
2684 let message: ControlResponseMessage = response.into();
2685 let json = serde_json::to_string(&message).unwrap();
2686
2687 assert!(json.contains("\"type\":\"control_response\""));
2688 assert!(json.contains("\"behavior\":\"allow\""));
2689 assert!(json.contains("\"updatedPermissions\""));
2690 assert!(json.contains("\"toolName\":\"Bash\""));
2691 }
2692
2693 #[test]
2694 fn test_tool_permission_request_allow_and_remember_suggestion() {
2695 let req = ToolPermissionRequest {
2696 tool_name: "Bash".to_string(),
2697 input: serde_json::json!({"command": "npm test"}),
2698 permission_suggestions: vec![PermissionSuggestion {
2699 suggestion_type: "setMode".to_string(),
2700 destination: "session".to_string(),
2701 mode: Some("acceptEdits".to_string()),
2702 behavior: None,
2703 rules: None,
2704 }],
2705 blocked_path: None,
2706 decision_reason: None,
2707 tool_use_id: None,
2708 };
2709
2710 let response = req.allow_and_remember_suggestion("req-123");
2711 assert!(response.is_some());
2712
2713 let message: ControlResponseMessage = response.unwrap().into();
2714 let json = serde_json::to_string(&message).unwrap();
2715
2716 assert!(json.contains("\"type\":\"setMode\""));
2717 assert!(json.contains("\"mode\":\"acceptEdits\""));
2718 }
2719
2720 #[test]
2721 fn test_tool_permission_request_allow_and_remember_suggestion_none() {
2722 let req = ToolPermissionRequest {
2723 tool_name: "Bash".to_string(),
2724 input: serde_json::json!({"command": "npm test"}),
2725 permission_suggestions: vec![], blocked_path: None,
2727 decision_reason: None,
2728 tool_use_id: None,
2729 };
2730
2731 let response = req.allow_and_remember_suggestion("req-123");
2732 assert!(response.is_none());
2733 }
2734
2735 #[test]
2740 fn test_deserialize_anthropic_error() {
2741 let json = r#"{
2742 "type": "error",
2743 "error": {
2744 "type": "api_error",
2745 "message": "Internal server error"
2746 },
2747 "request_id": "req_011CXPC6BqUogB959LWEf52X"
2748 }"#;
2749
2750 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2751 assert!(output.is_api_error());
2752 assert_eq!(output.message_type(), "error");
2753
2754 if let ClaudeOutput::Error(err) = output {
2755 assert_eq!(err.error.error_type, "api_error");
2756 assert_eq!(err.error.message, "Internal server error");
2757 assert_eq!(
2758 err.request_id,
2759 Some("req_011CXPC6BqUogB959LWEf52X".to_string())
2760 );
2761 assert!(err.is_server_error());
2762 assert!(!err.is_overloaded());
2763 } else {
2764 panic!("Expected Error variant");
2765 }
2766 }
2767
2768 #[test]
2769 fn test_deserialize_anthropic_overloaded_error() {
2770 let json = r#"{
2771 "type": "error",
2772 "error": {
2773 "type": "overloaded_error",
2774 "message": "Overloaded"
2775 }
2776 }"#;
2777
2778 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2779
2780 if let ClaudeOutput::Error(err) = output {
2781 assert!(err.is_overloaded());
2782 assert!(!err.is_server_error());
2783 assert!(err.request_id.is_none());
2784 } else {
2785 panic!("Expected Error variant");
2786 }
2787 }
2788
2789 #[test]
2790 fn test_deserialize_anthropic_rate_limit_error() {
2791 let json = r#"{
2792 "type": "error",
2793 "error": {
2794 "type": "rate_limit_error",
2795 "message": "Rate limit exceeded"
2796 },
2797 "request_id": "req_456"
2798 }"#;
2799
2800 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2801
2802 if let ClaudeOutput::Error(err) = output {
2803 assert!(err.is_rate_limited());
2804 assert!(!err.is_overloaded());
2805 assert!(!err.is_server_error());
2806 } else {
2807 panic!("Expected Error variant");
2808 }
2809 }
2810
2811 #[test]
2812 fn test_deserialize_anthropic_authentication_error() {
2813 let json = r#"{
2814 "type": "error",
2815 "error": {
2816 "type": "authentication_error",
2817 "message": "Invalid API key"
2818 }
2819 }"#;
2820
2821 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2822
2823 if let ClaudeOutput::Error(err) = output {
2824 assert!(err.is_authentication_error());
2825 } else {
2826 panic!("Expected Error variant");
2827 }
2828 }
2829
2830 #[test]
2831 fn test_deserialize_anthropic_invalid_request_error() {
2832 let json = r#"{
2833 "type": "error",
2834 "error": {
2835 "type": "invalid_request_error",
2836 "message": "Invalid request body"
2837 }
2838 }"#;
2839
2840 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2841
2842 if let ClaudeOutput::Error(err) = output {
2843 assert!(err.is_invalid_request());
2844 } else {
2845 panic!("Expected Error variant");
2846 }
2847 }
2848
2849 #[test]
2850 fn test_anthropic_error_as_helper() {
2851 let json = r#"{"type":"error","error":{"type":"api_error","message":"Error"}}"#;
2852 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2853
2854 let err = output.as_anthropic_error();
2855 assert!(err.is_some());
2856 assert_eq!(err.unwrap().error.error_type, "api_error");
2857
2858 let result_json = r#"{
2860 "type": "result",
2861 "subtype": "success",
2862 "is_error": false,
2863 "duration_ms": 100,
2864 "duration_api_ms": 200,
2865 "num_turns": 1,
2866 "session_id": "abc",
2867 "total_cost_usd": 0.01
2868 }"#;
2869 let result: ClaudeOutput = serde_json::from_str(result_json).unwrap();
2870 assert!(result.as_anthropic_error().is_none());
2871 }
2872
2873 #[test]
2874 fn test_anthropic_error_roundtrip() {
2875 let error = AnthropicError {
2876 error: AnthropicErrorDetails {
2877 error_type: "api_error".to_string(),
2878 message: "Test error".to_string(),
2879 },
2880 request_id: Some("req_123".to_string()),
2881 };
2882
2883 let json = serde_json::to_string(&error).unwrap();
2884 assert!(json.contains("\"type\":\"api_error\""));
2885 assert!(json.contains("\"message\":\"Test error\""));
2886 assert!(json.contains("\"request_id\":\"req_123\""));
2887
2888 let parsed: AnthropicError = serde_json::from_str(&json).unwrap();
2889 assert_eq!(parsed, error);
2890 }
2891
2892 #[test]
2893 fn test_anthropic_error_session_id_is_none() {
2894 let json = r#"{"type":"error","error":{"type":"api_error","message":"Error"}}"#;
2895 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
2896 assert!(output.session_id().is_none());
2897 }
2898}