1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::collections::HashMap;
4use std::fmt;
5
6use crate::tool_inputs::AskUserQuestionInput;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub enum PermissionType {
18 AddRules,
20 SetMode,
22 Unknown(String),
24}
25
26impl PermissionType {
27 pub fn as_str(&self) -> &str {
28 match self {
29 Self::AddRules => "addRules",
30 Self::SetMode => "setMode",
31 Self::Unknown(s) => s.as_str(),
32 }
33 }
34}
35
36impl fmt::Display for PermissionType {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 f.write_str(self.as_str())
39 }
40}
41
42impl From<&str> for PermissionType {
43 fn from(s: &str) -> Self {
44 match s {
45 "addRules" => Self::AddRules,
46 "setMode" => Self::SetMode,
47 other => Self::Unknown(other.to_string()),
48 }
49 }
50}
51
52impl Serialize for PermissionType {
53 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
54 serializer.serialize_str(self.as_str())
55 }
56}
57
58impl<'de> Deserialize<'de> for PermissionType {
59 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
60 let s = String::deserialize(deserializer)?;
61 Ok(Self::from(s.as_str()))
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Hash)]
67pub enum PermissionDestination {
68 Session,
70 Project,
72 Unknown(String),
74}
75
76impl PermissionDestination {
77 pub fn as_str(&self) -> &str {
78 match self {
79 Self::Session => "session",
80 Self::Project => "project",
81 Self::Unknown(s) => s.as_str(),
82 }
83 }
84}
85
86impl fmt::Display for PermissionDestination {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 f.write_str(self.as_str())
89 }
90}
91
92impl From<&str> for PermissionDestination {
93 fn from(s: &str) -> Self {
94 match s {
95 "session" => Self::Session,
96 "project" => Self::Project,
97 other => Self::Unknown(other.to_string()),
98 }
99 }
100}
101
102impl Serialize for PermissionDestination {
103 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
104 serializer.serialize_str(self.as_str())
105 }
106}
107
108impl<'de> Deserialize<'de> for PermissionDestination {
109 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
110 let s = String::deserialize(deserializer)?;
111 Ok(Self::from(s.as_str()))
112 }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Hash)]
117pub enum PermissionBehavior {
118 Allow,
120 Deny,
122 Unknown(String),
124}
125
126impl PermissionBehavior {
127 pub fn as_str(&self) -> &str {
128 match self {
129 Self::Allow => "allow",
130 Self::Deny => "deny",
131 Self::Unknown(s) => s.as_str(),
132 }
133 }
134}
135
136impl fmt::Display for PermissionBehavior {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 f.write_str(self.as_str())
139 }
140}
141
142impl From<&str> for PermissionBehavior {
143 fn from(s: &str) -> Self {
144 match s {
145 "allow" => Self::Allow,
146 "deny" => Self::Deny,
147 other => Self::Unknown(other.to_string()),
148 }
149 }
150}
151
152impl Serialize for PermissionBehavior {
153 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
154 serializer.serialize_str(self.as_str())
155 }
156}
157
158impl<'de> Deserialize<'de> for PermissionBehavior {
159 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
160 let s = String::deserialize(deserializer)?;
161 Ok(Self::from(s.as_str()))
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Hash)]
167pub enum PermissionModeName {
168 AcceptEdits,
170 BypassPermissions,
172 Unknown(String),
174}
175
176impl PermissionModeName {
177 pub fn as_str(&self) -> &str {
178 match self {
179 Self::AcceptEdits => "acceptEdits",
180 Self::BypassPermissions => "bypassPermissions",
181 Self::Unknown(s) => s.as_str(),
182 }
183 }
184}
185
186impl fmt::Display for PermissionModeName {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 f.write_str(self.as_str())
189 }
190}
191
192impl From<&str> for PermissionModeName {
193 fn from(s: &str) -> Self {
194 match s {
195 "acceptEdits" => Self::AcceptEdits,
196 "bypassPermissions" => Self::BypassPermissions,
197 other => Self::Unknown(other.to_string()),
198 }
199 }
200}
201
202impl Serialize for PermissionModeName {
203 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
204 serializer.serialize_str(self.as_str())
205 }
206}
207
208impl<'de> Deserialize<'de> for PermissionModeName {
209 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
210 let s = String::deserialize(deserializer)?;
211 Ok(Self::from(s.as_str()))
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct ControlRequest {
226 pub request_id: String,
228 pub request: ControlRequestPayload,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234#[serde(tag = "subtype", rename_all = "snake_case")]
235pub enum ControlRequestPayload {
236 CanUseTool(ToolPermissionRequest),
238 HookCallback(HookCallbackRequest),
240 McpMessage(McpMessageRequest),
242 Initialize(InitializeRequest),
244 Interrupt,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
265pub struct Permission {
266 #[serde(rename = "type")]
268 pub permission_type: PermissionType,
269 pub destination: PermissionDestination,
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub mode: Option<PermissionModeName>,
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub behavior: Option<PermissionBehavior>,
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub rules: Option<Vec<PermissionRule>>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
284pub struct PermissionRule {
285 #[serde(rename = "toolName")]
287 pub tool_name: String,
288 #[serde(rename = "ruleContent")]
290 pub rule_content: String,
291}
292
293impl Permission {
294 pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
307 Permission {
308 permission_type: PermissionType::AddRules,
309 destination: PermissionDestination::Session,
310 mode: None,
311 behavior: Some(PermissionBehavior::Allow),
312 rules: Some(vec![PermissionRule {
313 tool_name: tool_name.into(),
314 rule_content: rule_content.into(),
315 }]),
316 }
317 }
318
319 pub fn allow_tool_with_destination(
329 tool_name: impl Into<String>,
330 rule_content: impl Into<String>,
331 destination: PermissionDestination,
332 ) -> Self {
333 Permission {
334 permission_type: PermissionType::AddRules,
335 destination,
336 mode: None,
337 behavior: Some(PermissionBehavior::Allow),
338 rules: Some(vec![PermissionRule {
339 tool_name: tool_name.into(),
340 rule_content: rule_content.into(),
341 }]),
342 }
343 }
344
345 pub fn set_mode(mode: PermissionModeName, destination: PermissionDestination) -> Self {
355 Permission {
356 permission_type: PermissionType::SetMode,
357 destination,
358 mode: Some(mode),
359 behavior: None,
360 rules: None,
361 }
362 }
363
364 pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
383 Permission {
384 permission_type: suggestion.suggestion_type.clone(),
385 destination: suggestion.destination.clone(),
386 mode: suggestion.mode.clone(),
387 behavior: suggestion.behavior.clone(),
388 rules: suggestion.rules.as_ref().map(|rules| {
389 rules
390 .iter()
391 .filter_map(|v| {
392 Some(PermissionRule {
393 tool_name: v.get("toolName")?.as_str()?.to_string(),
394 rule_content: v.get("ruleContent")?.as_str()?.to_string(),
395 })
396 })
397 .collect()
398 }),
399 }
400 }
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
414pub struct PermissionSuggestion {
415 #[serde(rename = "type")]
417 pub suggestion_type: PermissionType,
418 pub destination: PermissionDestination,
420 #[serde(skip_serializing_if = "Option::is_none")]
422 pub mode: Option<PermissionModeName>,
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub behavior: Option<PermissionBehavior>,
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub rules: Option<Vec<Value>>,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct ToolPermissionRequest {
458 pub tool_name: String,
460 pub input: Value,
462 #[serde(default)]
464 pub permission_suggestions: Vec<PermissionSuggestion>,
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub blocked_path: Option<String>,
468 #[serde(skip_serializing_if = "Option::is_none")]
470 pub decision_reason: Option<String>,
471 #[serde(skip_serializing_if = "Option::is_none")]
473 pub tool_use_id: Option<String>,
474}
475
476impl ToolPermissionRequest {
477 pub fn allow(&self, request_id: &str) -> ControlResponse {
494 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
495 }
496
497 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
519 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
520 }
521
522 pub fn allow_with_permissions(
526 &self,
527 modified_input: Value,
528 permissions: Vec<Value>,
529 request_id: &str,
530 ) -> ControlResponse {
531 ControlResponse::from_result(
532 request_id,
533 PermissionResult::allow_with_permissions(modified_input, permissions),
534 )
535 }
536
537 pub fn allow_and_remember(
563 &self,
564 permissions: Vec<Permission>,
565 request_id: &str,
566 ) -> ControlResponse {
567 ControlResponse::from_result(
568 request_id,
569 PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
570 )
571 }
572
573 pub fn allow_with_and_remember(
577 &self,
578 modified_input: Value,
579 permissions: Vec<Permission>,
580 request_id: &str,
581 ) -> ControlResponse {
582 ControlResponse::from_result(
583 request_id,
584 PermissionResult::allow_with_typed_permissions(modified_input, permissions),
585 )
586 }
587
588 pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
614 self.permission_suggestions.first().map(|suggestion| {
615 let perm = Permission::from_suggestion(suggestion);
616 self.allow_and_remember(vec![perm], request_id)
617 })
618 }
619
620 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
639 ControlResponse::from_result(request_id, PermissionResult::deny(message))
640 }
641
642 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
646 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
647 }
648
649 pub fn answer_questions(
703 &self,
704 answers_by_index: &HashMap<usize, String>,
705 request_id: &str,
706 ) -> Result<ControlResponse, AskUserQuestionResponseError> {
707 if self.tool_name != "AskUserQuestion" {
708 return Err(AskUserQuestionResponseError::WrongTool(
709 self.tool_name.clone(),
710 ));
711 }
712 let parsed: AskUserQuestionInput = serde_json::from_value(self.input.clone())
713 .map_err(AskUserQuestionResponseError::ParseInput)?;
714 let total = parsed.questions.len();
715
716 let mut answers_map = serde_json::Map::new();
717 for (idx, answer) in answers_by_index {
718 let q = parsed.questions.get(*idx).ok_or(
719 AskUserQuestionResponseError::QuestionIndexOutOfRange { index: *idx, total },
720 )?;
721 answers_map.insert(q.question.clone(), Value::String(answer.clone()));
722 }
723
724 let mut updated_input = self.input.clone();
725 updated_input
727 .as_object_mut()
728 .expect("AskUserQuestion input is a JSON object")
729 .insert("answers".to_string(), Value::Object(answers_map));
730
731 Ok(ControlResponse::from_result(
732 request_id,
733 PermissionResult::allow(updated_input),
734 ))
735 }
736}
737
738#[derive(Debug, thiserror::Error)]
741pub enum AskUserQuestionResponseError {
742 #[error("expected tool_name=AskUserQuestion, got `{0}`")]
744 WrongTool(String),
745 #[error("failed to parse AskUserQuestion input: {0}")]
747 ParseInput(#[source] serde_json::Error),
748 #[error("answer references question index {index}, but only {total} question(s) were asked")]
750 QuestionIndexOutOfRange {
751 index: usize,
753 total: usize,
755 },
756}
757
758#[derive(Debug, Clone, Serialize, Deserialize)]
763#[serde(tag = "behavior", rename_all = "snake_case")]
764pub enum PermissionResult {
765 Allow {
767 #[serde(rename = "updatedInput")]
769 updated_input: Value,
770 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
772 updated_permissions: Option<Vec<Value>>,
773 },
774 Deny {
776 message: String,
778 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
780 interrupt: bool,
781 },
782}
783
784impl PermissionResult {
785 pub fn allow(input: Value) -> Self {
787 PermissionResult::Allow {
788 updated_input: input,
789 updated_permissions: None,
790 }
791 }
792
793 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
797 PermissionResult::Allow {
798 updated_input: input,
799 updated_permissions: Some(permissions),
800 }
801 }
802
803 pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
819 let permission_values: Vec<Value> = permissions
820 .into_iter()
821 .filter_map(|p| serde_json::to_value(p).ok())
822 .collect();
823 PermissionResult::Allow {
824 updated_input: input,
825 updated_permissions: Some(permission_values),
826 }
827 }
828
829 pub fn deny(message: impl Into<String>) -> Self {
831 PermissionResult::Deny {
832 message: message.into(),
833 interrupt: false,
834 }
835 }
836
837 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
839 PermissionResult::Deny {
840 message: message.into(),
841 interrupt: true,
842 }
843 }
844}
845
846#[derive(Debug, Clone, Serialize, Deserialize)]
848pub struct HookCallbackRequest {
849 pub callback_id: String,
850 pub input: Value,
851 #[serde(skip_serializing_if = "Option::is_none")]
852 pub tool_use_id: Option<String>,
853}
854
855#[derive(Debug, Clone, Serialize, Deserialize)]
857pub struct McpMessageRequest {
858 pub server_name: String,
859 pub message: Value,
860}
861
862#[derive(Debug, Clone, Serialize, Deserialize)]
864pub struct InitializeRequest {
865 #[serde(skip_serializing_if = "Option::is_none")]
866 pub hooks: Option<Value>,
867}
868
869#[derive(Debug, Clone, Serialize, Deserialize)]
874pub struct ControlResponse {
875 pub response: ControlResponsePayload,
877}
878
879impl ControlResponse {
880 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
884 let response_value = serde_json::to_value(&result)
886 .expect("PermissionResult serialization should never fail");
887 ControlResponse {
888 response: ControlResponsePayload::Success {
889 request_id: request_id.to_string(),
890 response: Some(response_value),
891 },
892 }
893 }
894
895 pub fn success(request_id: &str, response_data: Value) -> Self {
897 ControlResponse {
898 response: ControlResponsePayload::Success {
899 request_id: request_id.to_string(),
900 response: Some(response_data),
901 },
902 }
903 }
904
905 pub fn success_empty(request_id: &str) -> Self {
907 ControlResponse {
908 response: ControlResponsePayload::Success {
909 request_id: request_id.to_string(),
910 response: None,
911 },
912 }
913 }
914
915 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
917 ControlResponse {
918 response: ControlResponsePayload::Error {
919 request_id: request_id.to_string(),
920 error: error_message.into(),
921 },
922 }
923 }
924
925 pub fn get_usage_response(&self) -> Option<Result<GetUsageResponse, serde_json::Error>> {
927 match &self.response {
928 ControlResponsePayload::Success {
929 response: Some(value),
930 ..
931 } => Some(serde_json::from_value(value.clone())),
932 _ => None,
933 }
934 }
935}
936
937#[derive(Debug, Clone, Serialize, Deserialize)]
939#[serde(tag = "subtype", rename_all = "snake_case")]
940pub enum ControlResponsePayload {
941 Success {
942 request_id: String,
943 #[serde(skip_serializing_if = "Option::is_none")]
944 response: Option<Value>,
945 },
946 Error {
947 request_id: String,
948 error: String,
949 },
950}
951
952#[derive(Debug, Clone, Serialize, Deserialize)]
954pub struct GetUsageResponse {
955 pub session: UsageSession,
956 pub subscription_type: Option<String>,
957 pub rate_limits_available: bool,
958 pub rate_limits: Option<UsageRateLimits>,
959 pub behaviors: UsageBehaviors,
960 #[serde(flatten)]
961 pub extra: serde_json::Map<String, Value>,
962}
963
964#[derive(Debug, Clone, Default, Serialize, Deserialize)]
965pub struct UsageSession {
966 pub total_cost_usd: f64,
967 pub total_api_duration_ms: u64,
968 pub total_duration_ms: u64,
969 pub total_lines_added: u64,
970 pub total_lines_removed: u64,
971 pub model_usage: HashMap<String, UsageModelUsage>,
972 #[serde(flatten)]
973 pub extra: serde_json::Map<String, Value>,
974}
975
976#[derive(Debug, Clone, Default, Serialize, Deserialize)]
977#[serde(rename_all = "camelCase")]
978pub struct UsageModelUsage {
979 #[serde(default)]
980 pub input_tokens: u64,
981 #[serde(default)]
982 pub output_tokens: u64,
983 #[serde(default)]
984 pub cache_read_input_tokens: u64,
985 #[serde(default)]
986 pub cache_creation_input_tokens: u64,
987 #[serde(default)]
988 pub web_search_requests: u64,
989 #[serde(default, rename = "costUSD")]
990 pub cost_usd: f64,
991 #[serde(default)]
992 pub context_window: u64,
993 #[serde(default)]
994 pub max_output_tokens: u64,
995 #[serde(flatten)]
996 pub extra: serde_json::Map<String, Value>,
997}
998
999#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1000pub struct UsageRateLimits {
1001 #[serde(default, skip_serializing_if = "Option::is_none")]
1002 pub five_hour: Option<UsageRateLimitWindow>,
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub seven_day: Option<UsageRateLimitWindow>,
1005 #[serde(default, skip_serializing_if = "Option::is_none")]
1006 pub seven_day_oauth_apps: Option<UsageRateLimitWindow>,
1007 #[serde(default, skip_serializing_if = "Option::is_none")]
1008 pub seven_day_opus: Option<UsageRateLimitWindow>,
1009 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub seven_day_sonnet: Option<UsageRateLimitWindow>,
1011 #[serde(default, skip_serializing_if = "Option::is_none")]
1012 pub model_scoped: Option<Vec<ModelScopedRateLimit>>,
1013 #[serde(flatten)]
1014 pub extra: serde_json::Map<String, Value>,
1015}
1016
1017#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1018pub struct UsageRateLimitWindow {
1019 pub utilization: Option<f64>,
1020 pub resets_at: Option<String>,
1021 #[serde(flatten)]
1022 pub extra: serde_json::Map<String, Value>,
1023}
1024
1025#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1026pub struct ModelScopedRateLimit {
1027 pub display_name: String,
1028 pub utilization: Option<f64>,
1029 pub resets_at: Option<String>,
1030 #[serde(flatten)]
1031 pub extra: serde_json::Map<String, Value>,
1032}
1033
1034#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1035pub struct UsageBehaviors {
1036 pub request_count: u64,
1037 pub session_count: u64,
1038 pub behaviors: Vec<UsageBehavior>,
1039 pub agents: Value,
1040 pub skills: Value,
1041 pub plugins: Value,
1042 pub mcp_servers: Value,
1043 #[serde(flatten)]
1044 pub extra: serde_json::Map<String, Value>,
1045}
1046
1047#[derive(Debug, Clone, Serialize, Deserialize)]
1048pub struct UsageBehavior {
1049 pub key: String,
1050 pub pct: f64,
1051 pub count: u64,
1052 #[serde(flatten)]
1053 pub extra: serde_json::Map<String, Value>,
1054}
1055
1056#[derive(Debug, Clone, Serialize, Deserialize)]
1058pub struct ControlResponseMessage {
1059 #[serde(rename = "type")]
1060 pub message_type: String,
1061 pub response: ControlResponsePayload,
1062}
1063
1064impl From<ControlResponse> for ControlResponseMessage {
1065 fn from(resp: ControlResponse) -> Self {
1066 ControlResponseMessage {
1067 message_type: "control_response".to_string(),
1068 response: resp.response,
1069 }
1070 }
1071}
1072
1073#[derive(Debug, Clone, Serialize, Deserialize)]
1075pub struct ControlRequestMessage {
1076 #[serde(rename = "type")]
1077 pub message_type: String,
1078 pub request_id: String,
1079 pub request: ControlRequestPayload,
1080}
1081
1082impl ControlRequestMessage {
1083 pub fn initialize(request_id: impl Into<String>) -> Self {
1085 ControlRequestMessage {
1086 message_type: "control_request".to_string(),
1087 request_id: request_id.into(),
1088 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
1089 }
1090 }
1091
1092 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
1094 ControlRequestMessage {
1095 message_type: "control_request".to_string(),
1096 request_id: request_id.into(),
1097 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
1098 }
1099 }
1100
1101 pub fn interrupt(request_id: impl Into<String>) -> Self {
1108 ControlRequestMessage {
1109 message_type: "control_request".to_string(),
1110 request_id: request_id.into(),
1111 request: ControlRequestPayload::Interrupt,
1112 }
1113 }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 use super::*;
1119 use crate::io::ClaudeOutput;
1120
1121 #[test]
1122 fn test_deserialize_control_request_can_use_tool() {
1123 let json = r#"{
1124 "type": "control_request",
1125 "request_id": "perm-abc123",
1126 "request": {
1127 "subtype": "can_use_tool",
1128 "tool_name": "Write",
1129 "input": {
1130 "file_path": "/home/user/hello.py",
1131 "content": "print('hello')"
1132 },
1133 "permission_suggestions": [],
1134 "blocked_path": null
1135 }
1136 }"#;
1137
1138 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1139 assert!(output.is_control_request());
1140
1141 if let ClaudeOutput::ControlRequest(req) = output {
1142 assert_eq!(req.request_id, "perm-abc123");
1143 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1144 assert_eq!(perm_req.tool_name, "Write");
1145 assert_eq!(
1146 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1147 "/home/user/hello.py"
1148 );
1149 } else {
1150 panic!("Expected CanUseTool payload");
1151 }
1152 } else {
1153 panic!("Expected ControlRequest");
1154 }
1155 }
1156
1157 #[test]
1158 fn test_deserialize_control_request_edit_tool_real() {
1159 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"}}"#;
1161
1162 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1163 assert!(output.is_control_request());
1164 assert_eq!(output.message_type(), "control_request");
1165
1166 if let ClaudeOutput::ControlRequest(req) = output {
1167 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1168 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1169 assert_eq!(perm_req.tool_name, "Edit");
1170 assert_eq!(
1171 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1172 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1173 );
1174 assert!(perm_req.input.get("old_string").is_some());
1175 assert!(perm_req.input.get("new_string").is_some());
1176 assert!(!perm_req
1177 .input
1178 .get("replace_all")
1179 .unwrap()
1180 .as_bool()
1181 .unwrap());
1182 } else {
1183 panic!("Expected CanUseTool payload");
1184 }
1185 } else {
1186 panic!("Expected ControlRequest");
1187 }
1188 }
1189
1190 #[test]
1191 fn test_tool_permission_request_allow() {
1192 let req = ToolPermissionRequest {
1193 tool_name: "Read".to_string(),
1194 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1195 permission_suggestions: vec![],
1196 blocked_path: None,
1197 decision_reason: None,
1198 tool_use_id: None,
1199 };
1200
1201 let response = req.allow("req-123");
1202 let message: ControlResponseMessage = response.into();
1203
1204 let json = serde_json::to_string(&message).unwrap();
1205 assert!(json.contains("\"type\":\"control_response\""));
1206 assert!(json.contains("\"subtype\":\"success\""));
1207 assert!(json.contains("\"request_id\":\"req-123\""));
1208 assert!(json.contains("\"behavior\":\"allow\""));
1209 assert!(json.contains("\"updatedInput\""));
1210 }
1211
1212 #[test]
1213 fn test_tool_permission_request_allow_with_modified_input() {
1214 let req = ToolPermissionRequest {
1215 tool_name: "Write".to_string(),
1216 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1217 permission_suggestions: vec![],
1218 blocked_path: None,
1219 decision_reason: None,
1220 tool_use_id: None,
1221 };
1222
1223 let modified_input = serde_json::json!({
1224 "file_path": "/tmp/safe/passwd",
1225 "content": "test"
1226 });
1227 let response = req.allow_with(modified_input, "req-456");
1228 let message: ControlResponseMessage = response.into();
1229
1230 let json = serde_json::to_string(&message).unwrap();
1231 assert!(json.contains("/tmp/safe/passwd"));
1232 assert!(!json.contains("/etc/passwd"));
1233 }
1234
1235 #[test]
1236 fn test_tool_permission_request_deny() {
1237 let req = ToolPermissionRequest {
1238 tool_name: "Bash".to_string(),
1239 input: serde_json::json!({"command": "sudo rm -rf /"}),
1240 permission_suggestions: vec![],
1241 blocked_path: None,
1242 decision_reason: None,
1243 tool_use_id: None,
1244 };
1245
1246 let response = req.deny("Dangerous command blocked", "req-789");
1247 let message: ControlResponseMessage = response.into();
1248
1249 let json = serde_json::to_string(&message).unwrap();
1250 assert!(json.contains("\"behavior\":\"deny\""));
1251 assert!(json.contains("Dangerous command blocked"));
1252 assert!(!json.contains("\"interrupt\":true"));
1253 }
1254
1255 #[test]
1256 fn test_tool_permission_request_deny_and_stop() {
1257 let req = ToolPermissionRequest {
1258 tool_name: "Bash".to_string(),
1259 input: serde_json::json!({"command": "rm -rf /"}),
1260 permission_suggestions: vec![],
1261 blocked_path: None,
1262 decision_reason: None,
1263 tool_use_id: None,
1264 };
1265
1266 let response = req.deny_and_stop("Security violation", "req-000");
1267 let message: ControlResponseMessage = response.into();
1268
1269 let json = serde_json::to_string(&message).unwrap();
1270 assert!(json.contains("\"behavior\":\"deny\""));
1271 assert!(json.contains("\"interrupt\":true"));
1272 }
1273
1274 #[test]
1275 fn test_permission_result_serialization() {
1276 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1278 let json = serde_json::to_string(&allow).unwrap();
1279 assert!(json.contains("\"behavior\":\"allow\""));
1280 assert!(json.contains("\"updatedInput\""));
1281
1282 let deny = PermissionResult::deny("Not allowed");
1284 let json = serde_json::to_string(&deny).unwrap();
1285 assert!(json.contains("\"behavior\":\"deny\""));
1286 assert!(json.contains("\"message\":\"Not allowed\""));
1287 assert!(!json.contains("\"interrupt\""));
1288
1289 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1291 let json = serde_json::to_string(&deny_stop).unwrap();
1292 assert!(json.contains("\"interrupt\":true"));
1293 }
1294
1295 #[test]
1296 fn test_control_request_message_initialize() {
1297 let init = ControlRequestMessage::initialize("init-1");
1298
1299 let json = serde_json::to_string(&init).unwrap();
1300 assert!(json.contains("\"type\":\"control_request\""));
1301 assert!(json.contains("\"request_id\":\"init-1\""));
1302 assert!(json.contains("\"subtype\":\"initialize\""));
1303 }
1304
1305 #[test]
1306 fn test_control_request_message_interrupt_wire_shape() {
1307 let msg = ControlRequestMessage::interrupt("interrupt-1");
1310 assert_eq!(
1311 serde_json::to_value(&msg).unwrap(),
1312 serde_json::json!({
1313 "type": "control_request",
1314 "request_id": "interrupt-1",
1315 "request": {"subtype": "interrupt"}
1316 })
1317 );
1318 }
1319
1320 #[test]
1321 fn test_interrupt_payload_roundtrip() {
1322 let json = r#"{"subtype":"interrupt"}"#;
1323 let payload: ControlRequestPayload = serde_json::from_str(json).unwrap();
1324 assert!(matches!(payload, ControlRequestPayload::Interrupt));
1325 assert_eq!(serde_json::to_string(&payload).unwrap(), json);
1326 }
1327
1328 #[test]
1329 fn test_control_response_error() {
1330 let response = ControlResponse::error("req-err", "Something went wrong");
1331 let message: ControlResponseMessage = response.into();
1332
1333 let json = serde_json::to_string(&message).unwrap();
1334 assert!(json.contains("\"subtype\":\"error\""));
1335 assert!(json.contains("\"error\":\"Something went wrong\""));
1336 }
1337
1338 #[test]
1339 fn test_get_usage_response_parses_model_scoped_limits() {
1340 let response = ControlResponse::success(
1341 "usage-1",
1342 serde_json::json!({
1343 "session": {
1344 "total_cost_usd": 0.5,
1345 "total_api_duration_ms": 100,
1346 "total_duration_ms": 200,
1347 "total_lines_added": 3,
1348 "total_lines_removed": 1,
1349 "model_usage": {
1350 "claude-sonnet-4-6": {
1351 "inputTokens": 10,
1352 "outputTokens": 2,
1353 "cacheReadInputTokens": 1,
1354 "cacheCreationInputTokens": 0,
1355 "webSearchRequests": 0,
1356 "costUSD": 0.01,
1357 "contextWindow": 200000,
1358 "maxOutputTokens": 64000
1359 }
1360 }
1361 },
1362 "subscription_type": "pro",
1363 "rate_limits_available": true,
1364 "rate_limits": {
1365 "five_hour": {"utilization": 0.2, "resets_at": "2026-07-09T22:00:00Z"},
1366 "model_scoped": [
1367 {"display_name": "Sonnet", "utilization": 0.4, "resets_at": "2026-07-16T00:00:00Z"}
1368 ]
1369 },
1370 "behaviors": {
1371 "request_count": 1,
1372 "session_count": 1,
1373 "behaviors": [{"key": "cron", "pct": 100.0, "count": 1}],
1374 "agents": [],
1375 "skills": [],
1376 "plugins": [],
1377 "mcp_servers": []
1378 }
1379 }),
1380 );
1381
1382 let usage = response.get_usage_response().unwrap().unwrap();
1383 assert_eq!(usage.subscription_type.as_deref(), Some("pro"));
1384 let model = usage.session.model_usage.get("claude-sonnet-4-6").unwrap();
1385 assert_eq!(model.context_window, 200000);
1386 assert_eq!(model.max_output_tokens, 64000);
1387 let limits = usage.rate_limits.unwrap();
1388 assert_eq!(limits.five_hour.unwrap().utilization, Some(0.2));
1389 assert_eq!(limits.model_scoped.unwrap()[0].display_name, "Sonnet");
1390 }
1391
1392 #[test]
1393 fn test_roundtrip_control_request() {
1394 let original_json = r#"{
1395 "type": "control_request",
1396 "request_id": "test-123",
1397 "request": {
1398 "subtype": "can_use_tool",
1399 "tool_name": "Bash",
1400 "input": {"command": "ls -la"},
1401 "permission_suggestions": []
1402 }
1403 }"#;
1404
1405 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1406
1407 let reserialized = serde_json::to_string(&output).unwrap();
1408 assert!(reserialized.contains("control_request"));
1409 assert!(reserialized.contains("test-123"));
1410 assert!(reserialized.contains("Bash"));
1411 }
1412
1413 #[test]
1414 fn test_permission_suggestions_parsing() {
1415 let json = r#"{
1416 "type": "control_request",
1417 "request_id": "perm-456",
1418 "request": {
1419 "subtype": "can_use_tool",
1420 "tool_name": "Bash",
1421 "input": {"command": "npm test"},
1422 "permission_suggestions": [
1423 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1424 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1425 ]
1426 }
1427 }"#;
1428
1429 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1430 if let ClaudeOutput::ControlRequest(req) = output {
1431 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1432 assert_eq!(perm_req.permission_suggestions.len(), 2);
1433 assert_eq!(
1434 perm_req.permission_suggestions[0].suggestion_type,
1435 PermissionType::SetMode
1436 );
1437 assert_eq!(
1438 perm_req.permission_suggestions[0].mode,
1439 Some(PermissionModeName::AcceptEdits)
1440 );
1441 assert_eq!(
1442 perm_req.permission_suggestions[0].destination,
1443 PermissionDestination::Session
1444 );
1445 assert_eq!(
1446 perm_req.permission_suggestions[1].suggestion_type,
1447 PermissionType::SetMode
1448 );
1449 assert_eq!(
1450 perm_req.permission_suggestions[1].mode,
1451 Some(PermissionModeName::BypassPermissions)
1452 );
1453 assert_eq!(
1454 perm_req.permission_suggestions[1].destination,
1455 PermissionDestination::Project
1456 );
1457 } else {
1458 panic!("Expected CanUseTool payload");
1459 }
1460 } else {
1461 panic!("Expected ControlRequest");
1462 }
1463 }
1464
1465 #[test]
1466 fn test_permission_suggestion_set_mode_roundtrip() {
1467 let suggestion = PermissionSuggestion {
1468 suggestion_type: PermissionType::SetMode,
1469 destination: PermissionDestination::Session,
1470 mode: Some(PermissionModeName::AcceptEdits),
1471 behavior: None,
1472 rules: None,
1473 };
1474
1475 let json = serde_json::to_string(&suggestion).unwrap();
1476 assert!(json.contains("\"type\":\"setMode\""));
1477 assert!(json.contains("\"mode\":\"acceptEdits\""));
1478 assert!(json.contains("\"destination\":\"session\""));
1479 assert!(!json.contains("\"behavior\""));
1480 assert!(!json.contains("\"rules\""));
1481
1482 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1483 assert_eq!(parsed, suggestion);
1484 }
1485
1486 #[test]
1487 fn test_permission_suggestion_add_rules_roundtrip() {
1488 let suggestion = PermissionSuggestion {
1489 suggestion_type: PermissionType::AddRules,
1490 destination: PermissionDestination::Session,
1491 mode: None,
1492 behavior: Some(PermissionBehavior::Allow),
1493 rules: Some(vec![serde_json::json!({
1494 "toolName": "Read",
1495 "ruleContent": "//tmp/**"
1496 })]),
1497 };
1498
1499 let json = serde_json::to_string(&suggestion).unwrap();
1500 assert!(json.contains("\"type\":\"addRules\""));
1501 assert!(json.contains("\"behavior\":\"allow\""));
1502 assert!(json.contains("\"destination\":\"session\""));
1503 assert!(json.contains("\"rules\""));
1504 assert!(json.contains("\"toolName\":\"Read\""));
1505 assert!(!json.contains("\"mode\""));
1506
1507 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1508 assert_eq!(parsed, suggestion);
1509 }
1510
1511 #[test]
1512 fn test_permission_suggestion_add_rules_from_real_json() {
1513 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1514
1515 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1516 assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1517 assert_eq!(parsed.destination, PermissionDestination::Session);
1518 assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1519 assert!(parsed.rules.is_some());
1520 assert!(parsed.mode.is_none());
1521 }
1522
1523 #[test]
1524 fn test_permission_allow_tool() {
1525 let perm = Permission::allow_tool("Bash", "npm test");
1526
1527 assert_eq!(perm.permission_type, PermissionType::AddRules);
1528 assert_eq!(perm.destination, PermissionDestination::Session);
1529 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1530 assert!(perm.mode.is_none());
1531
1532 let rules = perm.rules.unwrap();
1533 assert_eq!(rules.len(), 1);
1534 assert_eq!(rules[0].tool_name, "Bash");
1535 assert_eq!(rules[0].rule_content, "npm test");
1536 }
1537
1538 #[test]
1539 fn test_permission_allow_tool_with_destination() {
1540 let perm = Permission::allow_tool_with_destination(
1541 "Read",
1542 "/tmp/**",
1543 PermissionDestination::Project,
1544 );
1545
1546 assert_eq!(perm.permission_type, PermissionType::AddRules);
1547 assert_eq!(perm.destination, PermissionDestination::Project);
1548 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1549
1550 let rules = perm.rules.unwrap();
1551 assert_eq!(rules[0].tool_name, "Read");
1552 assert_eq!(rules[0].rule_content, "/tmp/**");
1553 }
1554
1555 #[test]
1556 fn test_permission_set_mode() {
1557 let perm = Permission::set_mode(
1558 PermissionModeName::AcceptEdits,
1559 PermissionDestination::Session,
1560 );
1561
1562 assert_eq!(perm.permission_type, PermissionType::SetMode);
1563 assert_eq!(perm.destination, PermissionDestination::Session);
1564 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1565 assert!(perm.behavior.is_none());
1566 assert!(perm.rules.is_none());
1567 }
1568
1569 #[test]
1570 fn test_permission_serialization() {
1571 let perm = Permission::allow_tool("Bash", "npm test");
1572 let json = serde_json::to_string(&perm).unwrap();
1573
1574 assert!(json.contains("\"type\":\"addRules\""));
1575 assert!(json.contains("\"destination\":\"session\""));
1576 assert!(json.contains("\"behavior\":\"allow\""));
1577 assert!(json.contains("\"toolName\":\"Bash\""));
1578 assert!(json.contains("\"ruleContent\":\"npm test\""));
1579 }
1580
1581 #[test]
1582 fn test_permission_from_suggestion_set_mode() {
1583 let suggestion = PermissionSuggestion {
1584 suggestion_type: PermissionType::SetMode,
1585 destination: PermissionDestination::Session,
1586 mode: Some(PermissionModeName::AcceptEdits),
1587 behavior: None,
1588 rules: None,
1589 };
1590
1591 let perm = Permission::from_suggestion(&suggestion);
1592
1593 assert_eq!(perm.permission_type, PermissionType::SetMode);
1594 assert_eq!(perm.destination, PermissionDestination::Session);
1595 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1596 }
1597
1598 #[test]
1599 fn test_permission_from_suggestion_add_rules() {
1600 let suggestion = PermissionSuggestion {
1601 suggestion_type: PermissionType::AddRules,
1602 destination: PermissionDestination::Session,
1603 mode: None,
1604 behavior: Some(PermissionBehavior::Allow),
1605 rules: Some(vec![serde_json::json!({
1606 "toolName": "Read",
1607 "ruleContent": "/tmp/**"
1608 })]),
1609 };
1610
1611 let perm = Permission::from_suggestion(&suggestion);
1612
1613 assert_eq!(perm.permission_type, PermissionType::AddRules);
1614 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1615
1616 let rules = perm.rules.unwrap();
1617 assert_eq!(rules.len(), 1);
1618 assert_eq!(rules[0].tool_name, "Read");
1619 assert_eq!(rules[0].rule_content, "/tmp/**");
1620 }
1621
1622 #[test]
1623 fn test_permission_result_allow_with_typed_permissions() {
1624 let result = PermissionResult::allow_with_typed_permissions(
1625 serde_json::json!({"command": "npm test"}),
1626 vec![Permission::allow_tool("Bash", "npm test")],
1627 );
1628
1629 let json = serde_json::to_string(&result).unwrap();
1630 assert!(json.contains("\"behavior\":\"allow\""));
1631 assert!(json.contains("\"updatedPermissions\""));
1632 assert!(json.contains("\"toolName\":\"Bash\""));
1633 }
1634
1635 #[test]
1636 fn test_tool_permission_request_allow_and_remember() {
1637 let req = ToolPermissionRequest {
1638 tool_name: "Bash".to_string(),
1639 input: serde_json::json!({"command": "npm test"}),
1640 permission_suggestions: vec![],
1641 blocked_path: None,
1642 decision_reason: None,
1643 tool_use_id: None,
1644 };
1645
1646 let response =
1647 req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1648 let message: ControlResponseMessage = response.into();
1649 let json = serde_json::to_string(&message).unwrap();
1650
1651 assert!(json.contains("\"type\":\"control_response\""));
1652 assert!(json.contains("\"behavior\":\"allow\""));
1653 assert!(json.contains("\"updatedPermissions\""));
1654 assert!(json.contains("\"toolName\":\"Bash\""));
1655 }
1656
1657 #[test]
1658 fn test_tool_permission_request_allow_and_remember_suggestion() {
1659 let req = ToolPermissionRequest {
1660 tool_name: "Bash".to_string(),
1661 input: serde_json::json!({"command": "npm test"}),
1662 permission_suggestions: vec![PermissionSuggestion {
1663 suggestion_type: PermissionType::SetMode,
1664 destination: PermissionDestination::Session,
1665 mode: Some(PermissionModeName::AcceptEdits),
1666 behavior: None,
1667 rules: None,
1668 }],
1669 blocked_path: None,
1670 decision_reason: None,
1671 tool_use_id: None,
1672 };
1673
1674 let response = req.allow_and_remember_suggestion("req-123");
1675 assert!(response.is_some());
1676
1677 let message: ControlResponseMessage = response.unwrap().into();
1678 let json = serde_json::to_string(&message).unwrap();
1679
1680 assert!(json.contains("\"type\":\"setMode\""));
1681 assert!(json.contains("\"mode\":\"acceptEdits\""));
1682 }
1683
1684 #[test]
1685 fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1686 let req = ToolPermissionRequest {
1687 tool_name: "Bash".to_string(),
1688 input: serde_json::json!({"command": "npm test"}),
1689 permission_suggestions: vec![], blocked_path: None,
1691 decision_reason: None,
1692 tool_use_id: None,
1693 };
1694
1695 let response = req.allow_and_remember_suggestion("req-123");
1696 assert!(response.is_none());
1697 }
1698
1699 fn ask_user_question_request() -> ToolPermissionRequest {
1700 ToolPermissionRequest {
1701 tool_name: "AskUserQuestion".to_string(),
1702 input: serde_json::json!({
1703 "questions": [
1704 {
1705 "question": "Which color do you prefer?",
1706 "header": "Color",
1707 "options": [
1708 {"label": "Red", "description": "warm"},
1709 {"label": "Blue", "description": "cool"}
1710 ],
1711 "multiSelect": false
1712 },
1713 {
1714 "question": "Pick a size",
1715 "header": "Size",
1716 "options": [
1717 {"label": "Small"},
1718 {"label": "Large"}
1719 ],
1720 "multiSelect": false
1721 }
1722 ]
1723 }),
1724 permission_suggestions: vec![],
1725 blocked_path: None,
1726 decision_reason: None,
1727 tool_use_id: None,
1728 }
1729 }
1730
1731 fn extract_updated_input(resp: &ControlResponse) -> Value {
1732 let ControlResponsePayload::Success { response, .. } = &resp.response else {
1733 panic!("expected Success payload");
1734 };
1735 let inner = response.as_ref().expect("response body present");
1736 inner
1737 .get("updatedInput")
1738 .cloned()
1739 .expect("updatedInput field present")
1740 }
1741
1742 #[test]
1743 fn answer_questions_keys_by_question_text_and_preserves_questions() {
1744 let req = ask_user_question_request();
1745 let mut answers = HashMap::new();
1746 answers.insert(0, "Blue".to_string());
1747 answers.insert(1, "Large".to_string());
1748
1749 let resp = req.answer_questions(&answers, "rid-1").unwrap();
1750 let updated = extract_updated_input(&resp);
1751
1752 assert_eq!(
1754 updated["questions"], req.input["questions"],
1755 "questions array must round-trip unchanged"
1756 );
1757 assert_eq!(
1759 updated["answers"]["Which color do you prefer?"],
1760 Value::String("Blue".into())
1761 );
1762 assert_eq!(
1763 updated["answers"]["Pick a size"],
1764 Value::String("Large".into())
1765 );
1766 assert!(updated["answers"].get("Color").is_none());
1768 assert!(updated["answers"].get("Size").is_none());
1769 }
1770
1771 #[test]
1772 fn answer_questions_partial_answers_omits_unanswered() {
1773 let req = ask_user_question_request();
1774 let mut answers = HashMap::new();
1775 answers.insert(1, "Small".to_string());
1776
1777 let resp = req.answer_questions(&answers, "rid-2").unwrap();
1778 let updated = extract_updated_input(&resp);
1779
1780 assert_eq!(
1781 updated["answers"]["Pick a size"],
1782 Value::String("Small".into())
1783 );
1784 assert!(updated["answers"]
1785 .get("Which color do you prefer?")
1786 .is_none());
1787 }
1788
1789 #[test]
1790 fn answer_questions_rejects_wrong_tool() {
1791 let mut req = ask_user_question_request();
1792 req.tool_name = "Bash".to_string();
1793
1794 let mut answers = HashMap::new();
1795 answers.insert(0, "Blue".to_string());
1796 match req.answer_questions(&answers, "rid-3").unwrap_err() {
1797 AskUserQuestionResponseError::WrongTool(name) => assert_eq!(name, "Bash"),
1798 other => panic!("expected WrongTool, got {other:?}"),
1799 }
1800 }
1801
1802 #[test]
1803 fn answer_questions_rejects_unparseable_input() {
1804 let req = ToolPermissionRequest {
1805 tool_name: "AskUserQuestion".to_string(),
1806 input: serde_json::json!({"not_questions": "garbage"}),
1807 permission_suggestions: vec![],
1808 blocked_path: None,
1809 decision_reason: None,
1810 tool_use_id: None,
1811 };
1812
1813 let answers = HashMap::new();
1814 match req.answer_questions(&answers, "rid-4").unwrap_err() {
1815 AskUserQuestionResponseError::ParseInput(_) => {}
1816 other => panic!("expected ParseInput, got {other:?}"),
1817 }
1818 }
1819
1820 #[test]
1821 fn answer_questions_rejects_out_of_range_index() {
1822 let req = ask_user_question_request();
1823 let mut answers = HashMap::new();
1824 answers.insert(7, "ghost".to_string());
1825
1826 match req.answer_questions(&answers, "rid-5").unwrap_err() {
1827 AskUserQuestionResponseError::QuestionIndexOutOfRange { index, total } => {
1828 assert_eq!(index, 7);
1829 assert_eq!(total, 2);
1830 }
1831 other => panic!("expected QuestionIndexOutOfRange, got {other:?}"),
1832 }
1833 }
1834}