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)]
241pub struct ControlCancelRequest {
242 pub request_id: String,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub session_id: Option<String>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251#[serde(tag = "subtype", rename_all = "snake_case")]
252pub enum ControlRequestPayload {
253 CanUseTool(ToolPermissionRequest),
255 HookCallback(HookCallbackRequest),
257 McpMessage(McpMessageRequest),
259 Initialize(InitializeRequest),
261 Interrupt,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
282pub struct Permission {
283 #[serde(rename = "type")]
285 pub permission_type: PermissionType,
286 pub destination: PermissionDestination,
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub mode: Option<PermissionModeName>,
291 #[serde(skip_serializing_if = "Option::is_none")]
293 pub behavior: Option<PermissionBehavior>,
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub rules: Option<Vec<PermissionRule>>,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
301pub struct PermissionRule {
302 #[serde(rename = "toolName")]
304 pub tool_name: String,
305 #[serde(rename = "ruleContent")]
307 pub rule_content: String,
308}
309
310impl Permission {
311 pub fn allow_tool(tool_name: impl Into<String>, rule_content: impl Into<String>) -> Self {
324 Permission {
325 permission_type: PermissionType::AddRules,
326 destination: PermissionDestination::Session,
327 mode: None,
328 behavior: Some(PermissionBehavior::Allow),
329 rules: Some(vec![PermissionRule {
330 tool_name: tool_name.into(),
331 rule_content: rule_content.into(),
332 }]),
333 }
334 }
335
336 pub fn allow_tool_with_destination(
346 tool_name: impl Into<String>,
347 rule_content: impl Into<String>,
348 destination: PermissionDestination,
349 ) -> Self {
350 Permission {
351 permission_type: PermissionType::AddRules,
352 destination,
353 mode: None,
354 behavior: Some(PermissionBehavior::Allow),
355 rules: Some(vec![PermissionRule {
356 tool_name: tool_name.into(),
357 rule_content: rule_content.into(),
358 }]),
359 }
360 }
361
362 pub fn set_mode(mode: PermissionModeName, destination: PermissionDestination) -> Self {
372 Permission {
373 permission_type: PermissionType::SetMode,
374 destination,
375 mode: Some(mode),
376 behavior: None,
377 rules: None,
378 }
379 }
380
381 pub fn from_suggestion(suggestion: &PermissionSuggestion) -> Self {
400 Permission {
401 permission_type: suggestion.suggestion_type.clone(),
402 destination: suggestion.destination.clone(),
403 mode: suggestion.mode.clone(),
404 behavior: suggestion.behavior.clone(),
405 rules: suggestion.rules.as_ref().map(|rules| {
406 rules
407 .iter()
408 .filter_map(|v| {
409 Some(PermissionRule {
410 tool_name: v.get("toolName")?.as_str()?.to_string(),
411 rule_content: v.get("ruleContent")?.as_str()?.to_string(),
412 })
413 })
414 .collect()
415 }),
416 }
417 }
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
431pub struct PermissionSuggestion {
432 #[serde(rename = "type")]
434 pub suggestion_type: PermissionType,
435 pub destination: PermissionDestination,
437 #[serde(skip_serializing_if = "Option::is_none")]
439 pub mode: Option<PermissionModeName>,
440 #[serde(skip_serializing_if = "Option::is_none")]
442 pub behavior: Option<PermissionBehavior>,
443 #[serde(skip_serializing_if = "Option::is_none")]
445 pub rules: Option<Vec<Value>>,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct ToolPermissionRequest {
475 pub tool_name: String,
477 pub input: Value,
479 #[serde(default)]
481 pub permission_suggestions: Vec<PermissionSuggestion>,
482 #[serde(skip_serializing_if = "Option::is_none")]
484 pub blocked_path: Option<String>,
485 #[serde(skip_serializing_if = "Option::is_none")]
487 pub decision_reason: Option<String>,
488 #[serde(skip_serializing_if = "Option::is_none")]
490 pub tool_use_id: Option<String>,
491}
492
493impl ToolPermissionRequest {
494 pub fn allow(&self, request_id: &str) -> ControlResponse {
511 ControlResponse::from_result(request_id, PermissionResult::allow(self.input.clone()))
512 }
513
514 pub fn allow_with(&self, modified_input: Value, request_id: &str) -> ControlResponse {
536 ControlResponse::from_result(request_id, PermissionResult::allow(modified_input))
537 }
538
539 pub fn allow_with_permissions(
543 &self,
544 modified_input: Value,
545 permissions: Vec<Value>,
546 request_id: &str,
547 ) -> ControlResponse {
548 ControlResponse::from_result(
549 request_id,
550 PermissionResult::allow_with_permissions(modified_input, permissions),
551 )
552 }
553
554 pub fn allow_and_remember(
580 &self,
581 permissions: Vec<Permission>,
582 request_id: &str,
583 ) -> ControlResponse {
584 ControlResponse::from_result(
585 request_id,
586 PermissionResult::allow_with_typed_permissions(self.input.clone(), permissions),
587 )
588 }
589
590 pub fn allow_with_and_remember(
594 &self,
595 modified_input: Value,
596 permissions: Vec<Permission>,
597 request_id: &str,
598 ) -> ControlResponse {
599 ControlResponse::from_result(
600 request_id,
601 PermissionResult::allow_with_typed_permissions(modified_input, permissions),
602 )
603 }
604
605 pub fn allow_and_remember_suggestion(&self, request_id: &str) -> Option<ControlResponse> {
631 self.permission_suggestions.first().map(|suggestion| {
632 let perm = Permission::from_suggestion(suggestion);
633 self.allow_and_remember(vec![perm], request_id)
634 })
635 }
636
637 pub fn deny(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
656 ControlResponse::from_result(request_id, PermissionResult::deny(message))
657 }
658
659 pub fn deny_and_stop(&self, message: impl Into<String>, request_id: &str) -> ControlResponse {
663 ControlResponse::from_result(request_id, PermissionResult::deny_and_interrupt(message))
664 }
665
666 pub fn answer_questions(
720 &self,
721 answers_by_index: &HashMap<usize, String>,
722 request_id: &str,
723 ) -> Result<ControlResponse, AskUserQuestionResponseError> {
724 if self.tool_name != "AskUserQuestion" {
725 return Err(AskUserQuestionResponseError::WrongTool(
726 self.tool_name.clone(),
727 ));
728 }
729 let parsed: AskUserQuestionInput = serde_json::from_value(self.input.clone())
730 .map_err(AskUserQuestionResponseError::ParseInput)?;
731 let total = parsed.questions.len();
732
733 let mut answers_map = serde_json::Map::new();
734 for (idx, answer) in answers_by_index {
735 let q = parsed.questions.get(*idx).ok_or(
736 AskUserQuestionResponseError::QuestionIndexOutOfRange { index: *idx, total },
737 )?;
738 answers_map.insert(q.question.clone(), Value::String(answer.clone()));
739 }
740
741 let mut updated_input = self.input.clone();
742 updated_input
744 .as_object_mut()
745 .expect("AskUserQuestion input is a JSON object")
746 .insert("answers".to_string(), Value::Object(answers_map));
747
748 Ok(ControlResponse::from_result(
749 request_id,
750 PermissionResult::allow(updated_input),
751 ))
752 }
753}
754
755#[derive(Debug, thiserror::Error)]
758pub enum AskUserQuestionResponseError {
759 #[error("expected tool_name=AskUserQuestion, got `{0}`")]
761 WrongTool(String),
762 #[error("failed to parse AskUserQuestion input: {0}")]
764 ParseInput(#[source] serde_json::Error),
765 #[error("answer references question index {index}, but only {total} question(s) were asked")]
767 QuestionIndexOutOfRange {
768 index: usize,
770 total: usize,
772 },
773}
774
775#[derive(Debug, Clone, Serialize, Deserialize)]
780#[serde(tag = "behavior", rename_all = "snake_case")]
781pub enum PermissionResult {
782 Allow {
784 #[serde(rename = "updatedInput")]
786 updated_input: Value,
787 #[serde(rename = "updatedPermissions", skip_serializing_if = "Option::is_none")]
789 updated_permissions: Option<Vec<Value>>,
790 },
791 Deny {
793 message: String,
795 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
797 interrupt: bool,
798 },
799}
800
801impl PermissionResult {
802 pub fn allow(input: Value) -> Self {
804 PermissionResult::Allow {
805 updated_input: input,
806 updated_permissions: None,
807 }
808 }
809
810 pub fn allow_with_permissions(input: Value, permissions: Vec<Value>) -> Self {
814 PermissionResult::Allow {
815 updated_input: input,
816 updated_permissions: Some(permissions),
817 }
818 }
819
820 pub fn allow_with_typed_permissions(input: Value, permissions: Vec<Permission>) -> Self {
836 let permission_values: Vec<Value> = permissions
837 .into_iter()
838 .filter_map(|p| serde_json::to_value(p).ok())
839 .collect();
840 PermissionResult::Allow {
841 updated_input: input,
842 updated_permissions: Some(permission_values),
843 }
844 }
845
846 pub fn deny(message: impl Into<String>) -> Self {
848 PermissionResult::Deny {
849 message: message.into(),
850 interrupt: false,
851 }
852 }
853
854 pub fn deny_and_interrupt(message: impl Into<String>) -> Self {
856 PermissionResult::Deny {
857 message: message.into(),
858 interrupt: true,
859 }
860 }
861}
862
863#[derive(Debug, Clone, Serialize, Deserialize)]
865pub struct HookCallbackRequest {
866 pub callback_id: String,
867 pub input: Value,
868 #[serde(skip_serializing_if = "Option::is_none")]
869 pub tool_use_id: Option<String>,
870}
871
872#[derive(Debug, Clone, Serialize, Deserialize)]
874pub struct McpMessageRequest {
875 pub server_name: String,
876 pub message: Value,
877}
878
879#[derive(Debug, Clone, Serialize, Deserialize)]
881pub struct InitializeRequest {
882 #[serde(skip_serializing_if = "Option::is_none")]
883 pub hooks: Option<Value>,
884}
885
886#[derive(Debug, Clone, Serialize, Deserialize)]
891pub struct ControlResponse {
892 pub response: ControlResponsePayload,
894}
895
896impl ControlResponse {
897 pub fn from_result(request_id: &str, result: PermissionResult) -> Self {
901 let response_value = serde_json::to_value(&result)
903 .expect("PermissionResult serialization should never fail");
904 ControlResponse {
905 response: ControlResponsePayload::Success {
906 request_id: request_id.to_string(),
907 response: Some(response_value),
908 },
909 }
910 }
911
912 pub fn success(request_id: &str, response_data: Value) -> Self {
914 ControlResponse {
915 response: ControlResponsePayload::Success {
916 request_id: request_id.to_string(),
917 response: Some(response_data),
918 },
919 }
920 }
921
922 pub fn success_empty(request_id: &str) -> Self {
924 ControlResponse {
925 response: ControlResponsePayload::Success {
926 request_id: request_id.to_string(),
927 response: None,
928 },
929 }
930 }
931
932 pub fn error(request_id: &str, error_message: impl Into<String>) -> Self {
934 ControlResponse {
935 response: ControlResponsePayload::Error {
936 request_id: request_id.to_string(),
937 error: error_message.into(),
938 },
939 }
940 }
941
942 pub fn get_usage_response(&self) -> Option<Result<GetUsageResponse, serde_json::Error>> {
944 match &self.response {
945 ControlResponsePayload::Success {
946 response: Some(value),
947 ..
948 } => Some(serde_json::from_value(value.clone())),
949 _ => None,
950 }
951 }
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize)]
956#[serde(tag = "subtype", rename_all = "snake_case")]
957pub enum ControlResponsePayload {
958 Success {
959 request_id: String,
960 #[serde(skip_serializing_if = "Option::is_none")]
961 response: Option<Value>,
962 },
963 Error {
964 request_id: String,
965 error: String,
966 },
967}
968
969#[derive(Debug, Clone, Serialize, Deserialize)]
971pub struct GetUsageResponse {
972 pub session: UsageSession,
973 pub subscription_type: Option<String>,
974 pub rate_limits_available: bool,
975 pub rate_limits: Option<UsageRateLimits>,
976 pub behaviors: UsageBehaviors,
977 #[serde(flatten)]
978 pub extra: serde_json::Map<String, Value>,
979}
980
981#[derive(Debug, Clone, Default, Serialize, Deserialize)]
982pub struct UsageSession {
983 pub total_cost_usd: f64,
984 pub total_api_duration_ms: u64,
985 pub total_duration_ms: u64,
986 pub total_lines_added: u64,
987 pub total_lines_removed: u64,
988 pub model_usage: HashMap<String, UsageModelUsage>,
989 #[serde(flatten)]
990 pub extra: serde_json::Map<String, Value>,
991}
992
993#[derive(Debug, Clone, Default, Serialize, Deserialize)]
994#[serde(rename_all = "camelCase")]
995pub struct UsageModelUsage {
996 #[serde(default)]
997 pub input_tokens: u64,
998 #[serde(default)]
999 pub output_tokens: u64,
1000 #[serde(default)]
1001 pub cache_read_input_tokens: u64,
1002 #[serde(default)]
1003 pub cache_creation_input_tokens: u64,
1004 #[serde(default)]
1005 pub web_search_requests: u64,
1006 #[serde(default, rename = "costUSD")]
1007 pub cost_usd: f64,
1008 #[serde(default)]
1009 pub context_window: u64,
1010 #[serde(default)]
1011 pub max_output_tokens: u64,
1012 #[serde(flatten)]
1013 pub extra: serde_json::Map<String, Value>,
1014}
1015
1016#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1017pub struct UsageRateLimits {
1018 #[serde(default, skip_serializing_if = "Option::is_none")]
1019 pub five_hour: Option<UsageRateLimitWindow>,
1020 #[serde(default, skip_serializing_if = "Option::is_none")]
1021 pub seven_day: Option<UsageRateLimitWindow>,
1022 #[serde(default, skip_serializing_if = "Option::is_none")]
1023 pub seven_day_oauth_apps: Option<UsageRateLimitWindow>,
1024 #[serde(default, skip_serializing_if = "Option::is_none")]
1025 pub seven_day_opus: Option<UsageRateLimitWindow>,
1026 #[serde(default, skip_serializing_if = "Option::is_none")]
1027 pub seven_day_sonnet: Option<UsageRateLimitWindow>,
1028 #[serde(default, skip_serializing_if = "Option::is_none")]
1029 pub model_scoped: Option<Vec<ModelScopedRateLimit>>,
1030 #[serde(flatten)]
1031 pub extra: serde_json::Map<String, Value>,
1032}
1033
1034#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1035pub struct UsageRateLimitWindow {
1036 pub utilization: Option<f64>,
1037 pub resets_at: Option<String>,
1038 #[serde(flatten)]
1039 pub extra: serde_json::Map<String, Value>,
1040}
1041
1042#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1043pub struct ModelScopedRateLimit {
1044 pub display_name: String,
1045 pub utilization: Option<f64>,
1046 pub resets_at: Option<String>,
1047 #[serde(flatten)]
1048 pub extra: serde_json::Map<String, Value>,
1049}
1050
1051#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1052pub struct UsageBehaviors {
1053 pub request_count: u64,
1054 pub session_count: u64,
1055 pub behaviors: Vec<UsageBehavior>,
1056 pub agents: Value,
1057 pub skills: Value,
1058 pub plugins: Value,
1059 pub mcp_servers: Value,
1060 #[serde(flatten)]
1061 pub extra: serde_json::Map<String, Value>,
1062}
1063
1064#[derive(Debug, Clone, Serialize, Deserialize)]
1065pub struct UsageBehavior {
1066 pub key: String,
1067 pub pct: f64,
1068 pub count: u64,
1069 #[serde(flatten)]
1070 pub extra: serde_json::Map<String, Value>,
1071}
1072
1073#[derive(Debug, Clone, Serialize, Deserialize)]
1075pub struct ControlResponseMessage {
1076 #[serde(rename = "type")]
1077 pub message_type: String,
1078 pub response: ControlResponsePayload,
1079}
1080
1081impl From<ControlResponse> for ControlResponseMessage {
1082 fn from(resp: ControlResponse) -> Self {
1083 ControlResponseMessage {
1084 message_type: "control_response".to_string(),
1085 response: resp.response,
1086 }
1087 }
1088}
1089
1090#[derive(Debug, Clone, Serialize, Deserialize)]
1092pub struct ControlRequestMessage {
1093 #[serde(rename = "type")]
1094 pub message_type: String,
1095 pub request_id: String,
1096 pub request: ControlRequestPayload,
1097}
1098
1099impl ControlRequestMessage {
1100 pub fn initialize(request_id: impl Into<String>) -> Self {
1102 ControlRequestMessage {
1103 message_type: "control_request".to_string(),
1104 request_id: request_id.into(),
1105 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: None }),
1106 }
1107 }
1108
1109 pub fn initialize_with_hooks(request_id: impl Into<String>, hooks: Value) -> Self {
1111 ControlRequestMessage {
1112 message_type: "control_request".to_string(),
1113 request_id: request_id.into(),
1114 request: ControlRequestPayload::Initialize(InitializeRequest { hooks: Some(hooks) }),
1115 }
1116 }
1117
1118 pub fn interrupt(request_id: impl Into<String>) -> Self {
1125 ControlRequestMessage {
1126 message_type: "control_request".to_string(),
1127 request_id: request_id.into(),
1128 request: ControlRequestPayload::Interrupt,
1129 }
1130 }
1131}
1132
1133#[cfg(test)]
1134mod tests {
1135 use super::*;
1136 use crate::io::ClaudeOutput;
1137
1138 #[test]
1139 fn test_deserialize_control_request_can_use_tool() {
1140 let json = r#"{
1141 "type": "control_request",
1142 "request_id": "perm-abc123",
1143 "request": {
1144 "subtype": "can_use_tool",
1145 "tool_name": "Write",
1146 "input": {
1147 "file_path": "/home/user/hello.py",
1148 "content": "print('hello')"
1149 },
1150 "permission_suggestions": [],
1151 "blocked_path": null
1152 }
1153 }"#;
1154
1155 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1156 assert!(output.is_control_request());
1157
1158 if let ClaudeOutput::ControlRequest(req) = output {
1159 assert_eq!(req.request_id, "perm-abc123");
1160 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1161 assert_eq!(perm_req.tool_name, "Write");
1162 assert_eq!(
1163 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1164 "/home/user/hello.py"
1165 );
1166 } else {
1167 panic!("Expected CanUseTool payload");
1168 }
1169 } else {
1170 panic!("Expected ControlRequest");
1171 }
1172 }
1173
1174 #[test]
1175 fn test_deserialize_control_request_edit_tool_real() {
1176 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"}}"#;
1178
1179 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1180 assert!(output.is_control_request());
1181 assert_eq!(output.message_type(), "control_request");
1182
1183 if let ClaudeOutput::ControlRequest(req) = output {
1184 assert_eq!(req.request_id, "f3cf357c-17d6-4eca-b498-dd17c7ac43dd");
1185 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1186 assert_eq!(perm_req.tool_name, "Edit");
1187 assert_eq!(
1188 perm_req.input.get("file_path").unwrap().as_str().unwrap(),
1189 "/home/meawoppl/repos/cc-proxy/proxy/src/ui.rs"
1190 );
1191 assert!(perm_req.input.get("old_string").is_some());
1192 assert!(perm_req.input.get("new_string").is_some());
1193 assert!(!perm_req
1194 .input
1195 .get("replace_all")
1196 .unwrap()
1197 .as_bool()
1198 .unwrap());
1199 } else {
1200 panic!("Expected CanUseTool payload");
1201 }
1202 } else {
1203 panic!("Expected ControlRequest");
1204 }
1205 }
1206
1207 #[test]
1208 fn test_tool_permission_request_allow() {
1209 let req = ToolPermissionRequest {
1210 tool_name: "Read".to_string(),
1211 input: serde_json::json!({"file_path": "/tmp/test.txt"}),
1212 permission_suggestions: vec![],
1213 blocked_path: None,
1214 decision_reason: None,
1215 tool_use_id: None,
1216 };
1217
1218 let response = req.allow("req-123");
1219 let message: ControlResponseMessage = response.into();
1220
1221 let json = serde_json::to_string(&message).unwrap();
1222 assert!(json.contains("\"type\":\"control_response\""));
1223 assert!(json.contains("\"subtype\":\"success\""));
1224 assert!(json.contains("\"request_id\":\"req-123\""));
1225 assert!(json.contains("\"behavior\":\"allow\""));
1226 assert!(json.contains("\"updatedInput\""));
1227 }
1228
1229 #[test]
1230 fn test_tool_permission_request_allow_with_modified_input() {
1231 let req = ToolPermissionRequest {
1232 tool_name: "Write".to_string(),
1233 input: serde_json::json!({"file_path": "/etc/passwd", "content": "test"}),
1234 permission_suggestions: vec![],
1235 blocked_path: None,
1236 decision_reason: None,
1237 tool_use_id: None,
1238 };
1239
1240 let modified_input = serde_json::json!({
1241 "file_path": "/tmp/safe/passwd",
1242 "content": "test"
1243 });
1244 let response = req.allow_with(modified_input, "req-456");
1245 let message: ControlResponseMessage = response.into();
1246
1247 let json = serde_json::to_string(&message).unwrap();
1248 assert!(json.contains("/tmp/safe/passwd"));
1249 assert!(!json.contains("/etc/passwd"));
1250 }
1251
1252 #[test]
1253 fn test_tool_permission_request_deny() {
1254 let req = ToolPermissionRequest {
1255 tool_name: "Bash".to_string(),
1256 input: serde_json::json!({"command": "sudo rm -rf /"}),
1257 permission_suggestions: vec![],
1258 blocked_path: None,
1259 decision_reason: None,
1260 tool_use_id: None,
1261 };
1262
1263 let response = req.deny("Dangerous command blocked", "req-789");
1264 let message: ControlResponseMessage = response.into();
1265
1266 let json = serde_json::to_string(&message).unwrap();
1267 assert!(json.contains("\"behavior\":\"deny\""));
1268 assert!(json.contains("Dangerous command blocked"));
1269 assert!(!json.contains("\"interrupt\":true"));
1270 }
1271
1272 #[test]
1273 fn test_tool_permission_request_deny_and_stop() {
1274 let req = ToolPermissionRequest {
1275 tool_name: "Bash".to_string(),
1276 input: serde_json::json!({"command": "rm -rf /"}),
1277 permission_suggestions: vec![],
1278 blocked_path: None,
1279 decision_reason: None,
1280 tool_use_id: None,
1281 };
1282
1283 let response = req.deny_and_stop("Security violation", "req-000");
1284 let message: ControlResponseMessage = response.into();
1285
1286 let json = serde_json::to_string(&message).unwrap();
1287 assert!(json.contains("\"behavior\":\"deny\""));
1288 assert!(json.contains("\"interrupt\":true"));
1289 }
1290
1291 #[test]
1292 fn test_permission_result_serialization() {
1293 let allow = PermissionResult::allow(serde_json::json!({"test": "value"}));
1295 let json = serde_json::to_string(&allow).unwrap();
1296 assert!(json.contains("\"behavior\":\"allow\""));
1297 assert!(json.contains("\"updatedInput\""));
1298
1299 let deny = PermissionResult::deny("Not allowed");
1301 let json = serde_json::to_string(&deny).unwrap();
1302 assert!(json.contains("\"behavior\":\"deny\""));
1303 assert!(json.contains("\"message\":\"Not allowed\""));
1304 assert!(!json.contains("\"interrupt\""));
1305
1306 let deny_stop = PermissionResult::deny_and_interrupt("Stop!");
1308 let json = serde_json::to_string(&deny_stop).unwrap();
1309 assert!(json.contains("\"interrupt\":true"));
1310 }
1311
1312 #[test]
1313 fn test_control_request_message_initialize() {
1314 let init = ControlRequestMessage::initialize("init-1");
1315
1316 let json = serde_json::to_string(&init).unwrap();
1317 assert!(json.contains("\"type\":\"control_request\""));
1318 assert!(json.contains("\"request_id\":\"init-1\""));
1319 assert!(json.contains("\"subtype\":\"initialize\""));
1320 }
1321
1322 #[test]
1323 fn test_control_request_message_interrupt_wire_shape() {
1324 let msg = ControlRequestMessage::interrupt("interrupt-1");
1327 assert_eq!(
1328 serde_json::to_value(&msg).unwrap(),
1329 serde_json::json!({
1330 "type": "control_request",
1331 "request_id": "interrupt-1",
1332 "request": {"subtype": "interrupt"}
1333 })
1334 );
1335 }
1336
1337 #[test]
1338 fn test_interrupt_payload_roundtrip() {
1339 let json = r#"{"subtype":"interrupt"}"#;
1340 let payload: ControlRequestPayload = serde_json::from_str(json).unwrap();
1341 assert!(matches!(payload, ControlRequestPayload::Interrupt));
1342 assert_eq!(serde_json::to_string(&payload).unwrap(), json);
1343 }
1344
1345 #[test]
1346 fn test_control_response_error() {
1347 let response = ControlResponse::error("req-err", "Something went wrong");
1348 let message: ControlResponseMessage = response.into();
1349
1350 let json = serde_json::to_string(&message).unwrap();
1351 assert!(json.contains("\"subtype\":\"error\""));
1352 assert!(json.contains("\"error\":\"Something went wrong\""));
1353 }
1354
1355 #[test]
1356 fn test_get_usage_response_parses_model_scoped_limits() {
1357 let response = ControlResponse::success(
1358 "usage-1",
1359 serde_json::json!({
1360 "session": {
1361 "total_cost_usd": 0.5,
1362 "total_api_duration_ms": 100,
1363 "total_duration_ms": 200,
1364 "total_lines_added": 3,
1365 "total_lines_removed": 1,
1366 "model_usage": {
1367 "claude-sonnet-4-6": {
1368 "inputTokens": 10,
1369 "outputTokens": 2,
1370 "cacheReadInputTokens": 1,
1371 "cacheCreationInputTokens": 0,
1372 "webSearchRequests": 0,
1373 "costUSD": 0.01,
1374 "contextWindow": 200000,
1375 "maxOutputTokens": 64000
1376 }
1377 }
1378 },
1379 "subscription_type": "pro",
1380 "rate_limits_available": true,
1381 "rate_limits": {
1382 "five_hour": {"utilization": 0.2, "resets_at": "2026-07-09T22:00:00Z"},
1383 "model_scoped": [
1384 {"display_name": "Sonnet", "utilization": 0.4, "resets_at": "2026-07-16T00:00:00Z"}
1385 ]
1386 },
1387 "behaviors": {
1388 "request_count": 1,
1389 "session_count": 1,
1390 "behaviors": [{"key": "cron", "pct": 100.0, "count": 1}],
1391 "agents": [],
1392 "skills": [],
1393 "plugins": [],
1394 "mcp_servers": []
1395 }
1396 }),
1397 );
1398
1399 let usage = response.get_usage_response().unwrap().unwrap();
1400 assert_eq!(usage.subscription_type.as_deref(), Some("pro"));
1401 let model = usage.session.model_usage.get("claude-sonnet-4-6").unwrap();
1402 assert_eq!(model.context_window, 200000);
1403 assert_eq!(model.max_output_tokens, 64000);
1404 let limits = usage.rate_limits.unwrap();
1405 assert_eq!(limits.five_hour.unwrap().utilization, Some(0.2));
1406 assert_eq!(limits.model_scoped.unwrap()[0].display_name, "Sonnet");
1407 }
1408
1409 #[test]
1410 fn test_roundtrip_control_request() {
1411 let original_json = r#"{
1412 "type": "control_request",
1413 "request_id": "test-123",
1414 "request": {
1415 "subtype": "can_use_tool",
1416 "tool_name": "Bash",
1417 "input": {"command": "ls -la"},
1418 "permission_suggestions": []
1419 }
1420 }"#;
1421
1422 let output: ClaudeOutput = serde_json::from_str(original_json).unwrap();
1423
1424 let reserialized = serde_json::to_string(&output).unwrap();
1425 assert!(reserialized.contains("control_request"));
1426 assert!(reserialized.contains("test-123"));
1427 assert!(reserialized.contains("Bash"));
1428 }
1429
1430 #[test]
1431 fn test_permission_suggestions_parsing() {
1432 let json = r#"{
1433 "type": "control_request",
1434 "request_id": "perm-456",
1435 "request": {
1436 "subtype": "can_use_tool",
1437 "tool_name": "Bash",
1438 "input": {"command": "npm test"},
1439 "permission_suggestions": [
1440 {"type": "setMode", "mode": "acceptEdits", "destination": "session"},
1441 {"type": "setMode", "mode": "bypassPermissions", "destination": "project"}
1442 ]
1443 }
1444 }"#;
1445
1446 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1447 if let ClaudeOutput::ControlRequest(req) = output {
1448 if let ControlRequestPayload::CanUseTool(perm_req) = req.request {
1449 assert_eq!(perm_req.permission_suggestions.len(), 2);
1450 assert_eq!(
1451 perm_req.permission_suggestions[0].suggestion_type,
1452 PermissionType::SetMode
1453 );
1454 assert_eq!(
1455 perm_req.permission_suggestions[0].mode,
1456 Some(PermissionModeName::AcceptEdits)
1457 );
1458 assert_eq!(
1459 perm_req.permission_suggestions[0].destination,
1460 PermissionDestination::Session
1461 );
1462 assert_eq!(
1463 perm_req.permission_suggestions[1].suggestion_type,
1464 PermissionType::SetMode
1465 );
1466 assert_eq!(
1467 perm_req.permission_suggestions[1].mode,
1468 Some(PermissionModeName::BypassPermissions)
1469 );
1470 assert_eq!(
1471 perm_req.permission_suggestions[1].destination,
1472 PermissionDestination::Project
1473 );
1474 } else {
1475 panic!("Expected CanUseTool payload");
1476 }
1477 } else {
1478 panic!("Expected ControlRequest");
1479 }
1480 }
1481
1482 #[test]
1483 fn test_permission_suggestion_set_mode_roundtrip() {
1484 let suggestion = PermissionSuggestion {
1485 suggestion_type: PermissionType::SetMode,
1486 destination: PermissionDestination::Session,
1487 mode: Some(PermissionModeName::AcceptEdits),
1488 behavior: None,
1489 rules: None,
1490 };
1491
1492 let json = serde_json::to_string(&suggestion).unwrap();
1493 assert!(json.contains("\"type\":\"setMode\""));
1494 assert!(json.contains("\"mode\":\"acceptEdits\""));
1495 assert!(json.contains("\"destination\":\"session\""));
1496 assert!(!json.contains("\"behavior\""));
1497 assert!(!json.contains("\"rules\""));
1498
1499 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1500 assert_eq!(parsed, suggestion);
1501 }
1502
1503 #[test]
1504 fn test_permission_suggestion_add_rules_roundtrip() {
1505 let suggestion = PermissionSuggestion {
1506 suggestion_type: PermissionType::AddRules,
1507 destination: PermissionDestination::Session,
1508 mode: None,
1509 behavior: Some(PermissionBehavior::Allow),
1510 rules: Some(vec![serde_json::json!({
1511 "toolName": "Read",
1512 "ruleContent": "//tmp/**"
1513 })]),
1514 };
1515
1516 let json = serde_json::to_string(&suggestion).unwrap();
1517 assert!(json.contains("\"type\":\"addRules\""));
1518 assert!(json.contains("\"behavior\":\"allow\""));
1519 assert!(json.contains("\"destination\":\"session\""));
1520 assert!(json.contains("\"rules\""));
1521 assert!(json.contains("\"toolName\":\"Read\""));
1522 assert!(!json.contains("\"mode\""));
1523
1524 let parsed: PermissionSuggestion = serde_json::from_str(&json).unwrap();
1525 assert_eq!(parsed, suggestion);
1526 }
1527
1528 #[test]
1529 fn test_permission_suggestion_add_rules_from_real_json() {
1530 let json = r#"{"type":"addRules","rules":[{"toolName":"Read","ruleContent":"//tmp/**"}],"behavior":"allow","destination":"session"}"#;
1531
1532 let parsed: PermissionSuggestion = serde_json::from_str(json).unwrap();
1533 assert_eq!(parsed.suggestion_type, PermissionType::AddRules);
1534 assert_eq!(parsed.destination, PermissionDestination::Session);
1535 assert_eq!(parsed.behavior, Some(PermissionBehavior::Allow));
1536 assert!(parsed.rules.is_some());
1537 assert!(parsed.mode.is_none());
1538 }
1539
1540 #[test]
1541 fn test_permission_allow_tool() {
1542 let perm = Permission::allow_tool("Bash", "npm test");
1543
1544 assert_eq!(perm.permission_type, PermissionType::AddRules);
1545 assert_eq!(perm.destination, PermissionDestination::Session);
1546 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1547 assert!(perm.mode.is_none());
1548
1549 let rules = perm.rules.unwrap();
1550 assert_eq!(rules.len(), 1);
1551 assert_eq!(rules[0].tool_name, "Bash");
1552 assert_eq!(rules[0].rule_content, "npm test");
1553 }
1554
1555 #[test]
1556 fn test_permission_allow_tool_with_destination() {
1557 let perm = Permission::allow_tool_with_destination(
1558 "Read",
1559 "/tmp/**",
1560 PermissionDestination::Project,
1561 );
1562
1563 assert_eq!(perm.permission_type, PermissionType::AddRules);
1564 assert_eq!(perm.destination, PermissionDestination::Project);
1565 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1566
1567 let rules = perm.rules.unwrap();
1568 assert_eq!(rules[0].tool_name, "Read");
1569 assert_eq!(rules[0].rule_content, "/tmp/**");
1570 }
1571
1572 #[test]
1573 fn test_permission_set_mode() {
1574 let perm = Permission::set_mode(
1575 PermissionModeName::AcceptEdits,
1576 PermissionDestination::Session,
1577 );
1578
1579 assert_eq!(perm.permission_type, PermissionType::SetMode);
1580 assert_eq!(perm.destination, PermissionDestination::Session);
1581 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1582 assert!(perm.behavior.is_none());
1583 assert!(perm.rules.is_none());
1584 }
1585
1586 #[test]
1587 fn test_permission_serialization() {
1588 let perm = Permission::allow_tool("Bash", "npm test");
1589 let json = serde_json::to_string(&perm).unwrap();
1590
1591 assert!(json.contains("\"type\":\"addRules\""));
1592 assert!(json.contains("\"destination\":\"session\""));
1593 assert!(json.contains("\"behavior\":\"allow\""));
1594 assert!(json.contains("\"toolName\":\"Bash\""));
1595 assert!(json.contains("\"ruleContent\":\"npm test\""));
1596 }
1597
1598 #[test]
1599 fn test_permission_from_suggestion_set_mode() {
1600 let suggestion = PermissionSuggestion {
1601 suggestion_type: PermissionType::SetMode,
1602 destination: PermissionDestination::Session,
1603 mode: Some(PermissionModeName::AcceptEdits),
1604 behavior: None,
1605 rules: None,
1606 };
1607
1608 let perm = Permission::from_suggestion(&suggestion);
1609
1610 assert_eq!(perm.permission_type, PermissionType::SetMode);
1611 assert_eq!(perm.destination, PermissionDestination::Session);
1612 assert_eq!(perm.mode, Some(PermissionModeName::AcceptEdits));
1613 }
1614
1615 #[test]
1616 fn test_permission_from_suggestion_add_rules() {
1617 let suggestion = PermissionSuggestion {
1618 suggestion_type: PermissionType::AddRules,
1619 destination: PermissionDestination::Session,
1620 mode: None,
1621 behavior: Some(PermissionBehavior::Allow),
1622 rules: Some(vec![serde_json::json!({
1623 "toolName": "Read",
1624 "ruleContent": "/tmp/**"
1625 })]),
1626 };
1627
1628 let perm = Permission::from_suggestion(&suggestion);
1629
1630 assert_eq!(perm.permission_type, PermissionType::AddRules);
1631 assert_eq!(perm.behavior, Some(PermissionBehavior::Allow));
1632
1633 let rules = perm.rules.unwrap();
1634 assert_eq!(rules.len(), 1);
1635 assert_eq!(rules[0].tool_name, "Read");
1636 assert_eq!(rules[0].rule_content, "/tmp/**");
1637 }
1638
1639 #[test]
1640 fn test_permission_result_allow_with_typed_permissions() {
1641 let result = PermissionResult::allow_with_typed_permissions(
1642 serde_json::json!({"command": "npm test"}),
1643 vec![Permission::allow_tool("Bash", "npm test")],
1644 );
1645
1646 let json = serde_json::to_string(&result).unwrap();
1647 assert!(json.contains("\"behavior\":\"allow\""));
1648 assert!(json.contains("\"updatedPermissions\""));
1649 assert!(json.contains("\"toolName\":\"Bash\""));
1650 }
1651
1652 #[test]
1653 fn test_tool_permission_request_allow_and_remember() {
1654 let req = ToolPermissionRequest {
1655 tool_name: "Bash".to_string(),
1656 input: serde_json::json!({"command": "npm test"}),
1657 permission_suggestions: vec![],
1658 blocked_path: None,
1659 decision_reason: None,
1660 tool_use_id: None,
1661 };
1662
1663 let response =
1664 req.allow_and_remember(vec![Permission::allow_tool("Bash", "npm test")], "req-123");
1665 let message: ControlResponseMessage = response.into();
1666 let json = serde_json::to_string(&message).unwrap();
1667
1668 assert!(json.contains("\"type\":\"control_response\""));
1669 assert!(json.contains("\"behavior\":\"allow\""));
1670 assert!(json.contains("\"updatedPermissions\""));
1671 assert!(json.contains("\"toolName\":\"Bash\""));
1672 }
1673
1674 #[test]
1675 fn test_tool_permission_request_allow_and_remember_suggestion() {
1676 let req = ToolPermissionRequest {
1677 tool_name: "Bash".to_string(),
1678 input: serde_json::json!({"command": "npm test"}),
1679 permission_suggestions: vec![PermissionSuggestion {
1680 suggestion_type: PermissionType::SetMode,
1681 destination: PermissionDestination::Session,
1682 mode: Some(PermissionModeName::AcceptEdits),
1683 behavior: None,
1684 rules: None,
1685 }],
1686 blocked_path: None,
1687 decision_reason: None,
1688 tool_use_id: None,
1689 };
1690
1691 let response = req.allow_and_remember_suggestion("req-123");
1692 assert!(response.is_some());
1693
1694 let message: ControlResponseMessage = response.unwrap().into();
1695 let json = serde_json::to_string(&message).unwrap();
1696
1697 assert!(json.contains("\"type\":\"setMode\""));
1698 assert!(json.contains("\"mode\":\"acceptEdits\""));
1699 }
1700
1701 #[test]
1702 fn test_tool_permission_request_allow_and_remember_suggestion_none() {
1703 let req = ToolPermissionRequest {
1704 tool_name: "Bash".to_string(),
1705 input: serde_json::json!({"command": "npm test"}),
1706 permission_suggestions: vec![], blocked_path: None,
1708 decision_reason: None,
1709 tool_use_id: None,
1710 };
1711
1712 let response = req.allow_and_remember_suggestion("req-123");
1713 assert!(response.is_none());
1714 }
1715
1716 fn ask_user_question_request() -> ToolPermissionRequest {
1717 ToolPermissionRequest {
1718 tool_name: "AskUserQuestion".to_string(),
1719 input: serde_json::json!({
1720 "questions": [
1721 {
1722 "question": "Which color do you prefer?",
1723 "header": "Color",
1724 "options": [
1725 {"label": "Red", "description": "warm"},
1726 {"label": "Blue", "description": "cool"}
1727 ],
1728 "multiSelect": false
1729 },
1730 {
1731 "question": "Pick a size",
1732 "header": "Size",
1733 "options": [
1734 {"label": "Small"},
1735 {"label": "Large"}
1736 ],
1737 "multiSelect": false
1738 }
1739 ]
1740 }),
1741 permission_suggestions: vec![],
1742 blocked_path: None,
1743 decision_reason: None,
1744 tool_use_id: None,
1745 }
1746 }
1747
1748 fn extract_updated_input(resp: &ControlResponse) -> Value {
1749 let ControlResponsePayload::Success { response, .. } = &resp.response else {
1750 panic!("expected Success payload");
1751 };
1752 let inner = response.as_ref().expect("response body present");
1753 inner
1754 .get("updatedInput")
1755 .cloned()
1756 .expect("updatedInput field present")
1757 }
1758
1759 #[test]
1760 fn answer_questions_keys_by_question_text_and_preserves_questions() {
1761 let req = ask_user_question_request();
1762 let mut answers = HashMap::new();
1763 answers.insert(0, "Blue".to_string());
1764 answers.insert(1, "Large".to_string());
1765
1766 let resp = req.answer_questions(&answers, "rid-1").unwrap();
1767 let updated = extract_updated_input(&resp);
1768
1769 assert_eq!(
1771 updated["questions"], req.input["questions"],
1772 "questions array must round-trip unchanged"
1773 );
1774 assert_eq!(
1776 updated["answers"]["Which color do you prefer?"],
1777 Value::String("Blue".into())
1778 );
1779 assert_eq!(
1780 updated["answers"]["Pick a size"],
1781 Value::String("Large".into())
1782 );
1783 assert!(updated["answers"].get("Color").is_none());
1785 assert!(updated["answers"].get("Size").is_none());
1786 }
1787
1788 #[test]
1789 fn answer_questions_partial_answers_omits_unanswered() {
1790 let req = ask_user_question_request();
1791 let mut answers = HashMap::new();
1792 answers.insert(1, "Small".to_string());
1793
1794 let resp = req.answer_questions(&answers, "rid-2").unwrap();
1795 let updated = extract_updated_input(&resp);
1796
1797 assert_eq!(
1798 updated["answers"]["Pick a size"],
1799 Value::String("Small".into())
1800 );
1801 assert!(updated["answers"]
1802 .get("Which color do you prefer?")
1803 .is_none());
1804 }
1805
1806 #[test]
1807 fn answer_questions_rejects_wrong_tool() {
1808 let mut req = ask_user_question_request();
1809 req.tool_name = "Bash".to_string();
1810
1811 let mut answers = HashMap::new();
1812 answers.insert(0, "Blue".to_string());
1813 match req.answer_questions(&answers, "rid-3").unwrap_err() {
1814 AskUserQuestionResponseError::WrongTool(name) => assert_eq!(name, "Bash"),
1815 other => panic!("expected WrongTool, got {other:?}"),
1816 }
1817 }
1818
1819 #[test]
1820 fn answer_questions_rejects_unparseable_input() {
1821 let req = ToolPermissionRequest {
1822 tool_name: "AskUserQuestion".to_string(),
1823 input: serde_json::json!({"not_questions": "garbage"}),
1824 permission_suggestions: vec![],
1825 blocked_path: None,
1826 decision_reason: None,
1827 tool_use_id: None,
1828 };
1829
1830 let answers = HashMap::new();
1831 match req.answer_questions(&answers, "rid-4").unwrap_err() {
1832 AskUserQuestionResponseError::ParseInput(_) => {}
1833 other => panic!("expected ParseInput, got {other:?}"),
1834 }
1835 }
1836
1837 #[test]
1838 fn answer_questions_rejects_out_of_range_index() {
1839 let req = ask_user_question_request();
1840 let mut answers = HashMap::new();
1841 answers.insert(7, "ghost".to_string());
1842
1843 match req.answer_questions(&answers, "rid-5").unwrap_err() {
1844 AskUserQuestionResponseError::QuestionIndexOutOfRange { index, total } => {
1845 assert_eq!(index, 7);
1846 assert_eq!(total, 2);
1847 }
1848 other => panic!("expected QuestionIndexOutOfRange, got {other:?}"),
1849 }
1850 }
1851}