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
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct UserMessage {
123 pub message: MessageContent,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 #[serde(
126 serialize_with = "serialize_optional_uuid",
127 deserialize_with = "deserialize_optional_uuid"
128 )]
129 pub session_id: Option<Uuid>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct MessageContent {
135 pub role: String,
136 #[serde(deserialize_with = "deserialize_content_blocks")]
137 pub content: Vec<ContentBlock>,
138}
139
140fn deserialize_content_blocks<'de, D>(deserializer: D) -> Result<Vec<ContentBlock>, D::Error>
142where
143 D: Deserializer<'de>,
144{
145 let value: Value = Value::deserialize(deserializer)?;
146 match value {
147 Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock { text: s })]),
148 Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
149 _ => Err(serde::de::Error::custom(
150 "content must be a string or array",
151 )),
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct SystemMessage {
158 pub subtype: String,
159 #[serde(flatten)]
160 pub data: Value, }
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct AssistantMessage {
166 pub message: AssistantMessageContent,
167 pub session_id: String,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub uuid: Option<String>,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub parent_tool_use_id: Option<String>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct AssistantMessageContent {
177 pub id: String,
178 pub role: String,
179 pub model: String,
180 pub content: Vec<ContentBlock>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub stop_reason: Option<String>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub stop_sequence: Option<String>,
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub usage: Option<serde_json::Value>,
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191#[serde(tag = "type", rename_all = "snake_case")]
192pub enum ContentBlock {
193 Text(TextBlock),
194 Image(ImageBlock),
195 Thinking(ThinkingBlock),
196 ToolUse(ToolUseBlock),
197 ToolResult(ToolResultBlock),
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct TextBlock {
203 pub text: String,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ImageBlock {
209 pub source: ImageSource,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct ImageSource {
215 #[serde(rename = "type")]
216 pub source_type: String, pub media_type: String, pub data: String, }
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct ThinkingBlock {
224 pub thinking: String,
225 pub signature: String,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ToolUseBlock {
231 pub id: String,
232 pub name: String,
233 pub input: Value,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct ToolResultBlock {
239 pub tool_use_id: String,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub content: Option<ToolResultContent>,
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub is_error: Option<bool>,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248#[serde(untagged)]
249pub enum ToolResultContent {
250 Text(String),
251 Structured(Vec<Value>),
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ResultMessage {
257 pub subtype: ResultSubtype,
258 pub is_error: bool,
259 pub duration_ms: u64,
260 pub duration_api_ms: u64,
261 pub num_turns: i32,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
264 pub result: Option<String>,
265
266 pub session_id: String,
267 pub total_cost_usd: f64,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub usage: Option<UsageInfo>,
271
272 #[serde(default)]
273 pub permission_denials: Vec<Value>,
274
275 #[serde(skip_serializing_if = "Option::is_none")]
276 pub uuid: Option<String>,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281#[serde(rename_all = "snake_case")]
282pub enum ResultSubtype {
283 Success,
284 ErrorMaxTurns,
285 ErrorDuringExecution,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290#[serde(tag = "type", rename_all = "snake_case")]
291pub enum McpServerConfig {
292 Stdio(McpStdioServerConfig),
293 Sse(McpSseServerConfig),
294 Http(McpHttpServerConfig),
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct McpStdioServerConfig {
300 pub command: String,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub args: Option<Vec<String>>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub env: Option<std::collections::HashMap<String, String>>,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct McpSseServerConfig {
310 pub url: String,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub headers: Option<std::collections::HashMap<String, String>>,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct McpHttpServerConfig {
318 pub url: String,
319 #[serde(skip_serializing_if = "Option::is_none")]
320 pub headers: Option<std::collections::HashMap<String, String>>,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325#[serde(rename_all = "camelCase")]
326pub enum PermissionMode {
327 Default,
328 AcceptEdits,
329 BypassPermissions,
330 Plan,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct ControlRequest {
344 pub request_id: String,
346 pub request: ControlRequestPayload,
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
352#[serde(tag = "subtype", rename_all = "snake_case")]
353pub enum ControlRequestPayload {
354 CanUseTool(ToolPermissionRequest),
356 HookCallback(HookCallbackRequest),
358 McpMessage(McpMessageRequest),
360 Initialize(InitializeRequest),
362}
363
364#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct ToolPermissionRequest {
391 pub tool_name: String,
393 pub input: Value,
395 #[serde(default)]
397 pub permission_suggestions: Vec<Value>,
398 #[serde(skip_serializing_if = "Option::is_none")]
400 pub blocked_path: Option<String>,
401}
402
403impl ToolPermissionRequest {
404 pub fn allow(&self, request_id: &str) -> ControlResponse {
419 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
420 }
421
422 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
442 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
443 }
444
445 pub fn allow_with_permissions(
447 &self,
448 modified_input: Value,
449 permissions: Vec<Value>,
450 request_id: &str,
451 ) -> ControlResponse {
452 ControlResponse::from_result(
453 request_id,
454 PermissionResult::allow_with_permissions(modified_input, permissions),
455 )
456 }
457
458 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
475 ControlResponse::from_result(request_id, PermissionResult::deny(message))
476 }
477
478 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
482 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
483 }
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
491#[serde(tag = "behavior", rename_all = "snake_case")]
492pub enum PermissionResult {
493 Allow {
495 #[serde(rename = "updatedInput")]
497 updated_input: Value,
498 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
500 updated_permissions: Option<Vec<Value>>,
501 },
502 Deny {
504 message: String,
506 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
508 interrupt: bool,
509 },
510}
511
512impl PermissionResult {
513 pub fn allow(input: Value) -> Self {
515 PermissionResult::Allow {
516 updated_input: input,
517 updated_permissions: None,
518 }
519 }
520
521 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
523 PermissionResult::Allow {
524 updated_input: input,
525 updated_permissions: Some(permissions),
526 }
527 }
528
529 pub fn deny(message: impl Into<String>) -> Self {
531 PermissionResult::Deny {
532 message: message.into(),
533 interrupt: false,
534 }
535 }
536
537 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
539 PermissionResult::Deny {
540 message: message.into(),
541 interrupt: true,
542 }
543 }
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct HookCallbackRequest {
549 pub callback_id: String,
550 pub input: Value,
551 #[serde(skip_serializing_if = "Option::is_none")]
552 pub tool_use_id: Option<String>,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize)]
557pub struct McpMessageRequest {
558 pub server_name: String,
559 pub message: Value,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
564pub struct InitializeRequest {
565 #[serde(skip_serializing_if = "Option::is_none")]
566 pub hooks: Option<Value>,
567}
568
569#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct ControlResponse {
575 pub response: ControlResponsePayload,
577}
578
579impl ControlResponse {
580 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
584 let response_value = serde_json::to_value(&result)
586 .expect("PermissionResult serialization should never fail");
587 ControlResponse {
588 response: ControlResponsePayload::Success {
589 request_id: request_id.to_string(),
590 response: Some(response_value),
591 },
592 }
593 }
594
595 pub fn success(request_id: &str, response_data: Value) -> Self {
597 ControlResponse {
598 response: ControlResponsePayload::Success {
599 request_id: request_id.to_string(),
600 response: Some(response_data),
601 },
602 }
603 }
604
605 pub fn success_empty(request_id: &str) -> Self {
607 ControlResponse {
608 response: ControlResponsePayload::Success {
609 request_id: request_id.to_string(),
610 response: None,
611 },
612 }
613 }
614
615 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
617 ControlResponse {
618 response: ControlResponsePayload::Error {
619 request_id: request_id.to_string(),
620 error: error_message.into(),
621 },
622 }
623 }
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize)]
628#[serde(tag = "subtype", rename_all = "snake_case")]
629pub enum ControlResponsePayload {
630 Success {
631 request_id: String,
632 #[serde(skip_serializing_if = "Option::is_none")]
633 response: Option<Value>,
634 },
635 Error {
636 request_id: String,
637 error: String,
638 },
639}
640
641#[derive(Debug, Clone, Serialize, Deserialize)]
643pub struct ControlResponseMessage {
644 #[serde(rename = "type")]
645 pub message_type: String,
646 pub response: ControlResponsePayload,
647}
648
649impl From<ControlResponse> for ControlResponseMessage {
650 fn from(resp: ControlResponse) -> Self {
651 ControlResponseMessage {
652 message_type: "control_response".to_string(),
653 response: resp.response,
654 }
655 }
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct ControlRequestMessage {
661 #[serde(rename = "type")]
662 pub message_type: String,
663 pub request_id: String,
664 pub request: ControlRequestPayload,
665}
666
667impl ControlRequestMessage {
668 pub fn initialize(request_id: impl Into<String>) -> Self {
670 ControlRequestMessage {
671 message_type: "control_request".to_string(),
672 request_id: request_id.into(),
673 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
674 }
675 }
676
677 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
679 ControlRequestMessage {
680 message_type: "control_request".to_string(),
681 request_id: request_id.into(),
682 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
683 }
684 }
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize)]
689pub struct UsageInfo {
690 pub input_tokens: u32,
691 pub cache_creation_input_tokens: u32,
692 pub cache_read_input_tokens: u32,
693 pub output_tokens: u32,
694 pub server_tool_use: ServerToolUse,
695 pub service_tier: String,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
700pub struct ServerToolUse {
701 pub web_search_requests: u32,
702}
703
704impl ClaudeInput {
705 pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
707 ClaudeInput::User(UserMessage {
708 message: MessageContent {
709 role: "user".to_string(),
710 content: vec![ContentBlock::Text(TextBlock { text: text.into() })],
711 },
712 session_id: Some(session_id),
713 })
714 }
715
716 pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
718 ClaudeInput::User(UserMessage {
719 message: MessageContent {
720 role: "user".to_string(),
721 content: blocks,
722 },
723 session_id: Some(session_id),
724 })
725 }
726
727 pub fn user_message_with_image(
730 image_data: String,
731 media_type: String,
732 text: Option<String>,
733 session_id: Uuid,
734 ) -> Result<Self, String> {
735 let valid_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
737
738 if !valid_types.contains(&media_type.as_str()) {
739 return Err(format!(
740 "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
741 media_type
742 ));
743 }
744
745 let mut blocks = vec![ContentBlock::Image(ImageBlock {
746 source: ImageSource {
747 source_type: "base64".to_string(),
748 media_type,
749 data: image_data,
750 },
751 })];
752
753 if let Some(text_content) = text {
754 blocks.push(ContentBlock::Text(TextBlock { text: text_content }));
755 }
756
757 Ok(Self::user_message_blocks(blocks, session_id))
758 }
759}
760
761impl ClaudeOutput {
762 pub fn message_type(&self) -> String {
764 match self {
765 ClaudeOutput::System(_) => "system".to_string(),
766 ClaudeOutput::User(_) => "user".to_string(),
767 ClaudeOutput::Assistant(_) => "assistant".to_string(),
768 ClaudeOutput::Result(_) => "result".to_string(),
769 ClaudeOutput::ControlRequest(_) => "control_request".to_string(),
770 ClaudeOutput::ControlResponse(_) => "control_response".to_string(),
771 }
772 }
773
774 pub fn is_control_request(&self) -> bool {
776 matches!(self, ClaudeOutput::ControlRequest(_))
777 }
778
779 pub fn is_control_response(&self) -> bool {
781 matches!(self, ClaudeOutput::ControlResponse(_))
782 }
783
784 pub fn as_control_request(&self) -> Option<&ControlRequest> {
786 match self {
787 ClaudeOutput::ControlRequest(req) => Some(req),
788 _ => None,
789 }
790 }
791
792 pub fn is_error(&self) -> bool {
794 matches!(self, ClaudeOutput::Result(r) if r.is_error)
795 }
796
797 pub fn is_assistant_message(&self) -> bool {
799 matches!(self, ClaudeOutput::Assistant(_))
800 }
801
802 pub fn is_system_message(&self) -> bool {
804 matches!(self, ClaudeOutput::System(_))
805 }
806
807 pub fn parse_json_tolerant(s: &str) -> Result<ClaudeOutput, ParseError> {
812 match Self::parse_json(s) {
814 Ok(output) => Ok(output),
815 Err(first_error) => {
816 if let Some(json_start) = s.find('{') {
818 let trimmed = &s[json_start..];
819 match Self::parse_json(trimmed) {
820 Ok(output) => Ok(output),
821 Err(_) => {
822 Err(first_error)
824 }
825 }
826 } else {
827 Err(first_error)
828 }
829 }
830 }
831 }
832
833 pub fn parse_json(s: &str) -> Result<ClaudeOutput, ParseError> {
835 let value: Value = serde_json::from_str(s).map_err(|e| ParseError {
837 raw_json: Value::String(s.to_string()),
838 error_message: format!("Invalid JSON: {}", e),
839 })?;
840
841 serde_json::from_value::<ClaudeOutput>(value.clone()).map_err(|e| ParseError {
843 raw_json: value,
844 error_message: e.to_string(),
845 })
846 }
847}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852
853 #[test]
854 fn test_serialize_user_message() {
855 let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
856 let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
857 let json = serde_json::to_string(&input).unwrap();
858 assert!(json.contains("\"type\":\"user\""));
859 assert!(json.contains("\"role\":\"user\""));
860 assert!(json.contains("\"text\":\"Hello, Claude!\""));
861 assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
862 }
863
864 #[test]
865 fn test_deserialize_assistant_message() {
866 let json = r#"{
867 "type": "assistant",
868 "message": {
869 "id": "msg_123",
870 "role": "assistant",
871 "model": "claude-3-sonnet",
872 "content": [{"type": "text", "text": "Hello! How can I help you?"}]
873 },
874 "session_id": "123"
875 }"#;
876
877 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
878 assert!(output.is_assistant_message());
879 }
880
881 #[test]
882 fn test_deserialize_result_message() {
883 let json = r#"{
884 "type": "result",
885 "subtype": "success",
886 "is_error": false,
887 "duration_ms": 100,
888 "duration_api_ms": 200,
889 "num_turns": 1,
890 "result": "Done",
891 "session_id": "123",
892 "total_cost_usd": 0.01,
893 "permission_denials": []
894 }"#;
895
896 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
897 assert!(!output.is_error());
898 }
899
900 #[test]
905 fn test_deserialize_control_request_can_use_tool() {
906 let json = r#"{
907 "type": "control_request",
908 "request_id": "perm-abc123",
909 "request": {
910 "subtype": "can_use_tool",
911 "tool_name": "Write",
912 "input": {
913 "file_path": "/home/user/hello.py",
914 "content": "print('hello')"
915 },
916 "permission_suggestions": [],
917 "blocked_path": null
918 }
919 }"#;
920
921 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
922 assert!(output.is_control_request());
923
924 if let ClaudeOutput::ControlRequest(req) = output {
925 assert_eq!(req.request_id, "perm-abc123");
926 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
927 assert_eq!(perm_req.tool_name, "Write");
928 assert_eq!(
929 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
930 "/home/user/hello.py"
931 );
932 } else {
933 panic!("Expected CanUseTool payload");
934 }
935 } else {
936 panic!("Expected ControlRequest");
937 }
938 }
939
940 #[test]
941 fn test_tool_permission_request_allow() {
942 let req = ToolPermissionRequest {
943 tool_name: "Read".to_string(),
944 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
945 permission_suggestions: vec![],
946 blocked_path: None,
947 };
948
949 let response = req.allow("req-123");
950 let message: ControlResponseMessage = response.into();
951
952 let json = serde_json::to_string(&message).unwrap();
953 assert!(json.contains("\"type\":\"control_response\""));
954 assert!(json.contains("\"subtype\":\"success\""));
955 assert!(json.contains("\"request_id\":\"req-123\""));
956 assert!(json.contains("\"behavior\":\"allow\""));
957 assert!(json.contains("\"updatedInput\""));
958 }
959
960 #[test]
961 fn test_tool_permission_request_allow_with_modified_input() {
962 let req = ToolPermissionRequest {
963 tool_name: "Write".to_string(),
964 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
965 permission_suggestions: vec![],
966 blocked_path: None,
967 };
968
969 let modified_input = serde_json::json!({
970 "file_path": "/tmp/safe/passwd",
971 "content": "test"
972 });
973 let response = req.allow_with(modified_input, "req-456");
974 let message: ControlResponseMessage = response.into();
975
976 let json = serde_json::to_string(&message).unwrap();
977 assert!(json.contains("/tmp/safe/passwd"));
978 assert!(!json.contains("/etc/passwd"));
979 }
980
981 #[test]
982 fn test_tool_permission_request_deny() {
983 let req = ToolPermissionRequest {
984 tool_name: "Bash".to_string(),
985 input: serde_json::json!({"command": "sudo rm -rf /"}),
986 permission_suggestions: vec![],
987 blocked_path: None,
988 };
989
990 let response = req.deny("Dangerous command blocked", "req-789");
991 let message: ControlResponseMessage = response.into();
992
993 let json = serde_json::to_string(&message).unwrap();
994 assert!(json.contains("\"behavior\":\"deny\""));
995 assert!(json.contains("Dangerous command blocked"));
996 assert!(!json.contains("\"interrupt\":true"));
997 }
998
999 #[test]
1000 fn test_tool_permission_request_deny_and_stop() {
1001 let req = ToolPermissionRequest {
1002 tool_name: "Bash".to_string(),
1003 input: serde_json::json!({"command": "rm -rf /"}),
1004 permission_suggestions: vec![],
1005 blocked_path: None,
1006 };
1007
1008 let response = req.deny_and_stop("Security violation", "req-000");
1009 let message: ControlResponseMessage = response.into();
1010
1011 let json = serde_json::to_string(&message).unwrap();
1012 assert!(json.contains("\"behavior\":\"deny\""));
1013 assert!(json.contains("\"interrupt\":true"));
1014 }
1015
1016 #[test]
1017 fn test_permission_result_serialization() {
1018 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1020 let json = serde_json::to_string(&allow).unwrap();
1021 assert!(json.contains("\"behavior\":\"allow\""));
1022 assert!(json.contains("\"updatedInput\""));
1023
1024 let deny = PermissionResult::deny("Not allowed");
1026 let json = serde_json::to_string(&deny).unwrap();
1027 assert!(json.contains("\"behavior\":\"deny\""));
1028 assert!(json.contains("\"message\":\"Not allowed\""));
1029 assert!(!json.contains("\"interrupt\""));
1030
1031 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1033 let json = serde_json::to_string(&deny_stop).unwrap();
1034 assert!(json.contains("\"interrupt\":true"));
1035 }
1036
1037 #[test]
1038 fn test_control_request_message_initialize() {
1039 let init = ControlRequestMessage::initialize("init-1");
1040
1041 let json = serde_json::to_string(&init).unwrap();
1042 assert!(json.contains("\"type\":\"control_request\""));
1043 assert!(json.contains("\"request_id\":\"init-1\""));
1044 assert!(json.contains("\"subtype\":\"initialize\""));
1045 }
1046
1047 #[test]
1048 fn test_control_response_error() {
1049 let response = ControlResponse::error("req-err", "Something went wrong");
1050 let message: ControlResponseMessage = response.into();
1051
1052 let json = serde_json::to_string(&message).unwrap();
1053 assert!(json.contains("\"subtype\":\"error\""));
1054 assert!(json.contains("\"error\":\"Something went wrong\""));
1055 }
1056
1057 #[test]
1058 fn test_roundtrip_control_request() {
1059 let original_json = r#"{
1061 "type": "control_request",
1062 "request_id": "test-123",
1063 "request": {
1064 "subtype": "can_use_tool",
1065 "tool_name": "Bash",
1066 "input": {"command": "ls -la"},
1067 "permission_suggestions": []
1068 }
1069 }"#;
1070
1071 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1073
1074 let reserialized = serde_json::to_string(&output).unwrap();
1076 assert!(reserialized.contains("control_request"));
1077 assert!(reserialized.contains("test-123"));
1078 assert!(reserialized.contains("Bash"));
1079 }
1080}