1use crate::executor::{ToolCall, ToolError, ToolExecutor, ToolOutput};
7use crate::registry::ToolDef;
8
9#[derive(Debug)]
34pub struct CompositeExecutor<A: ToolExecutor, B: ToolExecutor> {
35 first: A,
36 second: B,
37}
38
39impl<A: ToolExecutor, B: ToolExecutor> CompositeExecutor<A, B> {
40 #[must_use]
42 pub fn new(first: A, second: B) -> Self {
43 Self { first, second }
44 }
45}
46
47impl<A: ToolExecutor, B: ToolExecutor> ToolExecutor for CompositeExecutor<A, B> {
48 async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
49 if let Some(output) = self.first.execute(response).await? {
50 return Ok(Some(output));
51 }
52 self.second.execute(response).await
53 }
54
55 async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
56 if let Some(output) = self.first.execute_confirmed(response).await? {
57 return Ok(Some(output));
58 }
59 self.second.execute_confirmed(response).await
60 }
61
62 fn tool_definitions(&self) -> Vec<ToolDef> {
63 let mut defs = self.first.tool_definitions();
64 let seen: std::collections::HashSet<String> =
65 defs.iter().map(|d| d.id.to_string()).collect();
66 for def in self.second.tool_definitions() {
67 if !seen.contains(def.id.as_ref()) {
68 defs.push(def);
69 }
70 }
71 defs
72 }
73
74 async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
75 if let Some(output) = self.first.execute_tool_call(call).await? {
76 return Ok(Some(output));
77 }
78 self.second.execute_tool_call(call).await
79 }
80
81 async fn execute_tool_call_confirmed(
82 &self,
83 call: &ToolCall,
84 ) -> Result<Option<ToolOutput>, ToolError> {
85 if let Some(output) = self.first.execute_tool_call_confirmed(call).await? {
86 return Ok(Some(output));
87 }
88 self.second.execute_tool_call_confirmed(call).await
89 }
90
91 fn is_tool_retryable(&self, tool_id: &str) -> bool {
92 self.first.is_tool_retryable(tool_id) || self.second.is_tool_retryable(tool_id)
93 }
94
95 fn is_tool_speculatable(&self, tool_id: &str) -> bool {
96 self.first.is_tool_speculatable(tool_id) || self.second.is_tool_speculatable(tool_id)
97 }
98
99 fn requires_confirmation(&self, call: &ToolCall) -> bool {
106 self.first.requires_confirmation(call) || self.second.requires_confirmation(call)
107 }
108
109 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
117 self.first.set_skill_env(env.clone());
118 self.second.set_skill_env(env);
119 }
120
121 fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
127 self.first.set_effective_trust(level);
128 self.second.set_effective_trust(level);
129 }
130
131 fn checkpoint_undo(&self, n: usize) -> crate::executor::CheckpointActionResult {
133 let result = self.first.checkpoint_undo(n);
134 if result.supported {
135 return result;
136 }
137 self.second.checkpoint_undo(n)
138 }
139
140 fn checkpoint_redo(&self) -> crate::executor::CheckpointActionResult {
142 let result = self.first.checkpoint_redo();
143 if result.supported {
144 return result;
145 }
146 self.second.checkpoint_redo()
147 }
148
149 fn checkpoint_list(&self) -> crate::executor::CheckpointListResult {
151 let result = self.first.checkpoint_list();
152 if result.supported {
153 return result;
154 }
155 self.second.checkpoint_list()
156 }
157}
158
159#[derive(Debug)]
168pub struct OptionalExecutor<T: ToolExecutor>(pub Option<T>);
169
170impl<T: ToolExecutor> ToolExecutor for OptionalExecutor<T> {
171 async fn execute(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
172 match &self.0 {
173 Some(inner) => inner.execute(response).await,
174 None => Ok(None),
175 }
176 }
177
178 async fn execute_confirmed(&self, response: &str) -> Result<Option<ToolOutput>, ToolError> {
179 match &self.0 {
180 Some(inner) => inner.execute_confirmed(response).await,
181 None => Ok(None),
182 }
183 }
184
185 fn tool_definitions(&self) -> Vec<ToolDef> {
186 self.0
187 .as_ref()
188 .map(ToolExecutor::tool_definitions)
189 .unwrap_or_default()
190 }
191
192 async fn execute_tool_call(&self, call: &ToolCall) -> Result<Option<ToolOutput>, ToolError> {
193 match &self.0 {
194 Some(inner) => inner.execute_tool_call(call).await,
195 None => Ok(None),
196 }
197 }
198
199 async fn execute_tool_call_confirmed(
200 &self,
201 call: &ToolCall,
202 ) -> Result<Option<ToolOutput>, ToolError> {
203 match &self.0 {
204 Some(inner) => inner.execute_tool_call_confirmed(call).await,
205 None => Ok(None),
206 }
207 }
208
209 fn is_tool_retryable(&self, tool_id: &str) -> bool {
210 self.0
211 .as_ref()
212 .is_some_and(|inner| inner.is_tool_retryable(tool_id))
213 }
214
215 fn is_tool_speculatable(&self, tool_id: &str) -> bool {
216 self.0
217 .as_ref()
218 .is_some_and(|inner| inner.is_tool_speculatable(tool_id))
219 }
220
221 fn requires_confirmation(&self, call: &ToolCall) -> bool {
222 self.0
223 .as_ref()
224 .is_some_and(|inner| inner.requires_confirmation(call))
225 }
226
227 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
228 if let Some(inner) = &self.0 {
229 inner.set_skill_env(env);
230 }
231 }
232
233 fn set_effective_trust(&self, level: crate::SkillTrustLevel) {
234 if let Some(inner) = &self.0 {
235 inner.set_effective_trust(level);
236 }
237 }
238
239 fn checkpoint_undo(&self, n: usize) -> crate::executor::CheckpointActionResult {
240 self.0.as_ref().map_or_else(
241 crate::executor::CheckpointActionResult::unsupported,
242 |inner| inner.checkpoint_undo(n),
243 )
244 }
245
246 fn checkpoint_redo(&self) -> crate::executor::CheckpointActionResult {
247 self.0.as_ref().map_or_else(
248 crate::executor::CheckpointActionResult::unsupported,
249 ToolExecutor::checkpoint_redo,
250 )
251 }
252
253 fn checkpoint_list(&self) -> crate::executor::CheckpointListResult {
254 self.0
255 .as_ref()
256 .map(ToolExecutor::checkpoint_list)
257 .unwrap_or_default()
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::ToolName;
265 use std::assert_matches;
266
267 #[derive(Debug)]
268 struct MatchingExecutor;
269 impl ToolExecutor for MatchingExecutor {
270 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
271 Ok(Some(ToolOutput {
272 tool_name: ToolName::new("test"),
273 summary: "matched".to_owned(),
274 blocks_executed: 1,
275 filter_stats: None,
276 diff: None,
277 streamed: false,
278 terminal_id: None,
279 locations: None,
280 raw_response: None,
281 claim_source: None,
282 ..Default::default()
283 }))
284 }
285
286 crate::tool_executor_no_inner_defaults!();
287 }
288
289 #[derive(Debug)]
290 struct NoMatchExecutor;
291 impl ToolExecutor for NoMatchExecutor {
292 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
293 Ok(None)
294 }
295
296 crate::tool_executor_no_inner_defaults!();
297 }
298
299 #[derive(Debug)]
300 struct ErrorExecutor;
301 impl ToolExecutor for ErrorExecutor {
302 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
303 Err(ToolError::Blocked {
304 command: "test".to_owned(),
305 })
306 }
307
308 crate::tool_executor_no_inner_defaults!();
309 }
310
311 #[derive(Debug)]
312 struct SecondExecutor;
313 impl ToolExecutor for SecondExecutor {
314 async fn execute(&self, _response: &str) -> Result<Option<ToolOutput>, ToolError> {
315 Ok(Some(ToolOutput {
316 tool_name: ToolName::new("test"),
317 summary: "second".to_owned(),
318 blocks_executed: 1,
319 filter_stats: None,
320 diff: None,
321 streamed: false,
322 terminal_id: None,
323 locations: None,
324 raw_response: None,
325 claim_source: None,
326 ..Default::default()
327 }))
328 }
329
330 crate::tool_executor_no_inner_defaults!();
331 }
332
333 #[tokio::test]
334 async fn first_matches_returns_first() {
335 let composite = CompositeExecutor::new(MatchingExecutor, SecondExecutor);
336 let result = composite.execute("anything").await.unwrap();
337 assert_eq!(result.unwrap().summary, "matched");
338 }
339
340 #[tokio::test]
341 async fn first_none_falls_through_to_second() {
342 let composite = CompositeExecutor::new(NoMatchExecutor, SecondExecutor);
343 let result = composite.execute("anything").await.unwrap();
344 assert_eq!(result.unwrap().summary, "second");
345 }
346
347 #[tokio::test]
348 async fn both_none_returns_none() {
349 let composite = CompositeExecutor::new(NoMatchExecutor, NoMatchExecutor);
350 let result = composite.execute("anything").await.unwrap();
351 assert!(result.is_none());
352 }
353
354 #[tokio::test]
355 async fn first_error_propagates_without_trying_second() {
356 let composite = CompositeExecutor::new(ErrorExecutor, SecondExecutor);
357 let result = composite.execute("anything").await;
358 assert_matches!(result, Err(ToolError::Blocked { .. }));
359 }
360
361 #[tokio::test]
362 async fn second_error_propagates_when_first_none() {
363 let composite = CompositeExecutor::new(NoMatchExecutor, ErrorExecutor);
364 let result = composite.execute("anything").await;
365 assert_matches!(result, Err(ToolError::Blocked { .. }));
366 }
367
368 #[tokio::test]
369 async fn execute_confirmed_first_matches() {
370 let composite = CompositeExecutor::new(MatchingExecutor, SecondExecutor);
371 let result = composite.execute_confirmed("anything").await.unwrap();
372 assert_eq!(result.unwrap().summary, "matched");
373 }
374
375 #[tokio::test]
376 async fn execute_confirmed_falls_through() {
377 let composite = CompositeExecutor::new(NoMatchExecutor, SecondExecutor);
378 let result = composite.execute_confirmed("anything").await.unwrap();
379 assert_eq!(result.unwrap().summary, "second");
380 }
381
382 #[test]
383 fn composite_debug() {
384 let composite = CompositeExecutor::new(MatchingExecutor, SecondExecutor);
385 let debug = format!("{composite:?}");
386 assert!(debug.contains("CompositeExecutor"));
387 }
388
389 #[derive(Debug, Default)]
394 struct ConfirmedSpy {
395 confirmed_called: std::sync::Mutex<bool>,
396 unconfirmed_called: std::sync::Mutex<bool>,
397 }
398 impl ToolExecutor for ConfirmedSpy {
399 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
400 Ok(None)
401 }
402 async fn execute_tool_call(
403 &self,
404 call: &ToolCall,
405 ) -> Result<Option<ToolOutput>, ToolError> {
406 *self.unconfirmed_called.lock().unwrap() = true;
407 Ok(Some(ToolOutput {
408 tool_name: call.tool_id.clone(),
409 summary: "unconfirmed".to_owned(),
410 blocks_executed: 1,
411 filter_stats: None,
412 diff: None,
413 streamed: false,
414 terminal_id: None,
415 locations: None,
416 raw_response: None,
417 claim_source: None,
418 ..Default::default()
419 }))
420 }
421 async fn execute_tool_call_confirmed(
422 &self,
423 call: &ToolCall,
424 ) -> Result<Option<ToolOutput>, ToolError> {
425 *self.confirmed_called.lock().unwrap() = true;
426 Ok(Some(ToolOutput {
427 tool_name: call.tool_id.clone(),
428 summary: "confirmed".to_owned(),
429 blocks_executed: 1,
430 filter_stats: None,
431 diff: None,
432 streamed: false,
433 terminal_id: None,
434 locations: None,
435 raw_response: None,
436 claim_source: None,
437 ..Default::default()
438 }))
439 }
440
441 fn checkpoint_undo(&self, _n: usize) -> crate::CheckpointActionResult {
442 crate::CheckpointActionResult::unsupported()
443 }
444 fn checkpoint_redo(&self) -> crate::CheckpointActionResult {
445 crate::CheckpointActionResult::unsupported()
446 }
447 fn checkpoint_list(&self) -> crate::CheckpointListResult {
448 crate::CheckpointListResult::default()
449 }
450 fn is_tool_speculatable(&self, _tool_id: &str) -> bool {
451 false
452 }
453 fn requires_confirmation(&self, _call: &ToolCall) -> bool {
454 false
455 }
456 }
457
458 #[tokio::test]
459 async fn execute_tool_call_confirmed_bypasses_unconfirmed_dispatch() {
460 let spy = ConfirmedSpy::default();
461 let composite = CompositeExecutor::new(spy, NoMatchExecutor);
462 let call = ToolCall {
463 tool_id: ToolName::new("read"),
464 params: serde_json::Map::new(),
465 caller_id: None,
466 context: None,
467 tool_call_id: String::new(),
468 skill_name: None,
469 };
470 let result = composite
471 .execute_tool_call_confirmed(&call)
472 .await
473 .unwrap()
474 .unwrap();
475 assert_eq!(result.summary, "confirmed");
476 assert!(
477 *composite.first.confirmed_called.lock().unwrap(),
478 "execute_tool_call_confirmed must reach the inner executor's confirmed override"
479 );
480 assert!(
481 !*composite.first.unconfirmed_called.lock().unwrap(),
482 "execute_tool_call_confirmed must NOT re-dispatch through execute_tool_call"
483 );
484 }
485
486 #[tokio::test]
487 async fn execute_tool_call_confirmed_falls_through_to_second() {
488 let composite = CompositeExecutor::new(NoMatchExecutor, ConfirmedSpy::default());
489 let call = ToolCall {
490 tool_id: ToolName::new("read"),
491 params: serde_json::Map::new(),
492 caller_id: None,
493 context: None,
494 tool_call_id: String::new(),
495 skill_name: None,
496 };
497 let result = composite
498 .execute_tool_call_confirmed(&call)
499 .await
500 .unwrap()
501 .unwrap();
502 assert_eq!(result.summary, "confirmed");
503 assert!(*composite.second.confirmed_called.lock().unwrap());
504 }
505
506 #[derive(Debug)]
507 struct FileToolExecutor;
508 impl ToolExecutor for FileToolExecutor {
509 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
510 Ok(None)
511 }
512 async fn execute_tool_call(
513 &self,
514 call: &ToolCall,
515 ) -> Result<Option<ToolOutput>, ToolError> {
516 if call.tool_id == "read" || call.tool_id == "write" {
517 Ok(Some(ToolOutput {
518 tool_name: call.tool_id.clone(),
519 summary: "file_handler".to_owned(),
520 blocks_executed: 1,
521 filter_stats: None,
522 diff: None,
523 streamed: false,
524 terminal_id: None,
525 locations: None,
526 raw_response: None,
527 claim_source: None,
528 ..Default::default()
529 }))
530 } else {
531 Ok(None)
532 }
533 }
534
535 crate::tool_executor_no_inner_defaults!();
536 }
537
538 #[derive(Debug)]
539 struct ShellToolExecutor;
540 impl ToolExecutor for ShellToolExecutor {
541 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
542 Ok(None)
543 }
544 async fn execute_tool_call(
545 &self,
546 call: &ToolCall,
547 ) -> Result<Option<ToolOutput>, ToolError> {
548 if call.tool_id == "bash" {
549 Ok(Some(ToolOutput {
550 tool_name: ToolName::new("bash"),
551 summary: "shell_handler".to_owned(),
552 blocks_executed: 1,
553 filter_stats: None,
554 diff: None,
555 streamed: false,
556 terminal_id: None,
557 locations: None,
558 raw_response: None,
559 claim_source: None,
560 ..Default::default()
561 }))
562 } else {
563 Ok(None)
564 }
565 }
566
567 crate::tool_executor_no_inner_defaults!();
568 }
569
570 #[tokio::test]
571 async fn tool_call_routes_to_file_executor() {
572 let composite = CompositeExecutor::new(FileToolExecutor, ShellToolExecutor);
573 let call = ToolCall {
574 tool_id: ToolName::new("read"),
575 params: serde_json::Map::new(),
576 caller_id: None,
577 context: None,
578
579 tool_call_id: String::new(),
580 skill_name: None,
581 };
582 let result = composite.execute_tool_call(&call).await.unwrap().unwrap();
583 assert_eq!(result.summary, "file_handler");
584 }
585
586 #[tokio::test]
587 async fn tool_call_routes_to_shell_executor() {
588 let composite = CompositeExecutor::new(FileToolExecutor, ShellToolExecutor);
589 let call = ToolCall {
590 tool_id: ToolName::new("bash"),
591 params: serde_json::Map::new(),
592 caller_id: None,
593 context: None,
594
595 tool_call_id: String::new(),
596 skill_name: None,
597 };
598 let result = composite.execute_tool_call(&call).await.unwrap().unwrap();
599 assert_eq!(result.summary, "shell_handler");
600 }
601
602 #[tokio::test]
603 async fn tool_call_unhandled_returns_none() {
604 let composite = CompositeExecutor::new(FileToolExecutor, ShellToolExecutor);
605 let call = ToolCall {
606 tool_id: ToolName::new("unknown"),
607 params: serde_json::Map::new(),
608 caller_id: None,
609 context: None,
610
611 tool_call_id: String::new(),
612 skill_name: None,
613 };
614 let result = composite.execute_tool_call(&call).await.unwrap();
615 assert!(result.is_none());
616 }
617
618 mod state_forwarding {
624 use super::*;
625 use crate::SkillTrustLevel;
626 use std::sync::Mutex;
627
628 #[derive(Debug, Default)]
629 struct SpyExecutor {
630 last_env: Mutex<Option<std::collections::HashMap<String, String>>>,
631 last_trust: Mutex<Option<SkillTrustLevel>>,
632 }
633 impl ToolExecutor for SpyExecutor {
634 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
635 Ok(None)
636 }
637 fn set_skill_env(&self, env: Option<std::collections::HashMap<String, String>>) {
638 *self.last_env.lock().unwrap() = env;
639 }
640 fn set_effective_trust(&self, level: SkillTrustLevel) {
641 *self.last_trust.lock().unwrap() = Some(level);
642 }
643
644 crate::tool_executor_no_inner_defaults!();
645 }
646
647 #[derive(Debug)]
653 struct FixedConfirmation(bool);
654 impl ToolExecutor for FixedConfirmation {
655 async fn execute(&self, _: &str) -> Result<Option<ToolOutput>, ToolError> {
656 Ok(None)
657 }
658 fn requires_confirmation(&self, _call: &ToolCall) -> bool {
659 self.0
660 }
661
662 async fn execute_tool_call_confirmed(
663 &self,
664 call: &ToolCall,
665 ) -> Result<Option<ToolOutput>, ToolError> {
666 self.execute_tool_call(call).await
667 }
668 fn checkpoint_undo(&self, _n: usize) -> crate::CheckpointActionResult {
669 crate::CheckpointActionResult::unsupported()
670 }
671 fn checkpoint_redo(&self) -> crate::CheckpointActionResult {
672 crate::CheckpointActionResult::unsupported()
673 }
674 fn checkpoint_list(&self) -> crate::CheckpointListResult {
675 crate::CheckpointListResult::default()
676 }
677 fn is_tool_speculatable(&self, _tool_id: &str) -> bool {
678 false
679 }
680 }
681
682 fn confirmation_call() -> ToolCall {
683 ToolCall {
684 tool_id: ToolName::new("shell"),
685 params: serde_json::Map::new(),
686 caller_id: None,
687 context: None,
688 tool_call_id: String::new(),
689 skill_name: None,
690 }
691 }
692
693 #[test]
694 fn requires_confirmation_false_when_both_leaves_false() {
695 let composite =
696 CompositeExecutor::new(FixedConfirmation(false), FixedConfirmation(false));
697 assert!(!composite.requires_confirmation(&confirmation_call()));
698 }
699
700 #[test]
701 fn requires_confirmation_true_when_first_leaf_true() {
702 let composite =
703 CompositeExecutor::new(FixedConfirmation(true), FixedConfirmation(false));
704 assert!(composite.requires_confirmation(&confirmation_call()));
705 }
706
707 #[test]
708 fn requires_confirmation_true_when_second_leaf_true() {
709 let composite =
710 CompositeExecutor::new(FixedConfirmation(false), FixedConfirmation(true));
711 assert!(composite.requires_confirmation(&confirmation_call()));
712 }
713
714 #[test]
715 fn requires_confirmation_or_forwards_across_nested_composition() {
716 let nested = CompositeExecutor::new(FixedConfirmation(false), FixedConfirmation(true));
717 let outer = CompositeExecutor::new(nested, FixedConfirmation(false));
718 assert!(
719 outer.requires_confirmation(&confirmation_call()),
720 "a confirmation requirement on a nested leaf must reach the outer composite"
721 );
722 }
723
724 #[test]
725 fn set_skill_env_reaches_both_inner_executors_in_nested_composition() {
726 let leaf_a = SpyExecutor::default();
729 let leaf_b = SpyExecutor::default();
730 let leaf_c = SpyExecutor::default();
731 let nested = CompositeExecutor::new(leaf_a, leaf_b);
732 let outer = CompositeExecutor::new(nested, leaf_c);
733
734 let mut env = std::collections::HashMap::new();
735 env.insert("GITHUB_TOKEN".to_owned(), "tok".to_owned());
736 outer.set_skill_env(Some(env.clone()));
737
738 assert_eq!(
740 outer.first.first.last_env.lock().unwrap().as_ref(),
741 Some(&env)
742 );
743 assert_eq!(
745 outer.first.second.last_env.lock().unwrap().as_ref(),
746 Some(&env)
747 );
748 assert_eq!(outer.second.last_env.lock().unwrap().as_ref(), Some(&env));
750 }
751
752 #[test]
753 fn set_effective_trust_reaches_both_inner_executors_in_nested_composition() {
754 let leaf_a = SpyExecutor::default();
755 let leaf_b = SpyExecutor::default();
756 let outer = CompositeExecutor::new(leaf_a, leaf_b);
757
758 outer.set_effective_trust(SkillTrustLevel::Quarantined);
759
760 assert_eq!(
761 *outer.first.last_trust.lock().unwrap(),
762 Some(SkillTrustLevel::Quarantined)
763 );
764 assert_eq!(
765 *outer.second.last_trust.lock().unwrap(),
766 Some(SkillTrustLevel::Quarantined)
767 );
768 }
769 }
770
771 mod optional_executor {
772 use super::*;
773
774 #[tokio::test]
775 async fn none_execute_returns_ok_none() {
776 let wrapped: OptionalExecutor<MatchingExecutor> = OptionalExecutor(None);
777 assert!(wrapped.execute("anything").await.unwrap().is_none());
778 }
779
780 #[tokio::test]
781 async fn some_execute_delegates_to_inner() {
782 let wrapped = OptionalExecutor(Some(MatchingExecutor));
783 let result = wrapped.execute("anything").await.unwrap();
784 assert_eq!(result.unwrap().summary, "matched");
785 }
786
787 #[tokio::test]
788 async fn none_execute_tool_call_returns_ok_none() {
789 let wrapped: OptionalExecutor<FileToolExecutor> = OptionalExecutor(None);
790 let call = ToolCall {
791 tool_id: ToolName::new("read"),
792 params: serde_json::Map::new(),
793 caller_id: None,
794 context: None,
795 tool_call_id: String::new(),
796 skill_name: None,
797 };
798 assert!(wrapped.execute_tool_call(&call).await.unwrap().is_none());
799 }
800
801 #[tokio::test]
802 async fn some_execute_tool_call_delegates_to_inner() {
803 let wrapped = OptionalExecutor(Some(FileToolExecutor));
804 let call = ToolCall {
805 tool_id: ToolName::new("read"),
806 params: serde_json::Map::new(),
807 caller_id: None,
808 context: None,
809 tool_call_id: String::new(),
810 skill_name: None,
811 };
812 let result = wrapped.execute_tool_call(&call).await.unwrap();
813 assert_eq!(result.unwrap().summary, "file_handler");
814 }
815
816 #[test]
817 fn none_tool_definitions_is_empty() {
818 let wrapped: OptionalExecutor<MatchingExecutor> = OptionalExecutor(None);
819 assert!(wrapped.tool_definitions().is_empty());
820 }
821
822 #[test]
823 fn none_checkpoint_undo_unsupported() {
824 let wrapped: OptionalExecutor<MatchingExecutor> = OptionalExecutor(None);
825 assert!(!wrapped.checkpoint_undo(1).supported);
826 assert!(!wrapped.checkpoint_redo().supported);
827 assert!(!wrapped.checkpoint_list().supported);
828 }
829
830 #[test]
831 fn none_not_retryable_or_speculatable() {
832 let wrapped: OptionalExecutor<MatchingExecutor> = OptionalExecutor(None);
833 assert!(!wrapped.is_tool_retryable("anything"));
834 assert!(!wrapped.is_tool_speculatable("anything"));
835 }
836
837 #[test]
838 fn none_requires_confirmation_false() {
839 let wrapped: OptionalExecutor<MatchingExecutor> = OptionalExecutor(None);
840 let call = ToolCall {
841 tool_id: ToolName::new("anything"),
842 params: serde_json::Map::new(),
843 caller_id: None,
844 context: None,
845 tool_call_id: String::new(),
846 skill_name: None,
847 };
848 assert!(!wrapped.requires_confirmation(&call));
849 }
850 }
851}