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