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, Default)]
262pub struct ToolOutput {
263 pub tool_name: ToolName,
265 pub summary: String,
267 pub blocks_executed: u32,
269 pub filter_stats: Option<FilterStats>,
271 pub diff: Option<DiffData>,
273 pub streamed: bool,
275 pub terminal_id: Option<String>,
277 pub locations: Option<Vec<String>>,
279 pub raw_response: Option<serde_json::Value>,
281 pub claim_source: Option<ClaimSource>,
284 pub media: Vec<zeph_llm::ImageData>,
287}
288
289impl fmt::Display for ToolOutput {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 f.write_str(&self.summary)
292 }
293}
294
295pub const MAX_TOOL_OUTPUT_CHARS: usize = 30_000;
300
301#[must_use]
314pub fn truncate_tool_output(output: &str) -> String {
315 truncate_tool_output_at(output, MAX_TOOL_OUTPUT_CHARS)
316}
317
318#[must_use]
334pub fn truncate_tool_output_at(output: &str, max_chars: usize) -> String {
335 if output.len() <= max_chars {
336 return output.to_string();
337 }
338
339 let half = max_chars / 2;
340 let head_end = output.floor_char_boundary(half);
341 let tail_start = output.ceil_char_boundary(output.len() - half);
342 let head = &output[..head_end];
343 let tail = &output[tail_start..];
344 let truncated = output.len() - head_end - (output.len() - tail_start);
345
346 format!(
347 "{head}\n\n... [truncated {truncated} chars, showing first and last ~{half} chars] ...\n\n{tail}"
348 )
349}
350
351#[derive(Debug, Clone)]
356#[non_exhaustive]
357pub enum ToolEvent {
358 Started {
360 tool_name: ToolName,
361 command: String,
362 sandbox_profile: Option<String>,
364 resolved_cwd: Option<String>,
367 execution_env: Option<String>,
370 },
371 OutputChunk {
373 tool_name: ToolName,
374 command: String,
375 chunk: String,
376 tool_call_id: String,
379 skill_name: Option<Vec<String>>,
381 },
382 Completed {
384 tool_name: ToolName,
385 command: String,
386 output: String,
388 success: bool,
390 filter_stats: Option<FilterStats>,
391 diff: Option<DiffData>,
392 run_id: Option<RunId>,
394 },
395 Rollback {
397 tool_name: ToolName,
398 command: String,
399 restored_count: usize,
401 deleted_count: usize,
403 },
404}
405
406pub type ToolEventTx = tokio::sync::mpsc::Sender<ToolEvent>;
414
415pub type ToolEventRx = tokio::sync::mpsc::Receiver<ToolEvent>;
417
418pub const TOOL_EVENT_CHANNEL_CAP: usize = 1024;
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
426#[non_exhaustive]
427pub enum ErrorKind {
428 Transient,
429 Permanent,
430}
431
432impl std::fmt::Display for ErrorKind {
433 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434 match self {
435 Self::Transient => f.write_str("transient"),
436 Self::Permanent => f.write_str("permanent"),
437 }
438 }
439}
440
441#[non_exhaustive]
442#[derive(Debug, thiserror::Error)]
444pub enum ToolError {
445 #[error("command blocked by policy: {command}")]
446 Blocked { command: String },
447
448 #[error("command blocked by policy: {command}")]
454 BlockedWithFix {
455 command: String,
456 suggestion: Option<crate::shell::SafeFixSuggestion>,
457 },
458
459 #[error("path not allowed by sandbox: {path}")]
460 SandboxViolation { path: String },
461
462 #[error("command requires confirmation: {command}")]
463 ConfirmationRequired { command: String },
464
465 #[error("command timed out after {timeout_secs}s")]
466 Timeout { timeout_secs: u64 },
467
468 #[error("operation cancelled")]
469 Cancelled,
470
471 #[error("invalid tool parameters: {message}")]
472 InvalidParams { message: String },
473
474 #[error("execution failed: {0}")]
475 Execution(#[from] std::io::Error),
476
477 #[error("HTTP error {status}: {message}")]
482 Http { status: u16, message: String },
483
484 #[error("shell error (exit {exit_code}): {message}")]
490 Shell {
491 exit_code: i32,
492 category: crate::error_taxonomy::ToolErrorCategory,
493 message: String,
494 },
495
496 #[error("snapshot failed: {reason}")]
497 SnapshotFailed { reason: String },
498
499 #[error("tool call denied by policy")]
505 OutOfScope {
506 tool_id: String,
508 task_type: Option<String>,
510 },
511
512 #[error("tool call denied by safety probe: {reason}")]
518 SafetyDenied {
519 reason: String,
521 },
522
523 #[error("tool call blocked: trajectory risk {score:.3} exceeds threshold")]
528 TrajectoryRiskExceeded {
529 score: f64,
531 top_signals: Vec<String>,
533 },
534}
535
536impl ToolError {
537 #[must_use]
542 pub fn category(&self) -> crate::error_taxonomy::ToolErrorCategory {
543 use crate::error_taxonomy::{ToolErrorCategory, classify_http_status, classify_io_error};
544 match self {
545 Self::Blocked { .. } | Self::BlockedWithFix { .. } | Self::SandboxViolation { .. } => {
546 ToolErrorCategory::PolicyBlocked
547 }
548 Self::ConfirmationRequired { .. } => ToolErrorCategory::ConfirmationRequired,
549 Self::Timeout { .. } => ToolErrorCategory::Timeout,
550 Self::Cancelled => ToolErrorCategory::Cancelled,
551 Self::InvalidParams { .. } => ToolErrorCategory::InvalidParameters,
552 Self::Http { status, .. } => classify_http_status(*status),
553 Self::Execution(io_err) => classify_io_error(io_err),
554 Self::Shell { category, .. } => *category,
555 Self::SnapshotFailed { .. } => ToolErrorCategory::PermanentFailure,
556 Self::OutOfScope { .. }
557 | Self::SafetyDenied { .. }
558 | Self::TrajectoryRiskExceeded { .. } => ToolErrorCategory::PolicyBlocked,
559 }
560 }
561
562 #[must_use]
570 pub fn kind(&self) -> ErrorKind {
571 use crate::error_taxonomy::ToolErrorCategoryExt;
572 self.category().error_kind()
573 }
574}
575
576pub fn deserialize_params<T: serde::de::DeserializeOwned>(
582 params: &serde_json::Map<String, serde_json::Value>,
583) -> Result<T, ToolError> {
584 let obj = serde_json::Value::Object(params.clone());
585 serde_json::from_value(obj).map_err(|e| ToolError::InvalidParams {
586 message: e.to_string(),
587 })
588}
589
590pub trait ToolExecutor: Send + Sync {
671 fn execute(
680 &self,
681 response: &str,
682 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send;
683
684 fn execute_confirmed(
693 &self,
694 response: &str,
695 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
696 self.execute(response)
697 }
698
699 fn tool_definitions(&self) -> Vec<crate::registry::ToolDef> {
704 vec![]
705 }
706
707 fn execute_tool_call(
713 &self,
714 _call: &ToolCall,
715 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
716 std::future::ready(Ok(None))
717 }
718
719 fn execute_tool_call_confirmed(
734 &self,
735 call: &ToolCall,
736 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send;
737
738 fn set_skill_env(&self, _env: Option<std::collections::HashMap<String, String>>) {}
743
744 fn set_effective_trust(&self, _level: crate::SkillTrustLevel) {}
748
749 fn is_tool_retryable(&self, _tool_id: &str) -> bool {
755 false
756 }
757
758 fn checkpoint_undo(&self, n: usize) -> CheckpointActionResult;
766
767 fn checkpoint_redo(&self) -> CheckpointActionResult;
771
772 fn checkpoint_list(&self) -> CheckpointListResult;
776
777 fn is_tool_speculatable(&self, _tool_id: &str) -> bool;
822
823 fn requires_confirmation(&self, _call: &ToolCall) -> bool;
832}
833
834pub trait ErasedToolExecutor: Send + Sync {
843 fn execute_erased<'a>(
844 &'a self,
845 response: &'a str,
846 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>;
847
848 fn execute_confirmed_erased<'a>(
849 &'a self,
850 response: &'a str,
851 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>;
852
853 fn tool_definitions_erased(&self) -> Vec<crate::registry::ToolDef>;
854
855 fn execute_tool_call_erased<'a>(
856 &'a self,
857 call: &'a ToolCall,
858 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>;
859
860 fn execute_tool_call_confirmed_erased<'a>(
870 &'a self,
871 call: &'a ToolCall,
872 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>;
873
874 fn set_skill_env(&self, _env: Option<std::collections::HashMap<String, String>>) {}
876
877 fn set_effective_trust(&self, _level: crate::SkillTrustLevel) {}
879
880 fn checkpoint_undo_erased(&self, n: usize) -> CheckpointActionResult;
887
888 fn checkpoint_redo_erased(&self) -> CheckpointActionResult;
893
894 fn checkpoint_list_erased(&self) -> CheckpointListResult;
899
900 fn is_tool_retryable_erased(&self, tool_id: &str) -> bool;
902
903 fn is_tool_speculatable_erased(&self, _tool_id: &str) -> bool;
908
909 fn requires_confirmation_erased(&self, _call: &ToolCall) -> bool;
918}
919
920impl<T: ToolExecutor> ErasedToolExecutor for T {
921 fn execute_erased<'a>(
922 &'a self,
923 response: &'a str,
924 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
925 {
926 Box::pin(self.execute(response))
927 }
928
929 fn execute_confirmed_erased<'a>(
930 &'a self,
931 response: &'a str,
932 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
933 {
934 Box::pin(self.execute_confirmed(response))
935 }
936
937 fn tool_definitions_erased(&self) -> Vec<crate::registry::ToolDef> {
938 self.tool_definitions()
939 }
940
941 fn execute_tool_call_erased<'a>(
942 &'a self,
943 call: &'a ToolCall,
944 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
945 {
946 Box::pin(self.execute_tool_call(call))
947 }
948
949 fn execute_tool_call_confirmed_erased<'a>(
950 &'a self,
951 call: &'a ToolCall,
952 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>>
953 {
954 Box::pin(self.execute_tool_call_confirmed(call))
955 }
956
957 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
958 ToolExecutor::set_skill_env(self, env);
959 }
960
961 fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
962 ToolExecutor::set_effective_trust(self, level);
963 }
964
965 fn checkpoint_undo_erased(&self, n: usize) -> CheckpointActionResult {
966 ToolExecutor::checkpoint_undo(self, n)
967 }
968
969 fn checkpoint_redo_erased(&self) -> CheckpointActionResult {
970 ToolExecutor::checkpoint_redo(self)
971 }
972
973 fn checkpoint_list_erased(&self) -> CheckpointListResult {
974 ToolExecutor::checkpoint_list(self)
975 }
976
977 fn is_tool_retryable_erased(&self, tool_id: &str) -> bool {
978 ToolExecutor::is_tool_retryable(self, tool_id)
979 }
980
981 fn is_tool_speculatable_erased(&self, tool_id: &str) -> bool {
982 ToolExecutor::is_tool_speculatable(self, tool_id)
983 }
984
985 fn requires_confirmation_erased(&self, call: &ToolCall) -> bool {
986 ToolExecutor::requires_confirmation(self, call)
987 }
988}
989
990pub struct DynExecutor(pub std::sync::Arc<dyn ErasedToolExecutor>);
994
995impl ToolExecutor for DynExecutor {
996 fn execute(
997 &self,
998 response: &str,
999 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
1000 let inner = std::sync::Arc::clone(&self.0);
1002 let response = response.to_owned();
1003 async move { inner.execute_erased(&response).await }
1004 }
1005
1006 fn execute_confirmed(
1007 &self,
1008 response: &str,
1009 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
1010 let inner = std::sync::Arc::clone(&self.0);
1011 let response = response.to_owned();
1012 async move { inner.execute_confirmed_erased(&response).await }
1013 }
1014
1015 fn tool_definitions(&self) -> Vec<crate::registry::ToolDef> {
1016 self.0.tool_definitions_erased()
1017 }
1018
1019 fn execute_tool_call(
1020 &self,
1021 call: &ToolCall,
1022 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
1023 let inner = std::sync::Arc::clone(&self.0);
1024 let call = call.clone();
1025 async move { inner.execute_tool_call_erased(&call).await }
1026 }
1027
1028 fn execute_tool_call_confirmed(
1029 &self,
1030 call: &ToolCall,
1031 ) -> impl Future<Output = Result<Option<ToolOutput>, ToolError>> + Send {
1032 let inner = std::sync::Arc::clone(&self.0);
1033 let call = call.clone();
1034 async move { inner.execute_tool_call_confirmed_erased(&call).await }
1035 }
1036
1037 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
1038 ErasedToolExecutor::set_skill_env(self.0.as_ref(), env);
1039 }
1040
1041 fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
1042 ErasedToolExecutor::set_effective_trust(self.0.as_ref(), level);
1043 }
1044
1045 fn checkpoint_undo(&self, n: usize) -> CheckpointActionResult {
1046 self.0.checkpoint_undo_erased(n)
1047 }
1048
1049 fn checkpoint_redo(&self) -> CheckpointActionResult {
1050 self.0.checkpoint_redo_erased()
1051 }
1052
1053 fn checkpoint_list(&self) -> CheckpointListResult {
1054 self.0.checkpoint_list_erased()
1055 }
1056
1057 fn is_tool_retryable(&self, tool_id: &str) -> bool {
1058 self.0.is_tool_retryable_erased(tool_id)
1059 }
1060
1061 fn is_tool_speculatable(&self, tool_id: &str) -> bool {
1062 self.0.is_tool_speculatable_erased(tool_id)
1063 }
1064
1065 fn requires_confirmation(&self, call: &ToolCall) -> bool {
1066 self.0.requires_confirmation_erased(call)
1067 }
1068}
1069
1070#[must_use]
1074pub fn extract_fenced_blocks<'a>(text: &'a str, lang: &str) -> Vec<&'a str> {
1075 let marker = format!("```{lang}");
1076 let marker_len = marker.len();
1077 let mut blocks = Vec::new();
1078 let mut rest = text;
1079
1080 let mut search_from = 0;
1081 while let Some(rel) = rest[search_from..].find(&marker) {
1082 let start = search_from + rel;
1083 let after = &rest[start + marker_len..];
1084 let boundary_ok = after
1088 .chars()
1089 .next()
1090 .is_none_or(|c| !c.is_alphanumeric() && c != '_' && c != '-');
1091 if !boundary_ok {
1092 search_from = start + marker_len;
1093 continue;
1094 }
1095 if let Some(end) = after.find("```") {
1096 blocks.push(after[..end].trim());
1097 rest = &after[end + 3..];
1098 search_from = 0;
1099 } else {
1100 break;
1101 }
1102 }
1103
1104 blocks
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109 use super::*;
1110 use std::assert_matches;
1111
1112 #[test]
1113 fn tool_output_display() {
1114 let output = ToolOutput {
1115 tool_name: ToolName::new("bash"),
1116 summary: "$ echo hello\nhello".to_owned(),
1117 blocks_executed: 1,
1118 filter_stats: None,
1119 diff: None,
1120 streamed: false,
1121 terminal_id: None,
1122 locations: None,
1123 raw_response: None,
1124 claim_source: None,
1125 ..Default::default()
1126 };
1127 assert_eq!(output.to_string(), "$ echo hello\nhello");
1128 }
1129
1130 #[test]
1131 fn test_tool_output_default_media_empty() {
1132 assert!(ToolOutput::default().media.is_empty());
1133 }
1134
1135 #[test]
1136 fn tool_error_blocked_display() {
1137 let err = ToolError::Blocked {
1138 command: "rm -rf /".to_owned(),
1139 };
1140 assert_eq!(err.to_string(), "command blocked by policy: rm -rf /");
1141 }
1142
1143 #[test]
1144 fn tool_error_sandbox_violation_display() {
1145 let err = ToolError::SandboxViolation {
1146 path: "/etc/shadow".to_owned(),
1147 };
1148 assert_eq!(err.to_string(), "path not allowed by sandbox: /etc/shadow");
1149 }
1150
1151 #[test]
1152 fn tool_error_confirmation_required_display() {
1153 let err = ToolError::ConfirmationRequired {
1154 command: "rm -rf /tmp".to_owned(),
1155 };
1156 assert_eq!(
1157 err.to_string(),
1158 "command requires confirmation: rm -rf /tmp"
1159 );
1160 }
1161
1162 #[test]
1163 fn tool_error_timeout_display() {
1164 let err = ToolError::Timeout { timeout_secs: 30 };
1165 assert_eq!(err.to_string(), "command timed out after 30s");
1166 }
1167
1168 #[test]
1169 fn tool_error_invalid_params_display() {
1170 let err = ToolError::InvalidParams {
1171 message: "missing field `command`".to_owned(),
1172 };
1173 assert_eq!(
1174 err.to_string(),
1175 "invalid tool parameters: missing field `command`"
1176 );
1177 }
1178
1179 #[test]
1180 fn deserialize_params_valid() {
1181 #[derive(Debug, serde::Deserialize, PartialEq)]
1182 struct P {
1183 name: String,
1184 count: u32,
1185 }
1186 let mut map = serde_json::Map::new();
1187 map.insert("name".to_owned(), serde_json::json!("test"));
1188 map.insert("count".to_owned(), serde_json::json!(42));
1189 let p: P = deserialize_params(&map).unwrap();
1190 assert_eq!(
1191 p,
1192 P {
1193 name: "test".to_owned(),
1194 count: 42
1195 }
1196 );
1197 }
1198
1199 #[test]
1200 fn deserialize_params_missing_required_field() {
1201 #[derive(Debug, serde::Deserialize)]
1202 #[allow(dead_code)]
1203 struct P {
1204 name: String,
1205 }
1206 let map = serde_json::Map::new();
1207 let err = deserialize_params::<P>(&map).unwrap_err();
1208 assert_matches!(err, ToolError::InvalidParams { .. });
1209 }
1210
1211 #[test]
1212 fn deserialize_params_wrong_type() {
1213 #[derive(Debug, serde::Deserialize)]
1214 #[allow(dead_code)]
1215 struct P {
1216 count: u32,
1217 }
1218 let mut map = serde_json::Map::new();
1219 map.insert("count".to_owned(), serde_json::json!("not a number"));
1220 let err = deserialize_params::<P>(&map).unwrap_err();
1221 assert_matches!(err, ToolError::InvalidParams { .. });
1222 }
1223
1224 #[test]
1225 fn deserialize_params_all_optional_empty() {
1226 #[derive(Debug, serde::Deserialize, PartialEq)]
1227 struct P {
1228 name: Option<String>,
1229 }
1230 let map = serde_json::Map::new();
1231 let p: P = deserialize_params(&map).unwrap();
1232 assert_eq!(p, P { name: None });
1233 }
1234
1235 #[test]
1236 fn deserialize_params_ignores_extra_fields() {
1237 #[derive(Debug, serde::Deserialize, PartialEq)]
1238 struct P {
1239 name: String,
1240 }
1241 let mut map = serde_json::Map::new();
1242 map.insert("name".to_owned(), serde_json::json!("test"));
1243 map.insert("extra".to_owned(), serde_json::json!(true));
1244 let p: P = deserialize_params(&map).unwrap();
1245 assert_eq!(
1246 p,
1247 P {
1248 name: "test".to_owned()
1249 }
1250 );
1251 }
1252
1253 #[test]
1254 fn tool_error_execution_display() {
1255 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "bash not found");
1256 let err = ToolError::Execution(io_err);
1257 assert!(err.to_string().starts_with("execution failed:"));
1258 assert!(err.to_string().contains("bash not found"));
1259 }
1260
1261 #[test]
1263 fn error_kind_timeout_is_transient() {
1264 let err = ToolError::Timeout { timeout_secs: 30 };
1265 assert_eq!(err.kind(), ErrorKind::Transient);
1266 }
1267
1268 #[test]
1269 fn error_kind_blocked_is_permanent() {
1270 let err = ToolError::Blocked {
1271 command: "rm -rf /".to_owned(),
1272 };
1273 assert_eq!(err.kind(), ErrorKind::Permanent);
1274 }
1275
1276 #[test]
1277 fn error_kind_sandbox_violation_is_permanent() {
1278 let err = ToolError::SandboxViolation {
1279 path: "/etc/shadow".to_owned(),
1280 };
1281 assert_eq!(err.kind(), ErrorKind::Permanent);
1282 }
1283
1284 #[test]
1285 fn error_kind_cancelled_is_permanent() {
1286 assert_eq!(ToolError::Cancelled.kind(), ErrorKind::Permanent);
1287 }
1288
1289 #[test]
1290 fn error_kind_invalid_params_is_permanent() {
1291 let err = ToolError::InvalidParams {
1292 message: "bad arg".to_owned(),
1293 };
1294 assert_eq!(err.kind(), ErrorKind::Permanent);
1295 }
1296
1297 #[test]
1298 fn error_kind_confirmation_required_is_permanent() {
1299 let err = ToolError::ConfirmationRequired {
1300 command: "rm /tmp/x".to_owned(),
1301 };
1302 assert_eq!(err.kind(), ErrorKind::Permanent);
1303 }
1304
1305 #[test]
1306 fn error_kind_execution_timed_out_is_transient() {
1307 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
1308 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1309 }
1310
1311 #[test]
1312 fn error_kind_execution_interrupted_is_transient() {
1313 let io_err = std::io::Error::new(std::io::ErrorKind::Interrupted, "interrupted");
1314 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1315 }
1316
1317 #[test]
1318 fn error_kind_execution_connection_reset_is_transient() {
1319 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
1320 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1321 }
1322
1323 #[test]
1324 fn error_kind_execution_broken_pipe_is_transient() {
1325 let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broken");
1326 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1327 }
1328
1329 #[test]
1330 fn error_kind_execution_would_block_is_transient() {
1331 let io_err = std::io::Error::new(std::io::ErrorKind::WouldBlock, "would block");
1332 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1333 }
1334
1335 #[test]
1336 fn error_kind_execution_connection_aborted_is_transient() {
1337 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionAborted, "aborted");
1338 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Transient);
1339 }
1340
1341 #[test]
1342 fn error_kind_execution_not_found_is_permanent() {
1343 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
1344 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
1345 }
1346
1347 #[test]
1348 fn error_kind_execution_permission_denied_is_permanent() {
1349 let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "denied");
1350 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
1351 }
1352
1353 #[test]
1354 fn error_kind_execution_other_is_permanent() {
1355 let io_err = std::io::Error::other("some other error");
1356 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
1357 }
1358
1359 #[test]
1360 fn error_kind_execution_already_exists_is_permanent() {
1361 let io_err = std::io::Error::new(std::io::ErrorKind::AlreadyExists, "exists");
1362 assert_eq!(ToolError::Execution(io_err).kind(), ErrorKind::Permanent);
1363 }
1364
1365 #[test]
1366 fn error_kind_display() {
1367 assert_eq!(ErrorKind::Transient.to_string(), "transient");
1368 assert_eq!(ErrorKind::Permanent.to_string(), "permanent");
1369 }
1370
1371 #[test]
1372 fn truncate_tool_output_short_passthrough() {
1373 let short = "hello world";
1374 assert_eq!(truncate_tool_output(short), short);
1375 }
1376
1377 #[test]
1378 fn truncate_tool_output_exact_limit() {
1379 let exact = "a".repeat(MAX_TOOL_OUTPUT_CHARS);
1380 assert_eq!(truncate_tool_output(&exact), exact);
1381 }
1382
1383 #[test]
1384 fn truncate_tool_output_long_split() {
1385 let long = "x".repeat(MAX_TOOL_OUTPUT_CHARS + 1000);
1386 let result = truncate_tool_output(&long);
1387 assert!(result.contains("truncated"));
1388 assert!(result.len() < long.len());
1389 }
1390
1391 #[test]
1392 fn truncate_tool_output_notice_contains_count() {
1393 let long = "y".repeat(MAX_TOOL_OUTPUT_CHARS + 2000);
1394 let result = truncate_tool_output(&long);
1395 assert!(result.contains("truncated"));
1396 assert!(result.contains("chars"));
1397 }
1398
1399 #[derive(Debug)]
1400 struct DefaultExecutor;
1401 impl ToolExecutor for DefaultExecutor {
1402 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
1403 Ok(None)
1404 }
1405
1406 crate::tool_executor_no_inner_defaults!();
1407 }
1408
1409 #[tokio::test]
1410 async fn execute_tool_call_default_returns_none() {
1411 let exec = DefaultExecutor;
1412 let call = ToolCall {
1413 tool_id: ToolName::new("anything"),
1414 params: serde_json::Map::new(),
1415 caller_id: None,
1416 context: None,
1417
1418 tool_call_id: String::new(),
1419 skill_name: None,
1420 };
1421 let result = exec.execute_tool_call(&call).await.unwrap();
1422 assert!(result.is_none());
1423 }
1424
1425 #[test]
1426 fn filter_stats_savings_pct() {
1427 let fs = FilterStats {
1428 raw_chars: 1000,
1429 filtered_chars: 200,
1430 ..Default::default()
1431 };
1432 assert!((fs.savings_pct() - 80.0).abs() < 0.01);
1433 }
1434
1435 #[test]
1436 fn filter_stats_savings_pct_zero() {
1437 let fs = FilterStats::default();
1438 assert!((fs.savings_pct()).abs() < 0.01);
1439 }
1440
1441 #[test]
1442 fn filter_stats_estimated_tokens_saved() {
1443 let fs = FilterStats {
1444 raw_chars: 1000,
1445 filtered_chars: 200,
1446 ..Default::default()
1447 };
1448 assert_eq!(fs.estimated_tokens_saved(), 200); }
1450
1451 #[test]
1452 fn filter_stats_format_inline() {
1453 let fs = FilterStats {
1454 raw_chars: 1000,
1455 filtered_chars: 200,
1456 raw_lines: 342,
1457 filtered_lines: 28,
1458 ..Default::default()
1459 };
1460 let line = fs.format_inline("shell");
1461 assert_eq!(line, "[shell] 342 lines \u{2192} 28 lines, 80.0% filtered");
1462 }
1463
1464 #[test]
1465 fn filter_stats_format_inline_zero() {
1466 let fs = FilterStats::default();
1467 let line = fs.format_inline("bash");
1468 assert_eq!(line, "[bash] 0 lines \u{2192} 0 lines, 0.0% filtered");
1469 }
1470
1471 struct FixedExecutor {
1474 tool_id: &'static str,
1475 output: &'static str,
1476 }
1477
1478 impl ToolExecutor for FixedExecutor {
1479 async fn execute(&self, _response: &str) -> 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 ..Default::default()
1492 }))
1493 }
1494
1495 fn tool_definitions(&self) -> Vec<crate::registry::ToolDef> {
1496 vec![]
1497 }
1498
1499 async fn execute_tool_call(
1500 &self,
1501 _call: &ToolCall,
1502 ) -> Result<Option<ToolOutput>, ToolError> {
1503 Ok(Some(ToolOutput {
1504 tool_name: ToolName::new(self.tool_id),
1505 summary: self.output.to_owned(),
1506 blocks_executed: 1,
1507 filter_stats: None,
1508 diff: None,
1509 streamed: false,
1510 terminal_id: None,
1511 locations: None,
1512 raw_response: None,
1513 claim_source: None,
1514 ..Default::default()
1515 }))
1516 }
1517
1518 crate::tool_executor_no_inner_defaults!();
1519 }
1520
1521 #[tokio::test]
1522 async fn dyn_executor_execute_delegates() {
1523 let inner = std::sync::Arc::new(FixedExecutor {
1524 tool_id: "bash",
1525 output: "hello",
1526 });
1527 let exec = DynExecutor(inner);
1528 let result = exec.execute("```bash\necho hello\n```").await.unwrap();
1529 assert!(result.is_some());
1530 assert_eq!(result.unwrap().summary, "hello");
1531 }
1532
1533 #[tokio::test]
1534 async fn dyn_executor_execute_confirmed_delegates() {
1535 let inner = std::sync::Arc::new(FixedExecutor {
1536 tool_id: "bash",
1537 output: "confirmed",
1538 });
1539 let exec = DynExecutor(inner);
1540 let result = exec.execute_confirmed("...").await.unwrap();
1541 assert!(result.is_some());
1542 assert_eq!(result.unwrap().summary, "confirmed");
1543 }
1544
1545 #[test]
1546 fn dyn_executor_tool_definitions_delegates() {
1547 let inner = std::sync::Arc::new(FixedExecutor {
1548 tool_id: "my_tool",
1549 output: "",
1550 });
1551 let exec = DynExecutor(inner);
1552 let defs = exec.tool_definitions();
1554 assert!(defs.is_empty());
1555 }
1556
1557 #[tokio::test]
1558 async fn dyn_executor_execute_tool_call_delegates() {
1559 let inner = std::sync::Arc::new(FixedExecutor {
1560 tool_id: "bash",
1561 output: "tool_call_result",
1562 });
1563 let exec = DynExecutor(inner);
1564 let call = ToolCall {
1565 tool_id: ToolName::new("bash"),
1566 params: serde_json::Map::new(),
1567 caller_id: None,
1568 context: None,
1569
1570 tool_call_id: String::new(),
1571 skill_name: None,
1572 };
1573 let result = exec.execute_tool_call(&call).await.unwrap();
1574 assert!(result.is_some());
1575 assert_eq!(result.unwrap().summary, "tool_call_result");
1576 }
1577
1578 #[test]
1579 fn dyn_executor_set_effective_trust_delegates() {
1580 use std::sync::atomic::{AtomicU8, Ordering};
1581
1582 struct TrustCapture(AtomicU8);
1583 impl ToolExecutor for TrustCapture {
1584 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
1585 Ok(None)
1586 }
1587 fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
1588 let v = match level {
1590 crate::SkillTrustLevel::Trusted => 0u8,
1591 crate::SkillTrustLevel::Verified => 1,
1592 crate::SkillTrustLevel::Quarantined => 2,
1593 _ => 3,
1594 };
1595 self.0.store(v, Ordering::Relaxed);
1596 }
1597
1598 crate::tool_executor_no_inner_defaults!();
1599 }
1600
1601 let inner = std::sync::Arc::new(TrustCapture(AtomicU8::new(0)));
1602 let exec =
1603 DynExecutor(std::sync::Arc::clone(&inner) as std::sync::Arc<dyn ErasedToolExecutor>);
1604 ToolExecutor::set_effective_trust(&exec, crate::SkillTrustLevel::Quarantined);
1605 assert_eq!(inner.0.load(Ordering::Relaxed), 2);
1606
1607 ToolExecutor::set_effective_trust(&exec, crate::SkillTrustLevel::Blocked);
1608 assert_eq!(inner.0.load(Ordering::Relaxed), 3);
1609 }
1610
1611 #[test]
1612 fn extract_fenced_blocks_no_prefix_match() {
1613 assert!(extract_fenced_blocks("```bashrc\nfoo\n```", "bash").is_empty());
1615 assert_eq!(
1617 extract_fenced_blocks("```bash\nfoo\n```", "bash"),
1618 vec!["foo"]
1619 );
1620 assert_eq!(
1622 extract_fenced_blocks("```bash \nfoo\n```", "bash"),
1623 vec!["foo"]
1624 );
1625 }
1626
1627 #[test]
1630 fn tool_error_http_400_category_is_invalid_parameters() {
1631 use crate::error_taxonomy::ToolErrorCategory;
1632 let err = ToolError::Http {
1633 status: 400,
1634 message: "bad request".to_owned(),
1635 };
1636 assert_eq!(err.category(), ToolErrorCategory::InvalidParameters);
1637 }
1638
1639 #[test]
1640 fn tool_error_http_401_category_is_policy_blocked() {
1641 use crate::error_taxonomy::ToolErrorCategory;
1642 let err = ToolError::Http {
1643 status: 401,
1644 message: "unauthorized".to_owned(),
1645 };
1646 assert_eq!(err.category(), ToolErrorCategory::PolicyBlocked);
1647 }
1648
1649 #[test]
1650 fn tool_error_http_403_category_is_policy_blocked() {
1651 use crate::error_taxonomy::ToolErrorCategory;
1652 let err = ToolError::Http {
1653 status: 403,
1654 message: "forbidden".to_owned(),
1655 };
1656 assert_eq!(err.category(), ToolErrorCategory::PolicyBlocked);
1657 }
1658
1659 #[test]
1660 fn tool_error_http_404_category_is_permanent_failure() {
1661 use crate::error_taxonomy::ToolErrorCategory;
1662 let err = ToolError::Http {
1663 status: 404,
1664 message: "not found".to_owned(),
1665 };
1666 assert_eq!(err.category(), ToolErrorCategory::PermanentFailure);
1667 }
1668
1669 #[test]
1670 fn tool_error_http_429_category_is_rate_limited() {
1671 use crate::error_taxonomy::ToolErrorCategory;
1672 let err = ToolError::Http {
1673 status: 429,
1674 message: "too many requests".to_owned(),
1675 };
1676 assert_eq!(err.category(), ToolErrorCategory::RateLimited);
1677 }
1678
1679 #[test]
1680 fn tool_error_http_500_category_is_server_error() {
1681 use crate::error_taxonomy::ToolErrorCategory;
1682 let err = ToolError::Http {
1683 status: 500,
1684 message: "internal server error".to_owned(),
1685 };
1686 assert_eq!(err.category(), ToolErrorCategory::ServerError);
1687 }
1688
1689 #[test]
1690 fn tool_error_http_502_category_is_server_error() {
1691 use crate::error_taxonomy::ToolErrorCategory;
1692 let err = ToolError::Http {
1693 status: 502,
1694 message: "bad gateway".to_owned(),
1695 };
1696 assert_eq!(err.category(), ToolErrorCategory::ServerError);
1697 }
1698
1699 #[test]
1700 fn tool_error_http_503_category_is_server_error() {
1701 use crate::error_taxonomy::ToolErrorCategory;
1702 let err = ToolError::Http {
1703 status: 503,
1704 message: "service unavailable".to_owned(),
1705 };
1706 assert_eq!(err.category(), ToolErrorCategory::ServerError);
1707 }
1708
1709 #[test]
1710 fn tool_error_http_503_is_transient_triggers_phase2_retry() {
1711 let err = ToolError::Http {
1714 status: 503,
1715 message: "service unavailable".to_owned(),
1716 };
1717 assert_eq!(
1718 err.kind(),
1719 ErrorKind::Transient,
1720 "HTTP 503 must be Transient so Phase 2 retry fires"
1721 );
1722 }
1723
1724 #[test]
1725 fn tool_error_blocked_category_is_policy_blocked() {
1726 use crate::error_taxonomy::ToolErrorCategory;
1727 let err = ToolError::Blocked {
1728 command: "rm -rf /".to_owned(),
1729 };
1730 assert_eq!(err.category(), ToolErrorCategory::PolicyBlocked);
1731 }
1732
1733 #[test]
1734 fn tool_error_sandbox_violation_category_is_policy_blocked() {
1735 use crate::error_taxonomy::ToolErrorCategory;
1736 let err = ToolError::SandboxViolation {
1737 path: "/etc/shadow".to_owned(),
1738 };
1739 assert_eq!(err.category(), ToolErrorCategory::PolicyBlocked);
1740 }
1741
1742 #[test]
1743 fn tool_error_confirmation_required_category() {
1744 use crate::error_taxonomy::ToolErrorCategory;
1745 let err = ToolError::ConfirmationRequired {
1746 command: "rm /tmp/x".to_owned(),
1747 };
1748 assert_eq!(err.category(), ToolErrorCategory::ConfirmationRequired);
1749 }
1750
1751 #[test]
1752 fn tool_error_timeout_category() {
1753 use crate::error_taxonomy::ToolErrorCategory;
1754 let err = ToolError::Timeout { timeout_secs: 30 };
1755 assert_eq!(err.category(), ToolErrorCategory::Timeout);
1756 }
1757
1758 #[test]
1759 fn tool_error_cancelled_category() {
1760 use crate::error_taxonomy::ToolErrorCategory;
1761 assert_eq!(
1762 ToolError::Cancelled.category(),
1763 ToolErrorCategory::Cancelled
1764 );
1765 }
1766
1767 #[test]
1768 fn tool_error_invalid_params_category() {
1769 use crate::error_taxonomy::ToolErrorCategory;
1770 let err = ToolError::InvalidParams {
1771 message: "missing field".to_owned(),
1772 };
1773 assert_eq!(err.category(), ToolErrorCategory::InvalidParameters);
1774 }
1775
1776 #[test]
1778 fn tool_error_execution_not_found_category_is_permanent_failure() {
1779 use crate::error_taxonomy::ToolErrorCategory;
1780 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "bash: not found");
1781 let err = ToolError::Execution(io_err);
1782 let cat = err.category();
1783 assert_ne!(
1784 cat,
1785 ToolErrorCategory::ToolNotFound,
1786 "Execution(NotFound) must NOT map to ToolNotFound"
1787 );
1788 assert_eq!(cat, ToolErrorCategory::PermanentFailure);
1789 }
1790
1791 #[test]
1792 fn tool_error_execution_timed_out_category_is_timeout() {
1793 use crate::error_taxonomy::ToolErrorCategory;
1794 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
1795 assert_eq!(
1796 ToolError::Execution(io_err).category(),
1797 ToolErrorCategory::Timeout
1798 );
1799 }
1800
1801 #[test]
1802 fn tool_error_execution_connection_refused_category_is_network_error() {
1803 use crate::error_taxonomy::ToolErrorCategory;
1804 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
1805 assert_eq!(
1806 ToolError::Execution(io_err).category(),
1807 ToolErrorCategory::NetworkError
1808 );
1809 }
1810
1811 #[test]
1813 fn b4_tool_error_http_429_not_quality_failure() {
1814 let err = ToolError::Http {
1815 status: 429,
1816 message: "rate limited".to_owned(),
1817 };
1818 assert!(
1819 !err.category().is_quality_failure(),
1820 "RateLimited must not be a quality failure"
1821 );
1822 }
1823
1824 #[test]
1825 fn b4_tool_error_http_503_not_quality_failure() {
1826 let err = ToolError::Http {
1827 status: 503,
1828 message: "service unavailable".to_owned(),
1829 };
1830 assert!(
1831 !err.category().is_quality_failure(),
1832 "ServerError must not be a quality failure"
1833 );
1834 }
1835
1836 #[test]
1837 fn b4_tool_error_execution_timed_out_not_quality_failure() {
1838 let io_err = std::io::Error::new(std::io::ErrorKind::TimedOut, "timeout");
1839 assert!(
1840 !ToolError::Execution(io_err).category().is_quality_failure(),
1841 "Timeout must not be a quality failure"
1842 );
1843 }
1844
1845 #[test]
1848 fn tool_error_shell_exit126_is_policy_blocked() {
1849 use crate::error_taxonomy::ToolErrorCategory;
1850 let err = ToolError::Shell {
1851 exit_code: 126,
1852 category: ToolErrorCategory::PolicyBlocked,
1853 message: "permission denied".to_owned(),
1854 };
1855 assert_eq!(err.category(), ToolErrorCategory::PolicyBlocked);
1856 }
1857
1858 #[test]
1859 fn tool_error_shell_exit127_is_permanent_failure() {
1860 use crate::error_taxonomy::ToolErrorCategory;
1861 let err = ToolError::Shell {
1862 exit_code: 127,
1863 category: ToolErrorCategory::PermanentFailure,
1864 message: "command not found".to_owned(),
1865 };
1866 assert_eq!(err.category(), ToolErrorCategory::PermanentFailure);
1867 assert!(!err.category().is_retryable());
1868 }
1869
1870 #[test]
1871 fn tool_error_shell_not_quality_failure() {
1872 use crate::error_taxonomy::ToolErrorCategory;
1873 let err = ToolError::Shell {
1874 exit_code: 127,
1875 category: ToolErrorCategory::PermanentFailure,
1876 message: "command not found".to_owned(),
1877 };
1878 assert!(!err.category().is_quality_failure());
1880 }
1881
1882 struct StubExecutor;
1886 impl ToolExecutor for StubExecutor {
1887 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
1888 Ok(None)
1889 }
1890
1891 crate::tool_executor_no_inner_defaults!();
1892 }
1893
1894 struct ConfirmingExecutor;
1896 impl ToolExecutor for ConfirmingExecutor {
1897 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
1898 Ok(None)
1899 }
1900 fn requires_confirmation(&self, _call: &ToolCall) -> bool {
1901 true
1902 }
1903
1904 async fn execute_tool_call_confirmed(
1905 &self,
1906 call: &ToolCall,
1907 ) -> Result<Option<ToolOutput>, ToolError> {
1908 self.execute_tool_call(call).await
1909 }
1910 fn checkpoint_undo(&self, _n: usize) -> CheckpointActionResult {
1911 CheckpointActionResult::unsupported()
1912 }
1913 fn checkpoint_redo(&self) -> CheckpointActionResult {
1914 CheckpointActionResult::unsupported()
1915 }
1916 fn checkpoint_list(&self) -> CheckpointListResult {
1917 CheckpointListResult::default()
1918 }
1919 fn is_tool_speculatable(&self, _tool_id: &str) -> bool {
1920 false
1921 }
1922 }
1923
1924 fn dummy_call() -> ToolCall {
1925 ToolCall {
1926 tool_id: ToolName::new("test"),
1927 params: serde_json::Map::new(),
1928 caller_id: None,
1929 context: None,
1930
1931 tool_call_id: String::new(),
1932 skill_name: None,
1933 }
1934 }
1935
1936 #[test]
1937 fn requires_confirmation_default_is_false_on_tool_executor() {
1938 let exec = StubExecutor;
1939 assert!(
1940 !exec.requires_confirmation(&dummy_call()),
1941 "ToolExecutor default requires_confirmation must be false"
1942 );
1943 }
1944
1945 #[test]
1946 fn requires_confirmation_erased_delegates_to_tool_executor_default() {
1947 let exec = StubExecutor;
1949 assert!(
1950 !ErasedToolExecutor::requires_confirmation_erased(&exec, &dummy_call()),
1951 "requires_confirmation_erased via blanket impl must return false for stub executor"
1952 );
1953 }
1954
1955 #[test]
1956 fn requires_confirmation_erased_delegates_override() {
1957 let exec = ConfirmingExecutor;
1960 assert!(
1961 ErasedToolExecutor::requires_confirmation_erased(&exec, &dummy_call()),
1962 "requires_confirmation_erased must return true when ToolExecutor override returns true"
1963 );
1964 }
1965
1966 #[test]
1967 fn requires_confirmation_erased_manual_impl_returns_true() {
1968 struct ManualErased;
1974 impl ErasedToolExecutor for ManualErased {
1975 fn execute_erased<'a>(
1976 &'a self,
1977 _response: &'a str,
1978 ) -> std::pin::Pin<
1979 Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>,
1980 > {
1981 Box::pin(std::future::ready(Ok(None)))
1982 }
1983 fn execute_confirmed_erased<'a>(
1984 &'a self,
1985 _response: &'a str,
1986 ) -> std::pin::Pin<
1987 Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>,
1988 > {
1989 Box::pin(std::future::ready(Ok(None)))
1990 }
1991 fn tool_definitions_erased(&self) -> Vec<crate::registry::ToolDef> {
1992 vec![]
1993 }
1994 fn execute_tool_call_erased<'a>(
1995 &'a self,
1996 _call: &'a ToolCall,
1997 ) -> std::pin::Pin<
1998 Box<dyn Future<Output = Result<Option<ToolOutput>, ToolError>> + Send + 'a>,
1999 > {
2000 Box::pin(std::future::ready(Ok(None)))
2001 }
2002 fn is_tool_retryable_erased(&self, _tool_id: &str) -> bool {
2003 false
2004 }
2005
2006 crate::erased_tool_executor_no_inner_defaults!();
2007 }
2008 let exec = ManualErased;
2009 assert!(
2010 exec.requires_confirmation_erased(&dummy_call()),
2011 "requires_confirmation_erased must be true (matches the removed trait default's value)"
2012 );
2013 }
2014
2015 #[test]
2018 fn dyn_executor_requires_confirmation_delegates() {
2019 let inner = std::sync::Arc::new(ConfirmingExecutor);
2020 let exec =
2021 DynExecutor(std::sync::Arc::clone(&inner) as std::sync::Arc<dyn ErasedToolExecutor>);
2022 assert!(
2023 ToolExecutor::requires_confirmation(&exec, &dummy_call()),
2024 "DynExecutor must delegate requires_confirmation to inner executor"
2025 );
2026 }
2027
2028 #[test]
2029 fn dyn_executor_requires_confirmation_default_false() {
2030 let inner = std::sync::Arc::new(StubExecutor);
2031 let exec =
2032 DynExecutor(std::sync::Arc::clone(&inner) as std::sync::Arc<dyn ErasedToolExecutor>);
2033 assert!(
2034 !ToolExecutor::requires_confirmation(&exec, &dummy_call()),
2035 "DynExecutor must return false when inner executor does not require confirmation"
2036 );
2037 }
2038}