1use crate::agent::AgentEvent;
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15use tokio::sync::{broadcast, oneshot, RwLock};
16
17pub use crate::queue::SessionLane;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
22pub enum TimeoutAction {
23 #[default]
25 Reject,
26 AutoApprove,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ConfirmationPolicy {
37 pub enabled: bool,
39
40 pub default_timeout_ms: u64,
42
43 pub timeout_action: TimeoutAction,
45
46 pub yolo_lanes: HashSet<SessionLane>,
50}
51
52impl Default for ConfirmationPolicy {
53 fn default() -> Self {
54 Self {
55 enabled: false, default_timeout_ms: 30_000, timeout_action: TimeoutAction::Reject,
58 yolo_lanes: HashSet::new(), }
60 }
61}
62
63impl ConfirmationPolicy {
64 pub fn enabled() -> Self {
66 Self {
67 enabled: true,
68 ..Default::default()
69 }
70 }
71
72 pub fn with_yolo_lanes(mut self, lanes: impl IntoIterator<Item = SessionLane>) -> Self {
74 self.yolo_lanes = lanes.into_iter().collect();
75 self
76 }
77
78 pub fn with_timeout(mut self, timeout_ms: u64, action: TimeoutAction) -> Self {
80 self.default_timeout_ms = timeout_ms;
81 self.timeout_action = action;
82 self
83 }
84
85 pub fn is_yolo(&self, tool_name: &str) -> bool {
90 if !self.enabled {
91 return true; }
93 let lane = SessionLane::from_tool_name(tool_name);
94 self.yolo_lanes.contains(&lane)
95 }
96
97 pub fn requires_confirmation(&self, tool_name: &str) -> bool {
102 !self.is_yolo(tool_name)
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ConfirmationResponse {
109 pub approved: bool,
111 pub reason: Option<String>,
113}
114
115#[async_trait::async_trait]
120pub trait ConfirmationProvider: Send + Sync {
121 async fn requires_confirmation(&self, tool_name: &str) -> bool;
123
124 async fn request_confirmation(
128 &self,
129 tool_id: &str,
130 tool_name: &str,
131 args: &serde_json::Value,
132 ) -> oneshot::Receiver<ConfirmationResponse>;
133
134 async fn confirm(
139 &self,
140 tool_id: &str,
141 approved: bool,
142 reason: Option<String>,
143 ) -> Result<bool, String>;
144
145 async fn policy(&self) -> ConfirmationPolicy;
147
148 async fn set_policy(&self, policy: ConfirmationPolicy);
150
151 async fn check_timeouts(&self) -> usize;
153
154 async fn cancel_all(&self) -> usize;
156}
157
158pub struct PendingConfirmation {
160 pub tool_id: String,
162 pub tool_name: String,
164 pub args: serde_json::Value,
166 pub created_at: Instant,
168 pub timeout_ms: u64,
170 response_tx: oneshot::Sender<ConfirmationResponse>,
172}
173
174impl PendingConfirmation {
175 pub fn is_timed_out(&self) -> bool {
177 self.created_at.elapsed() > Duration::from_millis(self.timeout_ms)
178 }
179
180 pub fn remaining_ms(&self) -> u64 {
182 let elapsed = self.created_at.elapsed().as_millis() as u64;
183 self.timeout_ms.saturating_sub(elapsed)
184 }
185}
186
187pub struct ConfirmationManager {
189 policy: RwLock<ConfirmationPolicy>,
191 pending: Arc<RwLock<HashMap<String, PendingConfirmation>>>,
193 event_tx: broadcast::Sender<AgentEvent>,
195}
196
197impl ConfirmationManager {
198 pub fn new(policy: ConfirmationPolicy, event_tx: broadcast::Sender<AgentEvent>) -> Self {
200 Self {
201 policy: RwLock::new(policy),
202 pending: Arc::new(RwLock::new(HashMap::new())),
203 event_tx,
204 }
205 }
206
207 pub async fn policy(&self) -> ConfirmationPolicy {
209 self.policy.read().await.clone()
210 }
211
212 pub async fn set_policy(&self, policy: ConfirmationPolicy) {
214 *self.policy.write().await = policy;
215 }
216
217 pub async fn requires_confirmation(&self, tool_name: &str) -> bool {
219 self.policy.read().await.requires_confirmation(tool_name)
220 }
221
222 pub async fn request_confirmation(
227 &self,
228 tool_id: &str,
229 tool_name: &str,
230 args: &serde_json::Value,
231 ) -> oneshot::Receiver<ConfirmationResponse> {
232 let (tx, rx) = oneshot::channel();
233
234 let policy = self.policy.read().await;
235 let timeout_ms = policy.default_timeout_ms;
236 drop(policy);
237
238 let pending = PendingConfirmation {
239 tool_id: tool_id.to_string(),
240 tool_name: tool_name.to_string(),
241 args: args.clone(),
242 created_at: Instant::now(),
243 timeout_ms,
244 response_tx: tx,
245 };
246
247 {
249 let mut pending_map = self.pending.write().await;
250 pending_map.insert(tool_id.to_string(), pending);
251 }
252
253 let _ = self.event_tx.send(AgentEvent::ConfirmationRequired {
255 tool_id: tool_id.to_string(),
256 tool_name: tool_name.to_string(),
257 args: args.clone(),
258 timeout_ms,
259 });
260
261 rx
262 }
263
264 pub async fn confirm(
269 &self,
270 tool_id: &str,
271 approved: bool,
272 reason: Option<String>,
273 ) -> Result<bool, String> {
274 let pending = {
275 let mut pending_map = self.pending.write().await;
276 pending_map.remove(tool_id)
277 };
278
279 if let Some(confirmation) = pending {
280 let _ = self.event_tx.send(AgentEvent::ConfirmationReceived {
282 tool_id: tool_id.to_string(),
283 approved,
284 reason: reason.clone(),
285 });
286
287 let response = ConfirmationResponse { approved, reason };
289 let _ = confirmation.response_tx.send(response);
290
291 Ok(true)
292 } else {
293 Ok(false)
294 }
295 }
296
297 pub async fn check_timeouts(&self) -> usize {
301 let policy = self.policy.read().await;
302 let timeout_action = policy.timeout_action;
303 drop(policy);
304
305 let mut timed_out = Vec::new();
306
307 {
309 let pending_map = self.pending.read().await;
310 for (tool_id, pending) in pending_map.iter() {
311 if pending.is_timed_out() {
312 timed_out.push(tool_id.clone());
313 }
314 }
315 }
316
317 for tool_id in &timed_out {
319 let pending = {
320 let mut pending_map = self.pending.write().await;
321 pending_map.remove(tool_id)
322 };
323
324 if let Some(confirmation) = pending {
325 let (approved, action_taken) = match timeout_action {
326 TimeoutAction::Reject => (false, "rejected"),
327 TimeoutAction::AutoApprove => (true, "auto_approved"),
328 };
329
330 let _ = self.event_tx.send(AgentEvent::ConfirmationTimeout {
332 tool_id: tool_id.clone(),
333 action_taken: action_taken.to_string(),
334 });
335
336 let response = ConfirmationResponse {
338 approved,
339 reason: Some(format!("Confirmation timed out, action: {}", action_taken)),
340 };
341 let _ = confirmation.response_tx.send(response);
342 }
343 }
344
345 timed_out.len()
346 }
347
348 pub async fn pending_count(&self) -> usize {
350 self.pending.read().await.len()
351 }
352
353 pub async fn pending_confirmations(&self) -> Vec<(String, String, u64)> {
355 let pending_map = self.pending.read().await;
356 pending_map
357 .values()
358 .map(|p| (p.tool_id.clone(), p.tool_name.clone(), p.remaining_ms()))
359 .collect()
360 }
361
362 pub async fn cancel(&self, tool_id: &str) -> bool {
364 let pending = {
365 let mut pending_map = self.pending.write().await;
366 pending_map.remove(tool_id)
367 };
368
369 if let Some(confirmation) = pending {
370 let response = ConfirmationResponse {
371 approved: false,
372 reason: Some("Confirmation cancelled".to_string()),
373 };
374 let _ = confirmation.response_tx.send(response);
375 true
376 } else {
377 false
378 }
379 }
380
381 pub async fn cancel_all(&self) -> usize {
383 let pending_list: Vec<_> = {
384 let mut pending_map = self.pending.write().await;
385 pending_map.drain().collect()
386 };
387
388 let count = pending_list.len();
389
390 for (_, confirmation) in pending_list {
391 let response = ConfirmationResponse {
392 approved: false,
393 reason: Some("Confirmation cancelled".to_string()),
394 };
395 let _ = confirmation.response_tx.send(response);
396 }
397
398 count
399 }
400}
401
402#[async_trait::async_trait]
404impl ConfirmationProvider for ConfirmationManager {
405 async fn requires_confirmation(&self, tool_name: &str) -> bool {
406 self.requires_confirmation(tool_name).await
407 }
408
409 async fn request_confirmation(
410 &self,
411 tool_id: &str,
412 tool_name: &str,
413 args: &serde_json::Value,
414 ) -> oneshot::Receiver<ConfirmationResponse> {
415 self.request_confirmation(tool_id, tool_name, args).await
416 }
417
418 async fn confirm(
419 &self,
420 tool_id: &str,
421 approved: bool,
422 reason: Option<String>,
423 ) -> Result<bool, String> {
424 self.confirm(tool_id, approved, reason).await
425 }
426
427 async fn policy(&self) -> ConfirmationPolicy {
428 self.policy().await
429 }
430
431 async fn set_policy(&self, policy: ConfirmationPolicy) {
432 self.set_policy(policy).await
433 }
434
435 async fn check_timeouts(&self) -> usize {
436 self.check_timeouts().await
437 }
438
439 async fn cancel_all(&self) -> usize {
440 self.cancel_all().await
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
453 fn test_session_lane() {
454 assert_eq!(SessionLane::from_tool_name("read"), SessionLane::Query);
455 assert_eq!(SessionLane::from_tool_name("grep"), SessionLane::Query);
456 assert_eq!(SessionLane::from_tool_name("bash"), SessionLane::Execute);
457 assert_eq!(SessionLane::from_tool_name("write"), SessionLane::Execute);
458 }
459
460 #[test]
461 fn test_session_lane_priority() {
462 assert_eq!(SessionLane::Control.priority(), 0);
463 assert_eq!(SessionLane::Query.priority(), 1);
464 assert_eq!(SessionLane::Execute.priority(), 2);
465 assert_eq!(SessionLane::Generate.priority(), 3);
466
467 assert!(SessionLane::Control.priority() < SessionLane::Query.priority());
469 assert!(SessionLane::Query.priority() < SessionLane::Execute.priority());
470 assert!(SessionLane::Execute.priority() < SessionLane::Generate.priority());
471 }
472
473 #[test]
474 fn test_session_lane_all_query() {
475 let query_tools = ["read", "glob", "ls", "grep", "list_files", "search"];
476 for tool in query_tools {
477 assert_eq!(
478 SessionLane::from_tool_name(tool),
479 SessionLane::Query,
480 "Tool '{}' should be in Query lane",
481 tool
482 );
483 }
484 }
485
486 #[test]
487 fn test_session_lane_all_execute() {
488 let execute_tools = ["bash", "write", "edit", "delete", "move", "copy", "execute"];
489 for tool in execute_tools {
490 assert_eq!(
491 SessionLane::from_tool_name(tool),
492 SessionLane::Execute,
493 "Tool '{}' should be in Execute lane",
494 tool
495 );
496 }
497 }
498
499 #[test]
508 fn test_confirmation_policy_default() {
509 let policy = ConfirmationPolicy::default();
510 assert!(!policy.enabled);
511 assert!(!policy.requires_confirmation("bash"));
513 assert!(!policy.requires_confirmation("write"));
514 assert!(!policy.requires_confirmation("read"));
515 }
516
517 #[test]
518 fn test_confirmation_policy_enabled() {
519 let policy = ConfirmationPolicy::enabled();
520 assert!(policy.enabled);
521 assert!(policy.requires_confirmation("bash"));
523 assert!(policy.requires_confirmation("write"));
524 assert!(policy.requires_confirmation("read"));
525 assert!(policy.requires_confirmation("grep"));
526 }
527
528 #[test]
529 fn test_confirmation_policy_yolo_mode() {
530 let policy = ConfirmationPolicy::enabled().with_yolo_lanes([SessionLane::Execute]);
531
532 assert!(!policy.requires_confirmation("bash")); assert!(!policy.requires_confirmation("write")); assert!(policy.requires_confirmation("read")); }
536
537 #[test]
538 fn test_confirmation_policy_yolo_multiple_lanes() {
539 let policy = ConfirmationPolicy::enabled()
540 .with_yolo_lanes([SessionLane::Query, SessionLane::Execute]);
541
542 assert!(!policy.requires_confirmation("bash")); assert!(!policy.requires_confirmation("read")); assert!(!policy.requires_confirmation("grep")); }
547
548 #[test]
549 fn test_confirmation_policy_is_yolo() {
550 let policy = ConfirmationPolicy::enabled().with_yolo_lanes([SessionLane::Execute]);
551
552 assert!(policy.is_yolo("bash")); assert!(policy.is_yolo("write")); assert!(!policy.is_yolo("read")); }
556
557 #[test]
558 fn test_confirmation_policy_disabled_is_always_yolo() {
559 let policy = ConfirmationPolicy::default(); assert!(policy.is_yolo("bash"));
561 assert!(policy.is_yolo("read"));
562 assert!(policy.is_yolo("unknown_tool"));
563 }
564
565 #[test]
566 fn test_confirmation_policy_with_timeout() {
567 let policy = ConfirmationPolicy::enabled().with_timeout(5000, TimeoutAction::AutoApprove);
568
569 assert_eq!(policy.default_timeout_ms, 5000);
570 assert_eq!(policy.timeout_action, TimeoutAction::AutoApprove);
571 }
572
573 #[tokio::test]
578 async fn test_confirmation_manager_no_hitl() {
579 let (event_tx, _) = broadcast::channel(100);
580 let manager = ConfirmationManager::new(ConfirmationPolicy::default(), event_tx);
581
582 assert!(!manager.requires_confirmation("bash").await);
583 }
584
585 #[tokio::test]
586 async fn test_confirmation_manager_with_hitl() {
587 let (event_tx, _) = broadcast::channel(100);
588 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
589
590 assert!(manager.requires_confirmation("bash").await);
592 assert!(manager.requires_confirmation("read").await);
593 }
594
595 #[tokio::test]
596 async fn test_confirmation_manager_with_yolo() {
597 let (event_tx, _) = broadcast::channel(100);
598 let policy = ConfirmationPolicy::enabled().with_yolo_lanes([SessionLane::Query]);
599 let manager = ConfirmationManager::new(policy, event_tx);
600
601 assert!(manager.requires_confirmation("bash").await); assert!(!manager.requires_confirmation("read").await); }
604
605 #[tokio::test]
606 async fn test_confirmation_manager_policy_update() {
607 let (event_tx, _) = broadcast::channel(100);
608 let manager = ConfirmationManager::new(ConfirmationPolicy::default(), event_tx);
609
610 assert!(!manager.requires_confirmation("bash").await);
612
613 manager.set_policy(ConfirmationPolicy::enabled()).await;
615 assert!(manager.requires_confirmation("bash").await);
616
617 manager
619 .set_policy(ConfirmationPolicy::enabled().with_yolo_lanes([SessionLane::Execute]))
620 .await;
621 assert!(!manager.requires_confirmation("bash").await);
622 }
623
624 #[tokio::test]
629 async fn test_confirmation_flow_approve() {
630 let (event_tx, mut event_rx) = broadcast::channel(100);
631 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
632
633 let rx = manager
635 .request_confirmation("tool-1", "bash", &serde_json::json!({"command": "ls"}))
636 .await;
637
638 let event = event_rx.recv().await.unwrap();
640 match event {
641 AgentEvent::ConfirmationRequired {
642 tool_id,
643 tool_name,
644 timeout_ms,
645 ..
646 } => {
647 assert_eq!(tool_id, "tool-1");
648 assert_eq!(tool_name, "bash");
649 assert_eq!(timeout_ms, 30_000); }
651 _ => panic!("Expected ConfirmationRequired event"),
652 }
653
654 let result = manager.confirm("tool-1", true, None).await;
656 assert!(result.is_ok());
657 assert!(result.unwrap());
658
659 let event = event_rx.recv().await.unwrap();
661 match event {
662 AgentEvent::ConfirmationReceived {
663 tool_id, approved, ..
664 } => {
665 assert_eq!(tool_id, "tool-1");
666 assert!(approved);
667 }
668 _ => panic!("Expected ConfirmationReceived event"),
669 }
670
671 let response = rx.await.unwrap();
673 assert!(response.approved);
674 assert!(response.reason.is_none());
675 }
676
677 #[tokio::test]
678 async fn test_confirmation_flow_reject() {
679 let (event_tx, mut event_rx) = broadcast::channel(100);
680 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
681
682 let rx = manager
684 .request_confirmation(
685 "tool-1",
686 "bash",
687 &serde_json::json!({"command": "rm -rf /"}),
688 )
689 .await;
690
691 let _ = event_rx.recv().await.unwrap();
693
694 let result = manager
696 .confirm("tool-1", false, Some("Dangerous command".to_string()))
697 .await;
698 assert!(result.is_ok());
699 assert!(result.unwrap());
700
701 let event = event_rx.recv().await.unwrap();
703 match event {
704 AgentEvent::ConfirmationReceived {
705 tool_id,
706 approved,
707 reason,
708 } => {
709 assert_eq!(tool_id, "tool-1");
710 assert!(!approved);
711 assert_eq!(reason, Some("Dangerous command".to_string()));
712 }
713 _ => panic!("Expected ConfirmationReceived event"),
714 }
715
716 let response = rx.await.unwrap();
718 assert!(!response.approved);
719 assert_eq!(response.reason, Some("Dangerous command".to_string()));
720 }
721
722 #[tokio::test]
723 async fn test_confirmation_not_found() {
724 let (event_tx, _) = broadcast::channel(100);
725 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
726
727 let result = manager.confirm("non-existent", true, None).await;
729 assert!(result.is_ok());
730 assert!(!result.unwrap()); }
732
733 #[tokio::test]
738 async fn test_multiple_confirmations() {
739 let (event_tx, _) = broadcast::channel(100);
740 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
741
742 let rx1 = manager
744 .request_confirmation("tool-1", "bash", &serde_json::json!({"cmd": "1"}))
745 .await;
746 let rx2 = manager
747 .request_confirmation("tool-2", "write", &serde_json::json!({"cmd": "2"}))
748 .await;
749 let rx3 = manager
750 .request_confirmation("tool-3", "edit", &serde_json::json!({"cmd": "3"}))
751 .await;
752
753 assert_eq!(manager.pending_count().await, 3);
755
756 manager.confirm("tool-1", true, None).await.unwrap();
758 let response1 = rx1.await.unwrap();
759 assert!(response1.approved);
760
761 manager.confirm("tool-2", false, None).await.unwrap();
763 let response2 = rx2.await.unwrap();
764 assert!(!response2.approved);
765
766 manager.confirm("tool-3", true, None).await.unwrap();
768 let response3 = rx3.await.unwrap();
769 assert!(response3.approved);
770
771 assert_eq!(manager.pending_count().await, 0);
773 }
774
775 #[tokio::test]
776 async fn test_pending_confirmations_info() {
777 let (event_tx, _) = broadcast::channel(100);
778 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
779
780 let _rx1 = manager
782 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
783 .await;
784 let _rx2 = manager
785 .request_confirmation("tool-2", "write", &serde_json::json!({}))
786 .await;
787
788 let pending = manager.pending_confirmations().await;
789 assert_eq!(pending.len(), 2);
790
791 let tool_ids: Vec<&str> = pending.iter().map(|(id, _, _)| id.as_str()).collect();
793 assert!(tool_ids.contains(&"tool-1"));
794 assert!(tool_ids.contains(&"tool-2"));
795 }
796
797 #[tokio::test]
802 async fn test_cancel_confirmation() {
803 let (event_tx, _) = broadcast::channel(100);
804 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
805
806 let rx = manager
808 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
809 .await;
810
811 assert_eq!(manager.pending_count().await, 1);
812
813 let cancelled = manager.cancel("tool-1").await;
815 assert!(cancelled);
816 assert_eq!(manager.pending_count().await, 0);
817
818 let response = rx.await.unwrap();
820 assert!(!response.approved);
821 assert_eq!(response.reason, Some("Confirmation cancelled".to_string()));
822 }
823
824 #[tokio::test]
825 async fn test_cancel_nonexistent() {
826 let (event_tx, _) = broadcast::channel(100);
827 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
828
829 let cancelled = manager.cancel("non-existent").await;
830 assert!(!cancelled);
831 }
832
833 #[tokio::test]
834 async fn test_cancel_all() {
835 let (event_tx, _) = broadcast::channel(100);
836 let manager = ConfirmationManager::new(ConfirmationPolicy::enabled(), event_tx);
837
838 let rx1 = manager
840 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
841 .await;
842 let rx2 = manager
843 .request_confirmation("tool-2", "write", &serde_json::json!({}))
844 .await;
845 let rx3 = manager
846 .request_confirmation("tool-3", "edit", &serde_json::json!({}))
847 .await;
848
849 assert_eq!(manager.pending_count().await, 3);
850
851 let cancelled_count = manager.cancel_all().await;
853 assert_eq!(cancelled_count, 3);
854 assert_eq!(manager.pending_count().await, 0);
855
856 for rx in [rx1, rx2, rx3] {
858 let response = rx.await.unwrap();
859 assert!(!response.approved);
860 assert_eq!(response.reason, Some("Confirmation cancelled".to_string()));
861 }
862 }
863
864 #[tokio::test]
869 async fn test_timeout_reject() {
870 let (event_tx, mut event_rx) = broadcast::channel(100);
871 let policy = ConfirmationPolicy {
872 enabled: true,
873 default_timeout_ms: 50, timeout_action: TimeoutAction::Reject,
875 ..Default::default()
876 };
877 let manager = ConfirmationManager::new(policy, event_tx);
878
879 let rx = manager
881 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
882 .await;
883
884 let _ = event_rx.recv().await.unwrap();
886
887 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
889
890 let timed_out = manager.check_timeouts().await;
892 assert_eq!(timed_out, 1);
893
894 let event = event_rx.recv().await.unwrap();
896 match event {
897 AgentEvent::ConfirmationTimeout {
898 tool_id,
899 action_taken,
900 } => {
901 assert_eq!(tool_id, "tool-1");
902 assert_eq!(action_taken, "rejected");
903 }
904 _ => panic!("Expected ConfirmationTimeout event"),
905 }
906
907 let response = rx.await.unwrap();
909 assert!(!response.approved);
910 assert!(response.reason.as_ref().unwrap().contains("timed out"));
911 }
912
913 #[tokio::test]
914 async fn test_timeout_auto_approve() {
915 let (event_tx, mut event_rx) = broadcast::channel(100);
916 let policy = ConfirmationPolicy {
917 enabled: true,
918 default_timeout_ms: 50, timeout_action: TimeoutAction::AutoApprove,
920 ..Default::default()
921 };
922 let manager = ConfirmationManager::new(policy, event_tx);
923
924 let rx = manager
926 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
927 .await;
928
929 let _ = event_rx.recv().await.unwrap();
931
932 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
934
935 let timed_out = manager.check_timeouts().await;
937 assert_eq!(timed_out, 1);
938
939 let event = event_rx.recv().await.unwrap();
941 match event {
942 AgentEvent::ConfirmationTimeout {
943 tool_id,
944 action_taken,
945 } => {
946 assert_eq!(tool_id, "tool-1");
947 assert_eq!(action_taken, "auto_approved");
948 }
949 _ => panic!("Expected ConfirmationTimeout event"),
950 }
951
952 let response = rx.await.unwrap();
954 assert!(response.approved);
955 assert!(response.reason.as_ref().unwrap().contains("auto_approved"));
956 }
957
958 #[tokio::test]
959 async fn test_no_timeout_when_confirmed() {
960 let (event_tx, _) = broadcast::channel(100);
961 let policy = ConfirmationPolicy {
962 enabled: true,
963 default_timeout_ms: 50,
964 timeout_action: TimeoutAction::Reject,
965 ..Default::default()
966 };
967 let manager = ConfirmationManager::new(policy, event_tx);
968
969 let rx = manager
971 .request_confirmation("tool-1", "bash", &serde_json::json!({}))
972 .await;
973
974 manager.confirm("tool-1", true, None).await.unwrap();
976
977 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
979
980 let timed_out = manager.check_timeouts().await;
982 assert_eq!(timed_out, 0);
983
984 let response = rx.await.unwrap();
986 assert!(response.approved);
987 assert!(response.reason.is_none());
988 }
989}