1use std::fmt;
5
6use zeph_common::ToolName;
7
8use crate::shell::background::RunId;
9
10#[derive(Debug, Clone)]
15pub struct DiffData {
16 pub file_path: String,
18 pub old_content: String,
20 pub new_content: String,
22}
23
24#[derive(Debug, Clone, Default)]
50pub struct ToolCall {
51 pub tool_id: ToolName,
53 pub params: serde_json::Map<String, serde_json::Value>,
55 pub caller_id: Option<String>,
58 pub context: Option<crate::ExecutionContext>,
61 pub tool_call_id: String,
64 pub skill_name: Option<Vec<String>>,
71}
72
73#[derive(Debug, Clone, Default)]
78pub struct FilterStats {
79 pub raw_chars: usize,
81 pub filtered_chars: usize,
83 pub raw_lines: usize,
85 pub filtered_lines: usize,
87 pub confidence: Option<crate::FilterConfidence>,
89 pub command: Option<String>,
91 pub kept_lines: Vec<usize>,
93}
94
95impl FilterStats {
96 #[must_use]
100 #[allow(clippy::cast_precision_loss)]
101 pub fn savings_pct(&self) -> f64 {
102 if self.raw_chars == 0 {
103 return 0.0;
104 }
105 (1.0 - self.filtered_chars as f64 / self.raw_chars as f64) * 100.0
106 }
107
108 #[must_use]
113 pub fn estimated_tokens_saved(&self) -> usize {
114 self.raw_chars.saturating_sub(self.filtered_chars) / 4
115 }
116
117 #[must_use]
136 pub fn format_inline(&self, tool_name: &str) -> String {
137 let cmd_label = self
138 .command
139 .as_deref()
140 .map(|c| {
141 let trimmed = c.trim();
142 if trimmed.len() > 60 {
143 format!(" `{}…`", &trimmed[..57])
144 } else {
145 format!(" `{trimmed}`")
146 }
147 })
148 .unwrap_or_default();
149 format!(
150 "[{tool_name}]{cmd_label} {} lines \u{2192} {} lines, {:.1}% filtered",
151 self.raw_lines,
152 self.filtered_lines,
153 self.savings_pct()
154 )
155 }
156}
157
158#[derive(Debug, Clone, Default)]
163pub struct CheckpointActionResult {
164 pub reverted_commands: usize,
166 pub restored: usize,
168 pub deleted: usize,
170 pub supported: bool,
172 pub message: String,
174}
175
176impl CheckpointActionResult {
177 #[must_use]
179 pub fn unsupported() -> Self {
180 Self {
181 supported: false,
182 message: String::new(),
183 ..Default::default()
184 }
185 }
186}
187
188#[derive(Debug, Clone)]
190pub struct CheckpointEntryView {
191 pub index: usize,
193 pub command: String,
195 pub captured_at_secs: u64,
197 pub file_count: usize,
199}
200
201#[derive(Debug, Clone, Default)]
203pub struct CheckpointListResult {
204 pub entries: Vec<CheckpointEntryView>,
206 pub redo_depth: usize,
208 pub supported: bool,
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
218#[serde(rename_all = "snake_case")]
219#[non_exhaustive]
220pub enum ClaimSource {
221 Shell,
223 FileSystem,
225 WebScrape,
227 Mcp,
229 A2a,
231 CodeSearch,
233 Diagnostics,
235 Memory,
237 Moderation,
239}
240
241#[derive(Debug, Clone)]
267pub struct ToolOutput {
268 pub tool_name: ToolName,
270 pub summary: String,
272 pub blocks_executed: u32,
274 pub filter_stats: Option<FilterStats>,
276 pub diff: Option<DiffData>,
278 pub streamed: bool,
280 pub terminal_id: Option<String>,
282 pub locations: Option<Vec<String>>,
284 pub raw_response: Option<serde_json::Value>,
286 pub claim_source: Option<ClaimSource>,
289}
290
291impl fmt::Display for ToolOutput {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 f.write_str(&self.summary)
294 }
295}
296
297pub const MAX_TOOL_OUTPUT_CHARS: usize = 30_000;
302
303#[must_use]
316pub fn truncate_tool_output(output: &str) -> String {
317 truncate_tool_output_at(output, MAX_TOOL_OUTPUT_CHARS)
318}
319
320#[must_use]
336pub fn truncate_tool_output_at(output: &str, max_chars: usize) -> String {
337 if output.len() <= max_chars {
338 return output.to_string();
339 }
340
341 let half = max_chars / 2;
342 let head_end = output.floor_char_boundary(half);
343 let tail_start = output.ceil_char_boundary(output.len() - half);
344 let head = &output[..head_end];
345 let tail = &output[tail_start..];
346 let truncated = output.len() - head_end - (output.len() - tail_start);
347
348 format!(
349 "{head}\n\n... [truncated {truncated} chars, showing first and last ~{half} chars] ...\n\n{tail}"
350 )
351}
352
353#[derive(Debug, Clone)]
358#[non_exhaustive]
359pub enum ToolEvent {
360 Started {
362 tool_name: ToolName,
363 command: String,
364 sandbox_profile: Option<String>,
366 resolved_cwd: Option<String>,
369 execution_env: Option<String>,
372 },
373 OutputChunk {
375 tool_name: ToolName,
376 command: String,
377 chunk: String,
378 tool_call_id: String,
381 skill_name: Option<Vec<String>>,
383 },
384 Completed {
386 tool_name: ToolName,
387 command: String,
388 output: String,
390 success: bool,
392 filter_stats: Option<FilterStats>,
393 diff: Option<DiffData>,
394 run_id: Option<RunId>,
396 },
397 Rollback {
399 tool_name: ToolName,
400 command: String,
401 restored_count: usize,
403 deleted_count: usize,
405 },
406}
407
408pub type ToolEventTx = tokio::sync::mpsc::Sender<ToolEvent>;
416
417pub type ToolEventRx = tokio::sync::mpsc::Receiver<ToolEvent>;
419
420pub const TOOL_EVENT_CHANNEL_CAP: usize = 1024;
422
423#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
428#[non_exhaustive]
429pub enum ErrorKind {
430 Transient,
431 Permanent,
432}
433
434impl std::fmt::Display for ErrorKind {
435 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436 match self {
437 Self::Transient => f.write_str("transient"),
438 Self::Permanent => f.write_str("permanent"),
439 }
440 }
441}
442
443#[non_exhaustive]
444#[derive(Debug, thiserror::Error)]
446pub enum ToolError {
447 #[error("command blocked by policy: {command}")]
448 Blocked { command: String },
449
450 #[error("command blocked by policy: {command}")]
456 BlockedWithFix {
457 command: String,
458 suggestion: Option<crate::shell::SafeFixSuggestion>,
459 },
460
461 #[error("path not allowed by sandbox: {path}")]
462 SandboxViolation { path: String },
463
464 #[error("command requires confirmation: {command}")]
465 ConfirmationRequired { command: String },
466
467 #[error("command timed out after {timeout_secs}s")]
468 Timeout { timeout_secs: u64 },
469
470 #[error("operation cancelled")]
471 Cancelled,
472
473 #[error("invalid tool parameters: {message}")]
474 InvalidParams { message: String },
475
476 #[error("execution failed: {0}")]
477 Execution(#[from] std::io::Error),
478
479 #[error("HTTP error {status}: {message}")]
484 Http { status: u16, message: String },
485
486 #[error("shell error (exit {exit_code}): {message}")]
492 Shell {
493 exit_code: i32,
494 category: crate::error_taxonomy::ToolErrorCategory,
495 message: String,
496 },
497
498 #[error("snapshot failed: {reason}")]
499 SnapshotFailed { reason: String },
500
501 #[error("tool call denied by policy")]
507 OutOfScope {
508 tool_id: String,
510 task_type: Option<String>,
512 },
513
514 #[error("tool call denied by safety probe: {reason}")]
520 SafetyDenied {
521 reason: String,
523 },
524
525 #[error("tool call blocked: trajectory risk {score:.3} exceeds threshold")]
530 TrajectoryRiskExceeded {
531 score: f64,
533 top_signals: Vec<String>,
535 },
536}
537
538impl ToolError {
539 #[must_use]
544 pub fn category(&self) -> crate::error_taxonomy::ToolErrorCategory {
545 use crate::error_taxonomy::{ToolErrorCategory, classify_http_status, classify_io_error};
546 match self {
547 Self::Blocked { .. } | Self::BlockedWithFix { .. } | Self::SandboxViolation { .. } => {
548 ToolErrorCategory::PolicyBlocked
549 }
550 Self::ConfirmationRequired { .. } => ToolErrorCategory::ConfirmationRequired,
551 Self::Timeout { .. } => ToolErrorCategory::Timeout,
552 Self::Cancelled => ToolErrorCategory::Cancelled,
553 Self::InvalidParams { .. } => ToolErrorCategory::InvalidParameters,
554 Self::Http { status, .. } => classify_http_status(*status),
555 Self::Execution(io_err) => classify_io_error(io_err),
556 Self::Shell { category, .. } => *category,
557 Self::SnapshotFailed { .. } => ToolErrorCategory::PermanentFailure,
558 Self::OutOfScope { .. }
559 | Self::SafetyDenied { .. }
560 | Self::TrajectoryRiskExceeded { .. } => ToolErrorCategory::PolicyBlocked,
561 }
562 }
563
564 #[must_use]
572 pub fn kind(&self) -> ErrorKind {
573 use crate::error_taxonomy::ToolErrorCategoryExt;
574 self.category().error_kind()
575 }
576}
577
578pub fn deserialize_params<T: serde::de::DeserializeOwned>(
584 params: &serde_json::Map<String, serde_json::Value>,
585) -> Result<T, ToolError> {
586 let obj = serde_json::Value::Object(params.clone());
587 serde_json::from_value(obj).map_err(|e| ToolError::InvalidParams {
588 message: e.to_string(),
589 })
590}
591
592pub trait ToolExecutor: Send + Sync {
677 fn execute(
686 &self,
687 response: &str,
688 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send;
689
690 fn execute_confirmed(
699 &self,
700 response: &str,
701 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
702 self.execute(response)
703 }
704
705 fn tool_definitions(&self) -> Vec<crate::registry::ToolDef> {
710 vec![]
711 }
712
713 fn execute_tool_call(
719 &self,
720 _call: &ToolCall,
721 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
722 std::future::ready(Ok(None))
723 }
724
725 fn execute_tool_call_confirmed(
734 &self,
735 call: &ToolCall,
736 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
737 self.execute_tool_call(call)
738 }
739
740 fn set_skill_env(&self, _env: Option<std::collections::HashMap<String, String>>) {}
745
746 fn set_effective_trust(&self, _level: crate::SkillTrustLevel) {}
750
751 fn is_tool_retryable(&self, _tool_id: &str) -> bool {
757 false
758 }
759
760 fn checkpoint_undo(&self, _n: usize) -> CheckpointActionResult {
766 CheckpointActionResult::unsupported()
767 }
768
769 fn checkpoint_redo(&self) -> CheckpointActionResult {
773 CheckpointActionResult::unsupported()
774 }
775
776 fn checkpoint_list(&self) -> CheckpointListResult {
780 CheckpointListResult::default()
781 }
782
783 fn is_tool_speculatable(&self, _tool_id: &str) -> bool {
810 false
811 }
812
813 fn requires_confirmation(&self, _call: &ToolCall) -> bool {
821 false
822 }
823}
824
825pub trait ErasedToolExecutor: Send + Sync {
834 fn execute_erased<'a>(
835 &'a self,
836 response: &'a str,
837 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>;
838
839 fn execute_confirmed_erased<'a>(
840 &'a self,
841 response: &'a str,
842 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>;
843
844 fn tool_definitions_erased(&self) -> Vec<crate::registry::ToolDef>;
845
846 fn execute_tool_call_erased<'a>(
847 &'a self,
848 call: &'a ToolCall,
849 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>;
850
851 fn execute_tool_call_confirmed_erased<'a>(
852 &'a self,
853 call: &'a ToolCall,
854 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
855 {
856 self.execute_tool_call_erased(call)
860 }
861
862 fn set_skill_env(&self, _env: Option<std::collections::HashMap<String, String>>) {}
864
865 fn set_effective_trust(&self, _level: crate::SkillTrustLevel) {}
867
868 fn checkpoint_undo_erased(&self, _n: usize) -> CheckpointActionResult {
870 CheckpointActionResult::unsupported()
871 }
872
873 fn checkpoint_redo_erased(&self) -> CheckpointActionResult {
875 CheckpointActionResult::unsupported()
876 }
877
878 fn checkpoint_list_erased(&self) -> CheckpointListResult {
880 CheckpointListResult::default()
881 }
882
883 fn is_tool_retryable_erased(&self, tool_id: &str) -> bool;
885
886 fn is_tool_speculatable_erased(&self, _tool_id: &str) -> bool {
890 false
891 }
892
893 fn requires_confirmation_erased(&self, _call: &ToolCall) -> bool {
902 true
903 }
904}
905
906impl<T: ToolExecutor> ErasedToolExecutor for T {
907 fn execute_erased<'a>(
908 &'a self,
909 response: &'a str,
910 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
911 {
912 Box::pin(self.execute(response))
913 }
914
915 fn execute_confirmed_erased<'a>(
916 &'a self,
917 response: &'a str,
918 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
919 {
920 Box::pin(self.execute_confirmed(response))
921 }
922
923 fn tool_definitions_erased(&self) -> Vec<crate::registry::ToolDef> {
924 self.tool_definitions()
925 }
926
927 fn execute_tool_call_erased<'a>(
928 &'a self,
929 call: &'a ToolCall,
930 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
931 {
932 Box::pin(self.execute_tool_call(call))
933 }
934
935 fn execute_tool_call_confirmed_erased<'a>(
936 &'a self,
937 call: &'a ToolCall,
938 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
939 {
940 Box::pin(self.execute_tool_call_confirmed(call))
941 }
942
943 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
944 ToolExecutor::set_skill_env(self, env);
945 }
946
947 fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
948 ToolExecutor::set_effective_trust(self, level);
949 }
950
951 fn checkpoint_undo_erased(&self, n: usize) -> CheckpointActionResult {
952 ToolExecutor::checkpoint_undo(self, n)
953 }
954
955 fn checkpoint_redo_erased(&self) -> CheckpointActionResult {
956 ToolExecutor::checkpoint_redo(self)
957 }
958
959 fn checkpoint_list_erased(&self) -> CheckpointListResult {
960 ToolExecutor::checkpoint_list(self)
961 }
962
963 fn is_tool_retryable_erased(&self, tool_id: &str) -> bool {
964 ToolExecutor::is_tool_retryable(self, tool_id)
965 }
966
967 fn is_tool_speculatable_erased(&self, tool_id: &str) -> bool {
968 ToolExecutor::is_tool_speculatable(self, tool_id)
969 }
970
971 fn requires_confirmation_erased(&self, call: &ToolCall) -> bool {
972 ToolExecutor::requires_confirmation(self, call)
973 }
974}
975
976pub struct DynExecutor(pub std::sync::Arc<dyn ErasedToolExecutor>);
980
981impl ToolExecutor for DynExecutor {
982 fn execute(
983 &self,
984 response: &str,
985 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
986 let inner = std::sync::Arc::clone(&self.0);
988 let response = response.to_owned();
989 async move { inner.execute_erased(&response).await }
990 }
991
992 fn execute_confirmed(
993 &self,
994 response: &str,
995 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
996 let inner = std::sync::Arc::clone(&self.0);
997 let response = response.to_owned();
998 async move { inner.execute_confirmed_erased(&response).await }
999 }
1000
1001 fn tool_definitions(&self) -> Vec<crate::registry::ToolDef> {
1002 self.0.tool_definitions_erased()
1003 }
1004
1005 fn execute_tool_call(
1006 &self,
1007 call: &ToolCall,
1008 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
1009 let inner = std::sync::Arc::clone(&self.0);
1010 let call = call.clone();
1011 async move { inner.execute_tool_call_erased(&call).await }
1012 }
1013
1014 fn execute_tool_call_confirmed(
1015 &self,
1016 call: &ToolCall,
1017 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
1018 let inner = std::sync::Arc::clone(&self.0);
1019 let call = call.clone();
1020 async move { inner.execute_tool_call_confirmed_erased(&call).await }
1021 }
1022
1023 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
1024 ErasedToolExecutor::set_skill_env(self.0.as_ref(), env);
1025 }
1026
1027 fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
1028 ErasedToolExecutor::set_effective_trust(self.0.as_ref(), level);
1029 }
1030
1031 fn checkpoint_undo(&self, n: usize) -> CheckpointActionResult {
1032 self.0.checkpoint_undo_erased(n)
1033 }
1034
1035 fn checkpoint_redo(&self) -> CheckpointActionResult {
1036 self.0.checkpoint_redo_erased()
1037 }
1038
1039 fn checkpoint_list(&self) -> CheckpointListResult {
1040 self.0.checkpoint_list_erased()
1041 }
1042
1043 fn is_tool_retryable(&self, tool_id: &str) -> bool {
1044 self.0.is_tool_retryable_erased(tool_id)
1045 }
1046
1047 fn is_tool_speculatable(&self, tool_id: &str) -> bool {
1048 self.0.is_tool_speculatable_erased(tool_id)
1049 }
1050
1051 fn requires_confirmation(&self, call: &ToolCall) -> bool {
1052 self.0.requires_confirmation_erased(call)
1053 }
1054}
1055
1056#[must_use]
1060pub fn extract_fenced_blocks<'a>(text: &'a str, lang: &str) -> Vec<&'a str> {
1061 let marker = format!("```{lang}");
1062 let marker_len = marker.len();
1063 let mut blocks = Vec::new();
1064 let mut rest = text;
1065
1066 let mut search_from = 0;
1067 while let Some(rel) = rest[search_from..].find(&marker) {
1068 let start = search_from + rel;
1069 let after = &rest[start + marker_len..];
1070 let boundary_ok = after
1074 .chars()
1075 .next()
1076 .is_none_or(|c| !c.is_alphanumeric() && c != '_' && c != '-');
1077 if !boundary_ok {
1078 search_from = start + marker_len;
1079 continue;
1080 }
1081 if let Some(end) = after.find("```") {
1082 blocks.push(after[..end].trim());
1083 rest = &after[end + 3..];
1084 search_from = 0;
1085 } else {
1086 break;
1087 }
1088 }
1089
1090 blocks
1091}
1092
1093#[cfg(test)]
1094mod tests {
1095 use super::*;
1096 use std::assert_matches;
1097
1098 #[test]
1099 fn tool_output_display() {
1100 let output = ToolOutput {
1101 tool_name: ToolName::new("bash"),
1102 summary: "$ echo hello\nhello".to_owned(),
1103 blocks_executed: 1,
1104 filter_stats: None,
1105 diff: None,
1106 streamed: false,
1107 terminal_id: None,
1108 locations: None,
1109 raw_response: None,
1110 claim_source: None,
1111 };
1112 assert_eq!(output.to_string(), "$ echo hello\nhello");
1113 }
1114
1115 #[test]
1116 fn tool_error_blocked_display() {
1117 let err = ToolError::Blocked {
1118 command: "rm -rf /".to_owned(),
1119 };
1120 assert_eq!(err.to_string(), "command blocked by policy: rm -rf /");
1121 }
1122
1123 #[test]
1124 fn tool_error_sandbox_violation_display() {
1125 let err = ToolError::SandboxViolation {
1126 path: "/etc/shadow".to_owned(),
1127 };
1128 assert_eq!(err.to_string(), "path not allowed by sandbox: /etc/shadow");
1129 }
1130
1131 #[test]
1132 fn tool_error_confirmation_required_display() {
1133 let err = ToolError::ConfirmationRequired {
1134 command: "rm -rf /tmp".to_owned(),
1135 };
1136 assert_eq!(
1137 err.to_string(),
1138 "command requires confirmation: rm -rf /tmp"
1139 );
1140 }
1141
1142 #[test]
1143 fn tool_error_timeout_display() {
1144 let err = ToolError::Timeout { timeout_secs: 30 };
1145 assert_eq!(err.to_string(), "command timed out after 30s");
1146 }
1147
1148 #[test]
1149 fn tool_error_invalid_params_display() {
1150 let err = ToolError::InvalidParams {
1151 message: "missing field `command`".to_owned(),
1152 };
1153 assert_eq!(
1154 err.to_string(),
1155 "invalid tool parameters: missing field `command`"
1156 );
1157 }
1158
1159 #[test]
1160 fn deserialize_params_valid() {
1161 #[derive(Debug, serde::Deserialize, PartialEq)]
1162 struct P {
1163 name: String,
1164 count: u32,
1165 }
1166 let mut map = serde_json::Map::new();
1167 map.insert("name".to_owned(), serde_json::json!("test"));
1168 map.insert("count".to_owned(), serde_json::json!(42));
1169 let p: P = deserialize_params(&map).unwrap();
1170 assert_eq!(
1171 p,
1172 P {
1173 name: "test".to_owned(),
1174 count: 42
1175 }
1176 );
1177 }
1178
1179 #[test]
1180 fn deserialize_params_missing_required_field() {
1181 #[derive(Debug, serde::Deserialize)]
1182 #[allow(dead_code)]
1183 struct P {
1184 name: String,
1185 }
1186 let map = serde_json::Map::new();
1187 let err = deserialize_params::<P>(&map).unwrap_err();
1188 assert_matches!(err, ToolError::InvalidParams { .. });
1189 }
1190
1191 #[test]
1192 fn deserialize_params_wrong_type() {
1193 #[derive(Debug, serde::Deserialize)]
1194 #[allow(dead_code)]
1195 struct P {
1196 count: u32,
1197 }
1198 let mut map = serde_json::Map::new();
1199 map.insert("count".to_owned(), serde_json::json!("not a number"));
1200 let err = deserialize_params::<P>(&map).unwrap_err();
1201 assert_matches!(err, ToolError::InvalidParams { .. });
1202 }
1203
1204 #[test]
1205 fn deserialize_params_all_optional_empty() {
1206 #[derive(Debug, serde::Deserialize, PartialEq)]
1207 struct P {
1208 name: Option<String>,
1209 }
1210 let map = serde_json::Map::new();
1211 let p: P = deserialize_params(&map).unwrap();
1212 assert_eq!(p, P { name: None });
1213 }
1214
1215 #[test]
1216 fn deserialize_params_ignores_extra_fields() {
1217 #[derive(Debug, serde::Deserialize, PartialEq)]
1218 struct P {
1219 name: String,
1220 }
1221 let mut map = serde_json::Map::new();
1222 map.insert("name".to_owned(), serde_json::json!("test"));
1223 map.insert("extra".to_owned(), serde_json::json!(true));
1224 let p: P = deserialize_params(&map).unwrap();
1225 assert_eq!(
1226 p,
1227 P {
1228 name: "test".to_owned()
1229 }
1230 );
1231 }
1232
1233 #[test]
1234 fn tool_error_execution_display() {
1235 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "bash not found");
1236 let err = ToolError::Execution(io_err);
1237 assert!(err.to_string().starts_with("execution failed:"));
1238 assert!(err.to_string().contains("bash not found"));
1239 }
1240
1241 #[test]
1243 fn error_kind_timeout_is_transient() {
1244 let err = ToolError::Timeout { timeout_secs: 30 };
1245 assert_eq!(err.kind(), ErrorKind::Transient);
1246 }
1247
1248 #[test]
1249 fn error_kind_blocked_is_permanent() {
1250 let err = ToolError::Blocked {
1251 command: "rm -rf /".to_owned(),
1252 };
1253 assert_eq!(err.kind(), ErrorKind::Permanent);
1254 }
1255
1256 #[test]
1257 fn error_kind_sandbox_violation_is_permanent() {
1258 let err = ToolError::SandboxViolation {
1259 path: "/etc/shadow".to_owned(),
1260 };
1261 assert_eq!(err.kind(), ErrorKind::Permanent);
1262 }
1263
1264 #[test]
1265 fn error_kind_cancelled_is_permanent() {
1266 assert_eq!(ToolError::Cancelled.kind(), ErrorKind::Permanent);
1267 }
1268
1269 #[test]
1270 fn error_kind_invalid_params_is_permanent() {
1271 let err = ToolError::InvalidParams {
1272 message: "bad arg".to_owned(),
1273 };
1274 assert_eq!(err.kind(), ErrorKind::Permanent);
1275 }
1276
1277 #[test]
1278 fn error_kind_confirmation_required_is_permanent() {
1279 let err = ToolError::ConfirmationRequired {
1280 command: "rm /tmp/x".to_owned(),
1281 };
1282 assert_eq!(err.kind(), ErrorKind::Permanent);
1283 }
1284
1285 #[test]
1286 fn error_kind_execution_timed_out_is_transient() {
1287 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
1288 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1289 }
1290
1291 #[test]
1292 fn error_kind_execution_interrupted_is_transient() {
1293 let io_err = std::io::Error::new(std::io::ErrorKind::Interrupted, "interrupted");
1294 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1295 }
1296
1297 #[test]
1298 fn error_kind_execution_connection_reset_is_transient() {
1299 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
1300 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1301 }
1302
1303 #[test]
1304 fn error_kind_execution_broken_pipe_is_transient() {
1305 let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broken");
1306 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1307 }
1308
1309 #[test]
1310 fn error_kind_execution_would_block_is_transient() {
1311 let io_err = std::io::Error::new(std::io::ErrorKind::WouldBlock, "would block");
1312 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1313 }
1314
1315 #[test]
1316 fn error_kind_execution_connection_aborted_is_transient() {
1317 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionAborted, "aborted");
1318 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1319 }
1320
1321 #[test]
1322 fn error_kind_execution_not_found_is_permanent() {
1323 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
1324 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
1325 }
1326
1327 #[test]
1328 fn error_kind_execution_permission_denied_is_permanent() {
1329 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
1330 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
1331 }
1332
1333 #[test]
1334 fn error_kind_execution_other_is_permanent() {
1335 let io_err = std::io::Error::other("some other error");
1336 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
1337 }
1338
1339 #[test]
1340 fn error_kind_execution_already_exists_is_permanent() {
1341 let io_err = std::io::Error::new(std::io::ErrorKind::AlreadyExists, "exists");
1342 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
1343 }
1344
1345 #[test]
1346 fn error_kind_display() {
1347 assert_eq!(ErrorKind::Transient.to_string(), "transient");
1348 assert_eq!(ErrorKind::Permanent.to_string(), "permanent");
1349 }
1350
1351 #[test]
1352 fn truncate_tool_output_short_passthrough() {
1353 let short = "hello world";
1354 assert_eq!(truncate_tool_output(short), short);
1355 }
1356
1357 #[test]
1358 fn truncate_tool_output_exact_limit() {
1359 let exact = "a".repeat(MAX_TOOL_OUTPUT_CHARS);
1360 assert_eq!(truncate_tool_output(&exact), exact);
1361 }
1362
1363 #[test]
1364 fn truncate_tool_output_long_split() {
1365 let long = "x".repeat(MAX_TOOL_OUTPUT_CHARS + 1000);
1366 let result = truncate_tool_output(&long);
1367 assert!(result.contains("truncated"));
1368 assert!(result.len() < long.len());
1369 }
1370
1371 #[test]
1372 fn truncate_tool_output_notice_contains_count() {
1373 let long = "y".repeat(MAX_TOOL_OUTPUT_CHARS + 2000);
1374 let result = truncate_tool_output(&long);
1375 assert!(result.contains("truncated"));
1376 assert!(result.contains("chars"));
1377 }
1378
1379 #[derive(Debug)]
1380 struct DefaultExecutor;
1381 impl ToolExecutor for DefaultExecutor {
1382 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
1383 Ok(None)
1384 }
1385 }
1386
1387 #[tokio::test]
1388 async fn execute_tool_call_default_returns_none() {
1389 let exec = DefaultExecutor;
1390 let call = ToolCall {
1391 tool_id: ToolName::new("anything"),
1392 params: serde_json::Map::new(),
1393 caller_id: None,
1394 context: None,
1395
1396 tool_call_id: String::new(),
1397 skill_name: None,
1398 };
1399 let result = exec.execute_tool_call(&call).await.unwrap();
1400 assert!(result.is_none());
1401 }
1402
1403 #[test]
1404 fn filter_stats_savings_pct() {
1405 let fs = FilterStats {
1406 raw_chars: 1000,
1407 filtered_chars: 200,
1408 ..Default::default()
1409 };
1410 assert!((fs.savings_pct() - 80.0).abs() < 0.01);
1411 }
1412
1413 #[test]
1414 fn filter_stats_savings_pct_zero() {
1415 let fs = FilterStats::default();
1416 assert!((fs.savings_pct()).abs() < 0.01);
1417 }
1418
1419 #[test]
1420 fn filter_stats_estimated_tokens_saved() {
1421 let fs = FilterStats {
1422 raw_chars: 1000,
1423 filtered_chars: 200,
1424 ..Default::default()
1425 };
1426 assert_eq!(fs.estimated_tokens_saved(), 200); }
1428
1429 #[test]
1430 fn filter_stats_format_inline() {
1431 let fs = FilterStats {
1432 raw_chars: 1000,
1433 filtered_chars: 200,
1434 raw_lines: 342,
1435 filtered_lines: 28,
1436 ..Default::default()
1437 };
1438 let line = fs.format_inline("shell");
1439 assert_eq!(line, "[shell] 342 lines \u{2192} 28 lines, 80.0% filtered");
1440 }
1441
1442 #[test]
1443 fn filter_stats_format_inline_zero() {
1444 let fs = FilterStats::default();
1445 let line = fs.format_inline("bash");
1446 assert_eq!(line, "[bash] 0 lines \u{2192} 0 lines, 0.0% filtered");
1447 }
1448
1449 struct FixedExecutor {
1452 tool_id: &'static str,
1453 output: &'static str,
1454 }
1455
1456 impl ToolExecutor for FixedExecutor {
1457 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
1458 Ok(Some(ToolOutput {
1459 tool_name: ToolName::new(self.tool_id),
1460 summary: self.output.to_owned(),
1461 blocks_executed: 1,
1462 filter_stats: None,
1463 diff: None,
1464 streamed: false,
1465 terminal_id: None,
1466 locations: None,
1467 raw_response: None,
1468 claim_source: None,
1469 }))
1470 }
1471
1472 fn tool_definitions(&self) -> Vec<crate::registry::ToolDef> {
1473 vec![]
1474 }
1475
1476 async fn execute_tool_call(
1477 &self,
1478 _call: &ToolCall,
1479 ) -> Result<Option<ToolOutput>, ToolError> {
1480 Ok(Some(ToolOutput {
1481 tool_name: ToolName::new(self.tool_id),
1482 summary: self.output.to_owned(),
1483 blocks_executed: 1,
1484 filter_stats: None,
1485 diff: None,
1486 streamed: false,
1487 terminal_id: None,
1488 locations: None,
1489 raw_response: None,
1490 claim_source: None,
1491 }))
1492 }
1493 }
1494
1495 #[tokio::test]
1496 async fn dyn_executor_execute_delegates() {
1497 let inner = std::sync::Arc::new(FixedExecutor {
1498 tool_id: "bash",
1499 output: "hello",
1500 });
1501 let exec = DynExecutor(inner);
1502 let result = exec.execute("```bash\necho hello\n```").await.unwrap();
1503 assert!(result.is_some());
1504 assert_eq!(result.unwrap().summary, "hello");
1505 }
1506
1507 #[tokio::test]
1508 async fn dyn_executor_execute_confirmed_delegates() {
1509 let inner = std::sync::Arc::new(FixedExecutor {
1510 tool_id: "bash",
1511 output: "confirmed",
1512 });
1513 let exec = DynExecutor(inner);
1514 let result = exec.execute_confirmed("...").await.unwrap();
1515 assert!(result.is_some());
1516 assert_eq!(result.unwrap().summary, "confirmed");
1517 }
1518
1519 #[test]
1520 fn dyn_executor_tool_definitions_delegates() {
1521 let inner = std::sync::Arc::new(FixedExecutor {
1522 tool_id: "my_tool",
1523 output: "",
1524 });
1525 let exec = DynExecutor(inner);
1526 let defs = exec.tool_definitions();
1528 assert!(defs.is_empty());
1529 }
1530
1531 #[tokio::test]
1532 async fn dyn_executor_execute_tool_call_delegates() {
1533 let inner = std::sync::Arc::new(FixedExecutor {
1534 tool_id: "bash",
1535 output: "tool_call_result",
1536 });
1537 let exec = DynExecutor(inner);
1538 let call = ToolCall {
1539 tool_id: ToolName::new("bash"),
1540 params: serde_json::Map::new(),
1541 caller_id: None,
1542 context: None,
1543
1544 tool_call_id: String::new(),
1545 skill_name: None,
1546 };
1547 let result = exec.execute_tool_call(&call).await.unwrap();
1548 assert!(result.is_some());
1549 assert_eq!(result.unwrap().summary, "tool_call_result");
1550 }
1551
1552 #[test]
1553 fn dyn_executor_set_effective_trust_delegates() {
1554 use std::sync::atomic::{AtomicU8, Ordering};
1555
1556 struct TrustCapture(AtomicU8);
1557 impl ToolExecutor for TrustCapture {
1558 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
1559 Ok(None)
1560 }
1561 fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
1562 let v = match level {
1564 crate::SkillTrustLevel::Trusted => 0u8,
1565 crate::SkillTrustLevel::Verified => 1,
1566 crate::SkillTrustLevel::Quarantined => 2,
1567 _ => 3,
1568 };
1569 self.0.store(v, Ordering::Relaxed);
1570 }
1571 }
1572
1573 let inner = std::sync::Arc::new(TrustCapture(AtomicU8::new(0)));
1574 let exec =
1575 DynExecutor(std::sync::Arc::clone(&inner) as std::sync::Arc<dyn ErasedToolExecutor>);
1576 ToolExecutor::set_effective_trust(&exec, crate::SkillTrustLevel::Quarantined);
1577 assert_eq!(inner.0.load(Ordering::Relaxed), 2);
1578
1579 ToolExecutor::set_effective_trust(&exec, crate::SkillTrustLevel::Blocked);
1580 assert_eq!(inner.0.load(Ordering::Relaxed), 3);
1581 }
1582
1583 #[test]
1584 fn extract_fenced_blocks_no_prefix_match() {
1585 assert!(extract_fenced_blocks("```bashrc\nfoo\n```", "bash").is_empty());
1587 assert_eq!(
1589 extract_fenced_blocks("```bash\nfoo\n```", "bash"),
1590 vec!["foo"]
1591 );
1592 assert_eq!(
1594 extract_fenced_blocks("```bash \nfoo\n```", "bash"),
1595 vec!["foo"]
1596 );
1597 }
1598
1599 #[test]
1602 fn tool_error_http_400_category_is_invalid_parameters() {
1603 use crate::error_taxonomy::ToolErrorCategory;
1604 let err = ToolError::Http {
1605 status: 400,
1606 message: "bad request".to_owned(),
1607 };
1608 assert_eq!(err.category(), ToolErrorCategory::InvalidParameters);
1609 }
1610
1611 #[test]
1612 fn tool_error_http_401_category_is_policy_blocked() {
1613 use crate::error_taxonomy::ToolErrorCategory;
1614 let err = ToolError::Http {
1615 status: 401,
1616 message: "unauthorized".to_owned(),
1617 };
1618 assert_eq!(err.category(), ToolErrorCategory::PolicyBlocked);
1619 }
1620
1621 #[test]
1622 fn tool_error_http_403_category_is_policy_blocked() {
1623 use crate::error_taxonomy::ToolErrorCategory;
1624 let err = ToolError::Http {
1625 status: 403,
1626 message: "forbidden".to_owned(),
1627 };
1628 assert_eq!(err.category(), ToolErrorCategory::PolicyBlocked);
1629 }
1630
1631 #[test]
1632 fn tool_error_http_404_category_is_permanent_failure() {
1633 use crate::error_taxonomy::ToolErrorCategory;
1634 let err = ToolError::Http {
1635 status: 404,
1636 message: "not found".to_owned(),
1637 };
1638 assert_eq!(err.category(), ToolErrorCategory::PermanentFailure);
1639 }
1640
1641 #[test]
1642 fn tool_error_http_429_category_is_rate_limited() {
1643 use crate::error_taxonomy::ToolErrorCategory;
1644 let err = ToolError::Http {
1645 status: 429,
1646 message: "too many requests".to_owned(),
1647 };
1648 assert_eq!(err.category(), ToolErrorCategory::RateLimited);
1649 }
1650
1651 #[test]
1652 fn tool_error_http_500_category_is_server_error() {
1653 use crate::error_taxonomy::ToolErrorCategory;
1654 let err = ToolError::Http {
1655 status: 500,
1656 message: "internal server error".to_owned(),
1657 };
1658 assert_eq!(err.category(), ToolErrorCategory::ServerError);
1659 }
1660
1661 #[test]
1662 fn tool_error_http_502_category_is_server_error() {
1663 use crate::error_taxonomy::ToolErrorCategory;
1664 let err = ToolError::Http {
1665 status: 502,
1666 message: "bad gateway".to_owned(),
1667 };
1668 assert_eq!(err.category(), ToolErrorCategory::ServerError);
1669 }
1670
1671 #[test]
1672 fn tool_error_http_503_category_is_server_error() {
1673 use crate::error_taxonomy::ToolErrorCategory;
1674 let err = ToolError::Http {
1675 status: 503,
1676 message: "service unavailable".to_owned(),
1677 };
1678 assert_eq!(err.category(), ToolErrorCategory::ServerError);
1679 }
1680
1681 #[test]
1682 fn tool_error_http_503_is_transient_triggers_phase2_retry() {
1683 let err = ToolError::Http {
1686 status: 503,
1687 message: "service unavailable".to_owned(),
1688 };
1689 assert_eq!(
1690 err.kind(),
1691 ErrorKind::Transient,
1692 "HTTP 503 must be Transient so Phase 2 retry fires"
1693 );
1694 }
1695
1696 #[test]
1697 fn tool_error_blocked_category_is_policy_blocked() {
1698 use crate::error_taxonomy::ToolErrorCategory;
1699 let err = ToolError::Blocked {
1700 command: "rm -rf /".to_owned(),
1701 };
1702 assert_eq!(err.category(), ToolErrorCategory::PolicyBlocked);
1703 }
1704
1705 #[test]
1706 fn tool_error_sandbox_violation_category_is_policy_blocked() {
1707 use crate::error_taxonomy::ToolErrorCategory;
1708 let err = ToolError::SandboxViolation {
1709 path: "/etc/shadow".to_owned(),
1710 };
1711 assert_eq!(err.category(), ToolErrorCategory::PolicyBlocked);
1712 }
1713
1714 #[test]
1715 fn tool_error_confirmation_required_category() {
1716 use crate::error_taxonomy::ToolErrorCategory;
1717 let err = ToolError::ConfirmationRequired {
1718 command: "rm /tmp/x".to_owned(),
1719 };
1720 assert_eq!(err.category(), ToolErrorCategory::ConfirmationRequired);
1721 }
1722
1723 #[test]
1724 fn tool_error_timeout_category() {
1725 use crate::error_taxonomy::ToolErrorCategory;
1726 let err = ToolError::Timeout { timeout_secs: 30 };
1727 assert_eq!(err.category(), ToolErrorCategory::Timeout);
1728 }
1729
1730 #[test]
1731 fn tool_error_cancelled_category() {
1732 use crate::error_taxonomy::ToolErrorCategory;
1733 assert_eq!(
1734 ToolError::Cancelled.category(),
1735 ToolErrorCategory::Cancelled
1736 );
1737 }
1738
1739 #[test]
1740 fn tool_error_invalid_params_category() {
1741 use crate::error_taxonomy::ToolErrorCategory;
1742 let err = ToolError::InvalidParams {
1743 message: "missing field".to_owned(),
1744 };
1745 assert_eq!(err.category(), ToolErrorCategory::InvalidParameters);
1746 }
1747
1748 #[test]
1750 fn tool_error_execution_not_found_category_is_permanent_failure() {
1751 use crate::error_taxonomy::ToolErrorCategory;
1752 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "bash: not found");
1753 let err = ToolError::Execution(io_err);
1754 let cat = err.category();
1755 assert_ne!(
1756 cat,
1757 ToolErrorCategory::ToolNotFound,
1758 "Execution(NotFound) must NOT map to ToolNotFound"
1759 );
1760 assert_eq!(cat, ToolErrorCategory::PermanentFailure);
1761 }
1762
1763 #[test]
1764 fn tool_error_execution_timed_out_category_is_timeout() {
1765 use crate::error_taxonomy::ToolErrorCategory;
1766 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
1767 assert_eq!(
1768 ToolError::Execution(io_err).category(),
1769 ToolErrorCategory::Timeout
1770 );
1771 }
1772
1773 #[test]
1774 fn tool_error_execution_connection_refused_category_is_network_error() {
1775 use crate::error_taxonomy::ToolErrorCategory;
1776 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
1777 assert_eq!(
1778 ToolError::Execution(io_err).category(),
1779 ToolErrorCategory::NetworkError
1780 );
1781 }
1782
1783 #[test]
1785 fn b4_tool_error_http_429_not_quality_failure() {
1786 let err = ToolError::Http {
1787 status: 429,
1788 message: "rate limited".to_owned(),
1789 };
1790 assert!(
1791 !err.category().is_quality_failure(),
1792 "RateLimited must not be a quality failure"
1793 );
1794 }
1795
1796 #[test]
1797 fn b4_tool_error_http_503_not_quality_failure() {
1798 let err = ToolError::Http {
1799 status: 503,
1800 message: "service unavailable".to_owned(),
1801 };
1802 assert!(
1803 !err.category().is_quality_failure(),
1804 "ServerError must not be a quality failure"
1805 );
1806 }
1807
1808 #[test]
1809 fn b4_tool_error_execution_timed_out_not_quality_failure() {
1810 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
1811 assert!(
1812 !ToolError::Execution(io_err).category().is_quality_failure(),
1813 "Timeout must not be a quality failure"
1814 );
1815 }
1816
1817 #[test]
1820 fn tool_error_shell_exit126_is_policy_blocked() {
1821 use crate::error_taxonomy::ToolErrorCategory;
1822 let err = ToolError::Shell {
1823 exit_code: 126,
1824 category: ToolErrorCategory::PolicyBlocked,
1825 message: "permission denied".to_owned(),
1826 };
1827 assert_eq!(err.category(), ToolErrorCategory::PolicyBlocked);
1828 }
1829
1830 #[test]
1831 fn tool_error_shell_exit127_is_permanent_failure() {
1832 use crate::error_taxonomy::ToolErrorCategory;
1833 let err = ToolError::Shell {
1834 exit_code: 127,
1835 category: ToolErrorCategory::PermanentFailure,
1836 message: "command not found".to_owned(),
1837 };
1838 assert_eq!(err.category(), ToolErrorCategory::PermanentFailure);
1839 assert!(!err.category().is_retryable());
1840 }
1841
1842 #[test]
1843 fn tool_error_shell_not_quality_failure() {
1844 use crate::error_taxonomy::ToolErrorCategory;
1845 let err = ToolError::Shell {
1846 exit_code: 127,
1847 category: ToolErrorCategory::PermanentFailure,
1848 message: "command not found".to_owned(),
1849 };
1850 assert!(!err.category().is_quality_failure());
1852 }
1853
1854 struct StubExecutor;
1858 impl ToolExecutor for StubExecutor {
1859 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
1860 Ok(None)
1861 }
1862 }
1863
1864 struct ConfirmingExecutor;
1866 impl ToolExecutor for ConfirmingExecutor {
1867 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
1868 Ok(None)
1869 }
1870 fn requires_confirmation(&self, _call: &ToolCall) -> bool {
1871 true
1872 }
1873 }
1874
1875 fn dummy_call() -> ToolCall {
1876 ToolCall {
1877 tool_id: ToolName::new("test"),
1878 params: serde_json::Map::new(),
1879 caller_id: None,
1880 context: None,
1881
1882 tool_call_id: String::new(),
1883 skill_name: None,
1884 }
1885 }
1886
1887 #[test]
1888 fn requires_confirmation_default_is_false_on_tool_executor() {
1889 let exec = StubExecutor;
1890 assert!(
1891 !exec.requires_confirmation(&dummy_call()),
1892 "ToolExecutor default requires_confirmation must be false"
1893 );
1894 }
1895
1896 #[test]
1897 fn requires_confirmation_erased_delegates_to_tool_executor_default() {
1898 let exec = StubExecutor;
1900 assert!(
1901 !ErasedToolExecutor::requires_confirmation_erased(&exec, &dummy_call()),
1902 "requires_confirmation_erased via blanket impl must return false for stub executor"
1903 );
1904 }
1905
1906 #[test]
1907 fn requires_confirmation_erased_delegates_override() {
1908 let exec = ConfirmingExecutor;
1911 assert!(
1912 ErasedToolExecutor::requires_confirmation_erased(&exec, &dummy_call()),
1913 "requires_confirmation_erased must return true when ToolExecutor override returns true"
1914 );
1915 }
1916
1917 #[test]
1918 fn requires_confirmation_erased_default_on_erased_trait_is_true() {
1919 struct ManualErased;
1924 impl ErasedToolExecutor for ManualErased {
1925 fn execute_erased<'a>(
1926 &'a self,
1927 _response: &'a str,
1928 ) -> std::pin::Pin<
1929 Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>,
1930 > {
1931 Box::pin(std::future::ready(Ok(None)))
1932 }
1933 fn execute_confirmed_erased<'a>(
1934 &'a self,
1935 _response: &'a str,
1936 ) -> std::pin::Pin<
1937 Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>,
1938 > {
1939 Box::pin(std::future::ready(Ok(None)))
1940 }
1941 fn tool_definitions_erased(&self) -> Vec<crate::registry::ToolDef> {
1942 vec![]
1943 }
1944 fn execute_tool_call_erased<'a>(
1945 &'a self,
1946 _call: &'a ToolCall,
1947 ) -> std::pin::Pin<
1948 Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>,
1949 > {
1950 Box::pin(std::future::ready(Ok(None)))
1951 }
1952 fn is_tool_retryable_erased(&self, _tool_id: &str) -> bool {
1953 false
1954 }
1955 }
1957 let exec = ManualErased;
1958 assert!(
1959 exec.requires_confirmation_erased(&dummy_call()),
1960 "ErasedToolExecutor trait-level default for requires_confirmation_erased must be true"
1961 );
1962 }
1963
1964 #[test]
1967 fn dyn_executor_requires_confirmation_delegates() {
1968 let inner = std::sync::Arc::new(ConfirmingExecutor);
1969 let exec =
1970 DynExecutor(std::sync::Arc::clone(&inner) as std::sync::Arc<dyn ErasedToolExecutor>);
1971 assert!(
1972 ToolExecutor::requires_confirmation(&exec, &dummy_call()),
1973 "DynExecutor must delegate requires_confirmation to inner executor"
1974 );
1975 }
1976
1977 #[test]
1978 fn dyn_executor_requires_confirmation_default_false() {
1979 let inner = std::sync::Arc::new(StubExecutor);
1980 let exec =
1981 DynExecutor(std::sync::Arc::clone(&inner) as std::sync::Arc<dyn ErasedToolExecutor>);
1982 assert!(
1983 !ToolExecutor::requires_confirmation(&exec, &dummy_call()),
1984 "DynExecutor must return false when inner executor does not require confirmation"
1985 );
1986 }
1987}