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}
245
246#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
263pub struct Permission {
264 #[serde(rename = "type")]
266 pub permission_type: PermissionType,
267 pub destination: PermissionDestination,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub mode: Option<PermissionModeName>,
272 #[serde(skip_serializing_if = "Option::is_none")]
274 pub behavior: Option<PermissionBehavior>,
275 #[serde(skip_serializing_if = "Option::is_none")]
277 pub rules: Option<Vec<PermissionRule>>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
282pub struct PermissionRule {
283 #[serde(rename = "toolName")]
285 pub tool_name: String,
286 #[serde(rename = "ruleContent")]
288 pub rule_content: String,
289}
290
291impl Permission {
292 pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
305 Permission {
306 permission_type: PermissionType::AddRules,
307 destination: PermissionDestination::Session,
308 mode: None,
309 behavior: Some(PermissionBehavior::Allow),
310 rules: Some(vec![PermissionRule {
311 tool_name: tool_name.into(),
312 rule_content: rule_content.into(),
313 }]),
314 }
315 }
316
317 pub fn allow_tool_with_destination(
327 tool_name: impl Into<String>,
328 rule_content: impl Into<String>,
329 destination: PermissionDestination,
330 ) -> Self {
331 Permission {
332 permission_type: PermissionType::AddRules,
333 destination,
334 mode: None,
335 behavior: Some(PermissionBehavior::Allow),
336 rules: Some(vec![PermissionRule {
337 tool_name: tool_name.into(),
338 rule_content: rule_content.into(),
339 }]),
340 }
341 }
342
343 pub fn set_mode(mode: PermissionModeName, destination: PermissionDestination) -> Self {
353 Permission {
354 permission_type: PermissionType::SetMode,
355 destination,
356 mode: Some(mode),
357 behavior: None,
358 rules: None,
359 }
360 }
361
362 pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
381 Permission {
382 permission_type: suggestion.suggestion_type.clone(),
383 destination: suggestion.destination.clone(),
384 mode: suggestion.mode.clone(),
385 behavior: suggestion.behavior.clone(),
386 rules: suggestion.rules.as_ref().map(|rules| {
387 rules
388 .iter()
389 .filter_map(|v| {
390 Some(PermissionRule {
391 tool_name: v.get("toolName")?.as_str()?.to_string(),
392 rule_content: v.get("ruleContent")?.as_str()?.to_string(),
393 })
394 })
395 .collect()
396 }),
397 }
398 }
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
412pub struct PermissionSuggestion {
413 #[serde(rename = "type")]
415 pub suggestion_type: PermissionType,
416 pub destination: PermissionDestination,
418 #[serde(skip_serializing_if = "Option::is_none")]
420 pub mode: Option<PermissionModeName>,
421 #[serde(skip_serializing_if = "Option::is_none")]
423 pub behavior: Option<PermissionBehavior>,
424 #[serde(skip_serializing_if = "Option::is_none")]
426 pub rules: Option<Vec<Value>>,
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct ToolPermissionRequest {
456 pub tool_name: String,
458 pub input: Value,
460 #[serde(default)]
462 pub permission_suggestions: Vec<PermissionSuggestion>,
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub blocked_path: Option<String>,
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub decision_reason: Option<String>,
469 #[serde(skip_serializing_if = "Option::is_none")]
471 pub tool_use_id: Option<String>,
472}
473
474impl ToolPermissionRequest {
475 pub fn allow(&self, request_id: &str) -> ControlResponse {
492 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
493 }
494
495 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
517 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
518 }
519
520 pub fn allow_with_permissions(
524 &self,
525 modified_input: Value,
526 permissions: Vec<Value>,
527 request_id: &str,
528 ) -> ControlResponse {
529 ControlResponse::from_result(
530 request_id,
531 PermissionResult::allow_with_permissions(modified_input, permissions),
532 )
533 }
534
535 pub fn allow_and_remember(
561 &self,
562 permissions: Vec<Permission>,
563 request_id: &str,
564 ) -> ControlResponse {
565 ControlResponse::from_result(
566 request_id,
567 PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
568 )
569 }
570
571 pub fn allow_with_and_remember(
575 &self,
576 modified_input: Value,
577 permissions: Vec<Permission>,
578 request_id: &str,
579 ) -> ControlResponse {
580 ControlResponse::from_result(
581 request_id,
582 PermissionResult::allow_with_typed_permissions(modified_input, permissions),
583 )
584 }
585
586 pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
612 self.permission_suggestions.first().map(|suggestion| {
613 let perm = Permission::from_suggestion(suggestion);
614 self.allow_and_remember(vec![perm], request_id)
615 })
616 }
617
618 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
637 ControlResponse::from_result(request_id, PermissionResult::deny(message))
638 }
639
640 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
644 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
645 }
646
647 pub fn answer_questions(
701 &self,
702 answers_by_index: &HashMap<usize, String>,
703 request_id: &str,
704 ) -> Result<ControlResponse, AskUserQuestionResponseError> {
705 if self.tool_name != "AskUserQuestion" {
706 return Err(AskUserQuestionResponseError::WrongTool(
707 self.tool_name.clone(),
708 ));
709 }
710 let parsed: AskUserQuestionInput = serde_json::from_value(self.input.clone())
711 .map_err(AskUserQuestionResponseError::ParseInput)?;
712 let total = parsed.questions.len();
713
714 let mut answers_map = serde_json::Map::new();
715 for (idx, answer) in answers_by_index {
716 let q = parsed.questions.get(*idx).ok_or(
717 AskUserQuestionResponseError::QuestionIndexOutOfRange { index: *idx, total },
718 )?;
719 answers_map.insert(q.question.clone(), Value::String(answer.clone()));
720 }
721
722 let mut updated_input = self.input.clone();
723 updated_input
725 .as_object_mut()
726 .expect("AskUserQuestion input is a JSON object")
727 .insert("answers".to_string(), Value::Object(answers_map));
728
729 Ok(ControlResponse::from_result(
730 request_id,
731 PermissionResult::allow(updated_input),
732 ))
733 }
734}
735
736#[derive(Debug, thiserror::Error)]
739pub enum AskUserQuestionResponseError {
740 #[error("expected tool_name=AskUserQuestion, got `{0}`")]
742 WrongTool(String),
743 #[error("failed to parse AskUserQuestion input: {0}")]
745 ParseInput(#[source] serde_json::Error),
746 #[error("answer references question index {index}, but only {total} question(s) were asked")]
748 QuestionIndexOutOfRange {
749 index: usize,
751 total: usize,
753 },
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize)]
761#[serde(tag = "behavior", rename_all = "snake_case")]
762pub enum PermissionResult {
763 Allow {
765 #[serde(rename = "updatedInput")]
767 updated_input: Value,
768 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
770 updated_permissions: Option<Vec<Value>>,
771 },
772 Deny {
774 message: String,
776 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
778 interrupt: bool,
779 },
780}
781
782impl PermissionResult {
783 pub fn allow(input: Value) -> Self {
785 PermissionResult::Allow {
786 updated_input: input,
787 updated_permissions: None,
788 }
789 }
790
791 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
795 PermissionResult::Allow {
796 updated_input: input,
797 updated_permissions: Some(permissions),
798 }
799 }
800
801 pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
817 let permission_values: Vec<Value> = permissions
818 .into_iter()
819 .filter_map(|p| serde_json::to_value(p).ok())
820 .collect();
821 PermissionResult::Allow {
822 updated_input: input,
823 updated_permissions: Some(permission_values),
824 }
825 }
826
827 pub fn deny(message: impl Into<String>) -> Self {
829 PermissionResult::Deny {
830 message: message.into(),
831 interrupt: false,
832 }
833 }
834
835 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
837 PermissionResult::Deny {
838 message: message.into(),
839 interrupt: true,
840 }
841 }
842}
843
844#[derive(Debug, Clone, Serialize, Deserialize)]
846pub struct HookCallbackRequest {
847 pub callback_id: String,
848 pub input: Value,
849 #[serde(skip_serializing_if = "Option::is_none")]
850 pub tool_use_id: Option<String>,
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize)]
855pub struct McpMessageRequest {
856 pub server_name: String,
857 pub message: Value,
858}
859
860#[derive(Debug, Clone, Serialize, Deserialize)]
862pub struct InitializeRequest {
863 #[serde(skip_serializing_if = "Option::is_none")]
864 pub hooks: Option<Value>,
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize)]
872pub struct ControlResponse {
873 pub response: ControlResponsePayload,
875}
876
877impl ControlResponse {
878 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
882 let response_value = serde_json::to_value(&result)
884 .expect("PermissionResult serialization should never fail");
885 ControlResponse {
886 response: ControlResponsePayload::Success {
887 request_id: request_id.to_string(),
888 response: Some(response_value),
889 },
890 }
891 }
892
893 pub fn success(request_id: &str, response_data: Value) -> Self {
895 ControlResponse {
896 response: ControlResponsePayload::Success {
897 request_id: request_id.to_string(),
898 response: Some(response_data),
899 },
900 }
901 }
902
903 pub fn success_empty(request_id: &str) -> Self {
905 ControlResponse {
906 response: ControlResponsePayload::Success {
907 request_id: request_id.to_string(),
908 response: None,
909 },
910 }
911 }
912
913 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
915 ControlResponse {
916 response: ControlResponsePayload::Error {
917 request_id: request_id.to_string(),
918 error: error_message.into(),
919 },
920 }
921 }
922
923 pub fn get_usage_response(&self) -> Option<Result<GetUsageResponse, serde_json::Error>> {
925 match &self.response {
926 ControlResponsePayload::Success {
927 response: Some(value),
928 ..
929 } => Some(serde_json::from_value(value.clone())),
930 _ => None,
931 }
932 }
933}
934
935#[derive(Debug, Clone, Serialize, Deserialize)]
937#[serde(tag = "subtype", rename_all = "snake_case")]
938pub enum ControlResponsePayload {
939 Success {
940 request_id: String,
941 #[serde(skip_serializing_if = "Option::is_none")]
942 response: Option<Value>,
943 },
944 Error {
945 request_id: String,
946 error: String,
947 },
948}
949
950#[derive(Debug, Clone, Serialize, Deserialize)]
952pub struct GetUsageResponse {
953 pub session: UsageSession,
954 pub subscription_type: Option<String>,
955 pub rate_limits_available: bool,
956 pub rate_limits: Option<UsageRateLimits>,
957 pub behaviors: UsageBehaviors,
958 #[serde(flatten)]
959 pub extra: serde_json::Map<String, Value>,
960}
961
962#[derive(Debug, Clone, Default, Serialize, Deserialize)]
963pub struct UsageSession {
964 pub total_cost_usd: f64,
965 pub total_api_duration_ms: u64,
966 pub total_duration_ms: u64,
967 pub total_lines_added: u64,
968 pub total_lines_removed: u64,
969 pub model_usage: HashMap<String, UsageModelUsage>,
970 #[serde(flatten)]
971 pub extra: serde_json::Map<String, Value>,
972}
973
974#[derive(Debug, Clone, Default, Serialize, Deserialize)]
975#[serde(rename_all = "camelCase")]
976pub struct UsageModelUsage {
977 #[serde(default)]
978 pub input_tokens: u64,
979 #[serde(default)]
980 pub output_tokens: u64,
981 #[serde(default)]
982 pub cache_read_input_tokens: u64,
983 #[serde(default)]
984 pub cache_creation_input_tokens: u64,
985 #[serde(default)]
986 pub web_search_requests: u64,
987 #[serde(default, rename = "costUSD")]
988 pub cost_usd: f64,
989 #[serde(default)]
990 pub context_window: u64,
991 #[serde(default)]
992 pub max_output_tokens: u64,
993 #[serde(flatten)]
994 pub extra: serde_json::Map<String, Value>,
995}
996
997#[derive(Debug, Clone, Default, Serialize, Deserialize)]
998pub struct UsageRateLimits {
999 #[serde(default, skip_serializing_if = "Option::is_none")]
1000 pub five_hour: Option<UsageRateLimitWindow>,
1001 #[serde(default, skip_serializing_if = "Option::is_none")]
1002 pub seven_day: Option<UsageRateLimitWindow>,
1003 #[serde(default, skip_serializing_if = "Option::is_none")]
1004 pub seven_day_oauth_apps: Option<UsageRateLimitWindow>,
1005 #[serde(default, skip_serializing_if = "Option::is_none")]
1006 pub seven_day_opus: Option<UsageRateLimitWindow>,
1007 #[serde(default, skip_serializing_if = "Option::is_none")]
1008 pub seven_day_sonnet: Option<UsageRateLimitWindow>,
1009 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub model_scoped: Option<Vec<ModelScopedRateLimit>>,
1011 #[serde(flatten)]
1012 pub extra: serde_json::Map<String, Value>,
1013}
1014
1015#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1016pub struct UsageRateLimitWindow {
1017 pub utilization: Option<f64>,
1018 pub resets_at: Option<String>,
1019 #[serde(flatten)]
1020 pub extra: serde_json::Map<String, Value>,
1021}
1022
1023#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1024pub struct ModelScopedRateLimit {
1025 pub display_name: String,
1026 pub utilization: Option<f64>,
1027 pub resets_at: Option<String>,
1028 #[serde(flatten)]
1029 pub extra: serde_json::Map<String, Value>,
1030}
1031
1032#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1033pub struct UsageBehaviors {
1034 pub request_count: u64,
1035 pub session_count: u64,
1036 pub behaviors: Vec<UsageBehavior>,
1037 pub agents: Value,
1038 pub skills: Value,
1039 pub plugins: Value,
1040 pub mcp_servers: Value,
1041 #[serde(flatten)]
1042 pub extra: serde_json::Map<String, Value>,
1043}
1044
1045#[derive(Debug, Clone, Serialize, Deserialize)]
1046pub struct UsageBehavior {
1047 pub key: String,
1048 pub pct: f64,
1049 pub count: u64,
1050 #[serde(flatten)]
1051 pub extra: serde_json::Map<String, Value>,
1052}
1053
1054#[derive(Debug, Clone, Serialize, Deserialize)]
1056pub struct ControlResponseMessage {
1057 #[serde(rename = "type")]
1058 pub message_type: String,
1059 pub response: ControlResponsePayload,
1060}
1061
1062impl From<ControlResponse> for ControlResponseMessage {
1063 fn from(resp: ControlResponse) -> Self {
1064 ControlResponseMessage {
1065 message_type: "control_response".to_string(),
1066 response: resp.response,
1067 }
1068 }
1069}
1070
1071#[derive(Debug, Clone, Serialize, Deserialize)]
1089pub struct SDKControlInterruptRequest {
1090 subtype: SDKControlInterruptSubtype,
1091}
1092
1093#[derive(Debug, Clone, Serialize, Deserialize)]
1094enum SDKControlInterruptSubtype {
1095 #[serde(rename = "interrupt")]
1096 Interrupt,
1097}
1098
1099impl SDKControlInterruptRequest {
1100 pub fn new() -> Self {
1102 SDKControlInterruptRequest {
1103 subtype: SDKControlInterruptSubtype::Interrupt,
1104 }
1105 }
1106}
1107
1108impl Default for SDKControlInterruptRequest {
1109 fn default() -> Self {
1110 Self::new()
1111 }
1112}
1113
1114#[derive(Debug, Clone, Serialize, Deserialize)]
1116pub struct ControlRequestMessage {
1117 #[serde(rename = "type")]
1118 pub message_type: String,
1119 pub request_id: String,
1120 pub request: ControlRequestPayload,
1121}
1122
1123impl ControlRequestMessage {
1124 pub fn initialize(request_id: impl Into<String>) -> Self {
1126 ControlRequestMessage {
1127 message_type: "control_request".to_string(),
1128 request_id: request_id.into(),
1129 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
1130 }
1131 }
1132
1133 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
1135 ControlRequestMessage {
1136 message_type: "control_request".to_string(),
1137 request_id: request_id.into(),
1138 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
1139 }
1140 }
1141}
1142
1143#[cfg(test)]
1144mod tests {
1145 use super::*;
1146 use crate::io::ClaudeOutput;
1147
1148 #[test]
1149 fn test_deserialize_control_request_can_use_tool() {
1150 let json = r#"{
1151 "type": "control_request",
1152 "request_id": "perm-abc123",
1153 "request": {
1154 "subtype": "can_use_tool",
1155 "tool_name": "Write",
1156 "input": {
1157 "file_path": "/home/user/hello.py",
1158 "content": "print('hello')"
1159 },
1160 "permission_suggestions": [],
1161 "blocked_path": null
1162 }
1163 }"#;
1164
1165 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1166 assert!(output.is_control_request());
1167
1168 if let ClaudeOutput::ControlRequest(req) = output {
1169 assert_eq!(req.request_id, "perm-abc123");
1170 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1171 assert_eq!(perm_req.tool_name, "Write");
1172 assert_eq!(
1173 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1174 "/home/user/hello.py"
1175 );
1176 } else {
1177 panic!("Expected CanUseTool payload");
1178 }
1179 } else {
1180 panic!("Expected ControlRequest");
1181 }
1182 }
1183
1184 #[test]
1185 fn test_deserialize_control_request_edit_tool_real() {
1186 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"}}"#;
1188
1189 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1190 assert!(output.is_control_request());
1191 assert_eq!(output.message_type(), "control_request");
1192
1193 if let ClaudeOutput::ControlRequest(req) = output {
1194 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1195 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1196 assert_eq!(perm_req.tool_name, "Edit");
1197 assert_eq!(
1198 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1199 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1200 );
1201 assert!(perm_req.input.get("old_string").is_some());
1202 assert!(perm_req.input.get("new_string").is_some());
1203 assert!(!perm_req
1204 .input
1205 .get("replace_all")
1206 .unwrap()
1207 .as_bool()
1208 .unwrap());
1209 } else {
1210 panic!("Expected CanUseTool payload");
1211 }
1212 } else {
1213 panic!("Expected ControlRequest");
1214 }
1215 }
1216
1217 #[test]
1218 fn test_tool_permission_request_allow() {
1219 let req = ToolPermissionRequest {
1220 tool_name: "Read".to_string(),
1221 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1222 permission_suggestions: vec![],
1223 blocked_path: None,
1224 decision_reason: None,
1225 tool_use_id: None,
1226 };
1227
1228 let response = req.allow("req-123");
1229 let message: ControlResponseMessage = response.into();
1230
1231 let json = serde_json::to_string(&message).unwrap();
1232 assert!(json.contains("\"type\":\"control_response\""));
1233 assert!(json.contains("\"subtype\":\"success\""));
1234 assert!(json.contains("\"request_id\":\"req-123\""));
1235 assert!(json.contains("\"behavior\":\"allow\""));
1236 assert!(json.contains("\"updatedInput\""));
1237 }
1238
1239 #[test]
1240 fn test_tool_permission_request_allow_with_modified_input() {
1241 let req = ToolPermissionRequest {
1242 tool_name: "Write".to_string(),
1243 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1244 permission_suggestions: vec![],
1245 blocked_path: None,
1246 decision_reason: None,
1247 tool_use_id: None,
1248 };
1249
1250 let modified_input = serde_json::json!({
1251 "file_path": "/tmp/safe/passwd",
1252 "content": "test"
1253 });
1254 let response = req.allow_with(modified_input, "req-456");
1255 let message: ControlResponseMessage = response.into();
1256
1257 let json = serde_json::to_string(&message).unwrap();
1258 assert!(json.contains("/tmp/safe/passwd"));
1259 assert!(!json.contains("/etc/passwd"));
1260 }
1261
1262 #[test]
1263 fn test_tool_permission_request_deny() {
1264 let req = ToolPermissionRequest {
1265 tool_name: "Bash".to_string(),
1266 input: serde_json::json!({"command": "sudo rm -rf /"}),
1267 permission_suggestions: vec![],
1268 blocked_path: None,
1269 decision_reason: None,
1270 tool_use_id: None,
1271 };
1272
1273 let response = req.deny("Dangerous command blocked", "req-789");
1274 let message: ControlResponseMessage = response.into();
1275
1276 let json = serde_json::to_string(&message).unwrap();
1277 assert!(json.contains("\"behavior\":\"deny\""));
1278 assert!(json.contains("Dangerous command blocked"));
1279 assert!(!json.contains("\"interrupt\":true"));
1280 }
1281
1282 #[test]
1283 fn test_tool_permission_request_deny_and_stop() {
1284 let req = ToolPermissionRequest {
1285 tool_name: "Bash".to_string(),
1286 input: serde_json::json!({"command": "rm -rf /"}),
1287 permission_suggestions: vec![],
1288 blocked_path: None,
1289 decision_reason: None,
1290 tool_use_id: None,
1291 };
1292
1293 let response = req.deny_and_stop("Security violation", "req-000");
1294 let message: ControlResponseMessage = response.into();
1295
1296 let json = serde_json::to_string(&message).unwrap();
1297 assert!(json.contains("\"behavior\":\"deny\""));
1298 assert!(json.contains("\"interrupt\":true"));
1299 }
1300
1301 #[test]
1302 fn test_permission_result_serialization() {
1303 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1305 let json = serde_json::to_string(&allow).unwrap();
1306 assert!(json.contains("\"behavior\":\"allow\""));
1307 assert!(json.contains("\"updatedInput\""));
1308
1309 let deny = PermissionResult::deny("Not allowed");
1311 let json = serde_json::to_string(&deny).unwrap();
1312 assert!(json.contains("\"behavior\":\"deny\""));
1313 assert!(json.contains("\"message\":\"Not allowed\""));
1314 assert!(!json.contains("\"interrupt\""));
1315
1316 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1318 let json = serde_json::to_string(&deny_stop).unwrap();
1319 assert!(json.contains("\"interrupt\":true"));
1320 }
1321
1322 #[test]
1323 fn test_control_request_message_initialize() {
1324 let init = ControlRequestMessage::initialize("init-1");
1325
1326 let json = serde_json::to_string(&init).unwrap();
1327 assert!(json.contains("\"type\":\"control_request\""));
1328 assert!(json.contains("\"request_id\":\"init-1\""));
1329 assert!(json.contains("\"subtype\":\"initialize\""));
1330 }
1331
1332 #[test]
1333 fn test_control_response_error() {
1334 let response = ControlResponse::error("req-err", "Something went wrong");
1335 let message: ControlResponseMessage = response.into();
1336
1337 let json = serde_json::to_string(&message).unwrap();
1338 assert!(json.contains("\"subtype\":\"error\""));
1339 assert!(json.contains("\"error\":\"Something went wrong\""));
1340 }
1341
1342 #[test]
1343 fn test_get_usage_response_parses_model_scoped_limits() {
1344 let response = ControlResponse::success(
1345 "usage-1",
1346 serde_json::json!({
1347 "session": {
1348 "total_cost_usd": 0.5,
1349 "total_api_duration_ms": 100,
1350 "total_duration_ms": 200,
1351 "total_lines_added": 3,
1352 "total_lines_removed": 1,
1353 "model_usage": {
1354 "claude-sonnet-4-6": {
1355 "inputTokens": 10,
1356 "outputTokens": 2,
1357 "cacheReadInputTokens": 1,
1358 "cacheCreationInputTokens": 0,
1359 "webSearchRequests": 0,
1360 "costUSD": 0.01,
1361 "contextWindow": 200000,
1362 "maxOutputTokens": 64000
1363 }
1364 }
1365 },
1366 "subscription_type": "pro",
1367 "rate_limits_available": true,
1368 "rate_limits": {
1369 "five_hour": {"utilization": 0.2, "resets_at": "2026-07-09T22:00:00Z"},
1370 "model_scoped": [
1371 {"display_name": "Sonnet", "utilization": 0.4, "resets_at": "2026-07-16T00:00:00Z"}
1372 ]
1373 },
1374 "behaviors": {
1375 "request_count": 1,
1376 "session_count": 1,
1377 "behaviors": [{"key": "cron", "pct": 100.0, "count": 1}],
1378 "agents": [],
1379 "skills": [],
1380 "plugins": [],
1381 "mcp_servers": []
1382 }
1383 }),
1384 );
1385
1386 let usage = response.get_usage_response().unwrap().unwrap();
1387 assert_eq!(usage.subscription_type.as_deref(), Some("pro"));
1388 let model = usage.session.model_usage.get("claude-sonnet-4-6").unwrap();
1389 assert_eq!(model.context_window, 200000);
1390 assert_eq!(model.max_output_tokens, 64000);
1391 let limits = usage.rate_limits.unwrap();
1392 assert_eq!(limits.five_hour.unwrap().utilization, Some(0.2));
1393 assert_eq!(limits.model_scoped.unwrap()[0].display_name, "Sonnet");
1394 }
1395
1396 #[test]
1397 fn test_roundtrip_control_request() {
1398 let original_json = r#"{
1399 "type": "control_request",
1400 "request_id": "test-123",
1401 "request": {
1402 "subtype": "can_use_tool",
1403 "tool_name": "Bash",
1404 "input": {"command": "ls -la"},
1405 "permission_suggestions": []
1406 }
1407 }"#;
1408
1409 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1410
1411 let reserialized = serde_json::to_string(&output).unwrap();
1412 assert!(reserialized.contains("control_request"));
1413 assert!(reserialized.contains("test-123"));
1414 assert!(reserialized.contains("Bash"));
1415 }
1416
1417 #[test]
1418 fn test_permission_suggestions_parsing() {
1419 let json = r#"{
1420 "type": "control_request",
1421 "request_id": "perm-456",
1422 "request": {
1423 "subtype": "can_use_tool",
1424 "tool_name": "Bash",
1425 "input": {"command": "npm test"},
1426 "permission_suggestions": [
1427 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1428 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1429 ]
1430 }
1431 }"#;
1432
1433 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1434 if let ClaudeOutput::ControlRequest(req) = output {
1435 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1436 assert_eq!(perm_req.permission_suggestions.len(), 2);
1437 assert_eq!(
1438 perm_req.permission_suggestions[0].suggestion_type,
1439 PermissionType::SetMode
1440 );
1441 assert_eq!(
1442 perm_req.permission_suggestions[0].mode,
1443 Some(PermissionModeName::AcceptEdits)
1444 );
1445 assert_eq!(
1446 perm_req.permission_suggestions[0].destination,
1447 PermissionDestination::Session
1448 );
1449 assert_eq!(
1450 perm_req.permission_suggestions[1].suggestion_type,
1451 PermissionType::SetMode
1452 );
1453 assert_eq!(
1454 perm_req.permission_suggestions[1].mode,
1455 Some(PermissionModeName::BypassPermissions)
1456 );
1457 assert_eq!(
1458 perm_req.permission_suggestions[1].destination,
1459 PermissionDestination::Project
1460 );
1461 } else {
1462 panic!("Expected CanUseTool payload");
1463 }
1464 } else {
1465 panic!("Expected ControlRequest");
1466 }
1467 }
1468
1469 #[test]
1470 fn test_permission_suggestion_set_mode_roundtrip() {
1471 let suggestion = PermissionSuggestion {
1472 suggestion_type: PermissionType::SetMode,
1473 destination: PermissionDestination::Session,
1474 mode: Some(PermissionModeName::AcceptEdits),
1475 behavior: None,
1476 rules: None,
1477 };
1478
1479 let json = serde_json::to_string(&suggestion).unwrap();
1480 assert!(json.contains("\"type\":\"setMode\""));
1481 assert!(json.contains("\"mode\":\"acceptEdits\""));
1482 assert!(json.contains("\"destination\":\"session\""));
1483 assert!(!json.contains("\"behavior\""));
1484 assert!(!json.contains("\"rules\""));
1485
1486 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1487 assert_eq!(parsed, suggestion);
1488 }
1489
1490 #[test]
1491 fn test_permission_suggestion_add_rules_roundtrip() {
1492 let suggestion = PermissionSuggestion {
1493 suggestion_type: PermissionType::AddRules,
1494 destination: PermissionDestination::Session,
1495 mode: None,
1496 behavior: Some(PermissionBehavior::Allow),
1497 rules: Some(vec![serde_json::json!({
1498 "toolName": "Read",
1499 "ruleContent": "//tmp/**"
1500 })]),
1501 };
1502
1503 let json = serde_json::to_string(&suggestion).unwrap();
1504 assert!(json.contains("\"type\":\"addRules\""));
1505 assert!(json.contains("\"behavior\":\"allow\""));
1506 assert!(json.contains("\"destination\":\"session\""));
1507 assert!(json.contains("\"rules\""));
1508 assert!(json.contains("\"toolName\":\"Read\""));
1509 assert!(!json.contains("\"mode\""));
1510
1511 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1512 assert_eq!(parsed, suggestion);
1513 }
1514
1515 #[test]
1516 fn test_permission_suggestion_add_rules_from_real_json() {
1517 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1518
1519 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1520 assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1521 assert_eq!(parsed.destination, PermissionDestination::Session);
1522 assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1523 assert!(parsed.rules.is_some());
1524 assert!(parsed.mode.is_none());
1525 }
1526
1527 #[test]
1528 fn test_permission_allow_tool() {
1529 let perm = Permission::allow_tool("Bash", "npm test");
1530
1531 assert_eq!(perm.permission_type, PermissionType::AddRules);
1532 assert_eq!(perm.destination, PermissionDestination::Session);
1533 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1534 assert!(perm.mode.is_none());
1535
1536 let rules = perm.rules.unwrap();
1537 assert_eq!(rules.len(), 1);
1538 assert_eq!(rules[0].tool_name, "Bash");
1539 assert_eq!(rules[0].rule_content, "npm test");
1540 }
1541
1542 #[test]
1543 fn test_permission_allow_tool_with_destination() {
1544 let perm = Permission::allow_tool_with_destination(
1545 "Read",
1546 "/tmp/**",
1547 PermissionDestination::Project,
1548 );
1549
1550 assert_eq!(perm.permission_type, PermissionType::AddRules);
1551 assert_eq!(perm.destination, PermissionDestination::Project);
1552 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1553
1554 let rules = perm.rules.unwrap();
1555 assert_eq!(rules[0].tool_name, "Read");
1556 assert_eq!(rules[0].rule_content, "/tmp/**");
1557 }
1558
1559 #[test]
1560 fn test_permission_set_mode() {
1561 let perm = Permission::set_mode(
1562 PermissionModeName::AcceptEdits,
1563 PermissionDestination::Session,
1564 );
1565
1566 assert_eq!(perm.permission_type, PermissionType::SetMode);
1567 assert_eq!(perm.destination, PermissionDestination::Session);
1568 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1569 assert!(perm.behavior.is_none());
1570 assert!(perm.rules.is_none());
1571 }
1572
1573 #[test]
1574 fn test_permission_serialization() {
1575 let perm = Permission::allow_tool("Bash", "npm test");
1576 let json = serde_json::to_string(&perm).unwrap();
1577
1578 assert!(json.contains("\"type\":\"addRules\""));
1579 assert!(json.contains("\"destination\":\"session\""));
1580 assert!(json.contains("\"behavior\":\"allow\""));
1581 assert!(json.contains("\"toolName\":\"Bash\""));
1582 assert!(json.contains("\"ruleContent\":\"npm test\""));
1583 }
1584
1585 #[test]
1586 fn test_permission_from_suggestion_set_mode() {
1587 let suggestion = PermissionSuggestion {
1588 suggestion_type: PermissionType::SetMode,
1589 destination: PermissionDestination::Session,
1590 mode: Some(PermissionModeName::AcceptEdits),
1591 behavior: None,
1592 rules: None,
1593 };
1594
1595 let perm = Permission::from_suggestion(&suggestion);
1596
1597 assert_eq!(perm.permission_type, PermissionType::SetMode);
1598 assert_eq!(perm.destination, PermissionDestination::Session);
1599 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1600 }
1601
1602 #[test]
1603 fn test_permission_from_suggestion_add_rules() {
1604 let suggestion = PermissionSuggestion {
1605 suggestion_type: PermissionType::AddRules,
1606 destination: PermissionDestination::Session,
1607 mode: None,
1608 behavior: Some(PermissionBehavior::Allow),
1609 rules: Some(vec![serde_json::json!({
1610 "toolName": "Read",
1611 "ruleContent": "/tmp/**"
1612 })]),
1613 };
1614
1615 let perm = Permission::from_suggestion(&suggestion);
1616
1617 assert_eq!(perm.permission_type, PermissionType::AddRules);
1618 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1619
1620 let rules = perm.rules.unwrap();
1621 assert_eq!(rules.len(), 1);
1622 assert_eq!(rules[0].tool_name, "Read");
1623 assert_eq!(rules[0].rule_content, "/tmp/**");
1624 }
1625
1626 #[test]
1627 fn test_permission_result_allow_with_typed_permissions() {
1628 let result = PermissionResult::allow_with_typed_permissions(
1629 serde_json::json!({"command": "npm test"}),
1630 vec![Permission::allow_tool("Bash", "npm test")],
1631 );
1632
1633 let json = serde_json::to_string(&result).unwrap();
1634 assert!(json.contains("\"behavior\":\"allow\""));
1635 assert!(json.contains("\"updatedPermissions\""));
1636 assert!(json.contains("\"toolName\":\"Bash\""));
1637 }
1638
1639 #[test]
1640 fn test_tool_permission_request_allow_and_remember() {
1641 let req = ToolPermissionRequest {
1642 tool_name: "Bash".to_string(),
1643 input: serde_json::json!({"command": "npm test"}),
1644 permission_suggestions: vec![],
1645 blocked_path: None,
1646 decision_reason: None,
1647 tool_use_id: None,
1648 };
1649
1650 let response =
1651 req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1652 let message: ControlResponseMessage = response.into();
1653 let json = serde_json::to_string(&message).unwrap();
1654
1655 assert!(json.contains("\"type\":\"control_response\""));
1656 assert!(json.contains("\"behavior\":\"allow\""));
1657 assert!(json.contains("\"updatedPermissions\""));
1658 assert!(json.contains("\"toolName\":\"Bash\""));
1659 }
1660
1661 #[test]
1662 fn test_tool_permission_request_allow_and_remember_suggestion() {
1663 let req = ToolPermissionRequest {
1664 tool_name: "Bash".to_string(),
1665 input: serde_json::json!({"command": "npm test"}),
1666 permission_suggestions: vec![PermissionSuggestion {
1667 suggestion_type: PermissionType::SetMode,
1668 destination: PermissionDestination::Session,
1669 mode: Some(PermissionModeName::AcceptEdits),
1670 behavior: None,
1671 rules: None,
1672 }],
1673 blocked_path: None,
1674 decision_reason: None,
1675 tool_use_id: None,
1676 };
1677
1678 let response = req.allow_and_remember_suggestion("req-123");
1679 assert!(response.is_some());
1680
1681 let message: ControlResponseMessage = response.unwrap().into();
1682 let json = serde_json::to_string(&message).unwrap();
1683
1684 assert!(json.contains("\"type\":\"setMode\""));
1685 assert!(json.contains("\"mode\":\"acceptEdits\""));
1686 }
1687
1688 #[test]
1689 fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1690 let req = ToolPermissionRequest {
1691 tool_name: "Bash".to_string(),
1692 input: serde_json::json!({"command": "npm test"}),
1693 permission_suggestions: vec![], blocked_path: None,
1695 decision_reason: None,
1696 tool_use_id: None,
1697 };
1698
1699 let response = req.allow_and_remember_suggestion("req-123");
1700 assert!(response.is_none());
1701 }
1702
1703 fn ask_user_question_request() -> ToolPermissionRequest {
1704 ToolPermissionRequest {
1705 tool_name: "AskUserQuestion".to_string(),
1706 input: serde_json::json!({
1707 "questions": [
1708 {
1709 "question": "Which color do you prefer?",
1710 "header": "Color",
1711 "options": [
1712 {"label": "Red", "description": "warm"},
1713 {"label": "Blue", "description": "cool"}
1714 ],
1715 "multiSelect": false
1716 },
1717 {
1718 "question": "Pick a size",
1719 "header": "Size",
1720 "options": [
1721 {"label": "Small"},
1722 {"label": "Large"}
1723 ],
1724 "multiSelect": false
1725 }
1726 ]
1727 }),
1728 permission_suggestions: vec![],
1729 blocked_path: None,
1730 decision_reason: None,
1731 tool_use_id: None,
1732 }
1733 }
1734
1735 fn extract_updated_input(resp: &ControlResponse) -> Value {
1736 let ControlResponsePayload::Success { response, .. } = &resp.response else {
1737 panic!("expected Success payload");
1738 };
1739 let inner = response.as_ref().expect("response body present");
1740 inner
1741 .get("updatedInput")
1742 .cloned()
1743 .expect("updatedInput field present")
1744 }
1745
1746 #[test]
1747 fn answer_questions_keys_by_question_text_and_preserves_questions() {
1748 let req = ask_user_question_request();
1749 let mut answers = HashMap::new();
1750 answers.insert(0, "Blue".to_string());
1751 answers.insert(1, "Large".to_string());
1752
1753 let resp = req.answer_questions(&answers, "rid-1").unwrap();
1754 let updated = extract_updated_input(&resp);
1755
1756 assert_eq!(
1758 updated["questions"], req.input["questions"],
1759 "questions array must round-trip unchanged"
1760 );
1761 assert_eq!(
1763 updated["answers"]["Which color do you prefer?"],
1764 Value::String("Blue".into())
1765 );
1766 assert_eq!(
1767 updated["answers"]["Pick a size"],
1768 Value::String("Large".into())
1769 );
1770 assert!(updated["answers"].get("Color").is_none());
1772 assert!(updated["answers"].get("Size").is_none());
1773 }
1774
1775 #[test]
1776 fn answer_questions_partial_answers_omits_unanswered() {
1777 let req = ask_user_question_request();
1778 let mut answers = HashMap::new();
1779 answers.insert(1, "Small".to_string());
1780
1781 let resp = req.answer_questions(&answers, "rid-2").unwrap();
1782 let updated = extract_updated_input(&resp);
1783
1784 assert_eq!(
1785 updated["answers"]["Pick a size"],
1786 Value::String("Small".into())
1787 );
1788 assert!(updated["answers"]
1789 .get("Which color do you prefer?")
1790 .is_none());
1791 }
1792
1793 #[test]
1794 fn answer_questions_rejects_wrong_tool() {
1795 let mut req = ask_user_question_request();
1796 req.tool_name = "Bash".to_string();
1797
1798 let mut answers = HashMap::new();
1799 answers.insert(0, "Blue".to_string());
1800 match req.answer_questions(&answers, "rid-3").unwrap_err() {
1801 AskUserQuestionResponseError::WrongTool(name) => assert_eq!(name, "Bash"),
1802 other => panic!("expected WrongTool, got {other:?}"),
1803 }
1804 }
1805
1806 #[test]
1807 fn answer_questions_rejects_unparseable_input() {
1808 let req = ToolPermissionRequest {
1809 tool_name: "AskUserQuestion".to_string(),
1810 input: serde_json::json!({"not_questions": "garbage"}),
1811 permission_suggestions: vec![],
1812 blocked_path: None,
1813 decision_reason: None,
1814 tool_use_id: None,
1815 };
1816
1817 let answers = HashMap::new();
1818 match req.answer_questions(&answers, "rid-4").unwrap_err() {
1819 AskUserQuestionResponseError::ParseInput(_) => {}
1820 other => panic!("expected ParseInput, got {other:?}"),
1821 }
1822 }
1823
1824 #[test]
1825 fn answer_questions_rejects_out_of_range_index() {
1826 let req = ask_user_question_request();
1827 let mut answers = HashMap::new();
1828 answers.insert(7, "ghost".to_string());
1829
1830 match req.answer_questions(&answers, "rid-5").unwrap_err() {
1831 AskUserQuestionResponseError::QuestionIndexOutOfRange { index, total } => {
1832 assert_eq!(index, 7);
1833 assert_eq!(total, 2);
1834 }
1835 other => panic!("expected QuestionIndexOutOfRange, got {other:?}"),
1836 }
1837 }
1838}