1use chrono::{DateTime, Utc};
10use once_cell::sync::Lazy;
11use regex::Regex;
12use serde::{Deserialize, Serialize};
13use std::fs::{self, OpenOptions};
14use std::io::Write;
15use std::path::PathBuf;
16use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
17use std::sync::RwLock;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct YoloConfig {
22 pub enabled: bool,
24 pub max_operations: usize,
26 pub max_hours: f64,
28 pub forbidden_operations: Vec<String>,
30 pub protected_paths: Vec<String>,
32 pub allow_git_push: bool,
34 pub allow_destructive_shell: bool,
36 pub audit_log_path: Option<PathBuf>,
38 pub status_interval: usize,
40}
41
42impl Default for YoloConfig {
43 fn default() -> Self {
44 Self {
45 enabled: false,
46 max_operations: 0, max_hours: 0.0, forbidden_operations: vec![
49 "rm -rf /".to_string(),
51 "rm -rf /*".to_string(),
52 "dd if=/dev/zero".to_string(),
53 "mkfs".to_string(),
54 "> /dev/sda".to_string(),
55 "chmod -R 777 /".to_string(),
56 ],
57 protected_paths: vec![
58 "/etc".to_string(),
59 "/usr".to_string(),
60 "/bin".to_string(),
61 "/sbin".to_string(),
62 "/boot".to_string(),
63 "/root".to_string(),
64 "~/.ssh".to_string(),
65 "~/.gnupg".to_string(),
66 ],
67 allow_git_push: true,
68 allow_destructive_shell: false,
70 audit_log_path: None,
71 status_interval: 100,
72 }
73 }
74}
75
76impl YoloConfig {
77 pub fn for_coding() -> Self {
82 Self {
83 enabled: true,
84 allow_git_push: false, allow_destructive_shell: false, status_interval: 50,
87 ..Default::default()
88 }
89 }
90
91 pub fn fully_autonomous() -> Self {
101 Self {
102 enabled: true,
103 allow_git_push: true,
104 allow_destructive_shell: false, status_interval: 100,
106 ..Default::default()
107 }
108 }
109
110 pub fn with_destructive_shell(mut self, allow: bool) -> Self {
116 self.allow_destructive_shell = allow;
117 self
118 }
119
120 pub fn with_git_push(mut self, allow: bool) -> Self {
122 self.allow_git_push = allow;
123 self
124 }
125
126 pub fn is_forbidden(&self, operation: &str) -> bool {
131 let normalized = normalize_input(operation);
132 self.forbidden_operations.iter().any(|f| {
133 let pattern = build_boundary_pattern(f);
134 Regex::new(&pattern)
135 .map(|re| re.is_match(&normalized))
136 .unwrap_or(false)
137 })
138 }
139
140 pub fn is_protected_path(&self, path: &str) -> bool {
142 let expanded = expand_home(path);
143 self.protected_paths.iter().any(|p| {
144 let protected = expand_home(p);
145 expanded.starts_with(&protected) || expanded == protected
146 })
147 }
148}
149
150fn normalize_input(input: &str) -> String {
155 static WS_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s+").unwrap());
156 WS_RE.replace_all(input.trim(), " ").to_lowercase()
157}
158
159fn build_boundary_pattern(pattern: &str) -> String {
166 let trimmed = pattern.trim().to_lowercase();
167 let tokens: Vec<&str> = trimmed.split_whitespace().collect();
168 if tokens.is_empty() {
169 return String::new();
170 }
171
172 let escaped_tokens: Vec<String> = tokens.iter().map(|t| regex::escape(t)).collect();
174 let flexible = escaped_tokens.join(r"\s+");
175
176 let first_char = tokens.first().and_then(|t| t.chars().next());
178 let last_char = tokens.last().and_then(|t| t.chars().last());
179
180 let prefix = if first_char.is_some_and(|c| c.is_alphanumeric() || c == '_') {
181 r"\b"
182 } else {
183 ""
184 };
185 let suffix = if last_char.is_some_and(|c| c.is_alphanumeric() || c == '_') {
186 r"\b"
187 } else {
188 ""
189 };
190
191 format!("(?i){}{}{}", prefix, flexible, suffix)
192}
193
194fn expand_home(path: &str) -> String {
196 if path.starts_with("~/") {
197 if let Some(home) = std::env::var_os("HOME") {
198 return format!("{}{}", home.to_string_lossy(), &path[1..]);
199 }
200 }
201 path.to_string()
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct AuditEntry {
207 pub timestamp: DateTime<Utc>,
208 pub operation_id: usize,
209 pub tool_name: String,
210 pub arguments_summary: String,
211 pub auto_approved: bool,
212 pub result: AuditResult,
213 pub duration_ms: u64,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub enum AuditResult {
218 Success,
219 Failed(String),
220 Blocked(String),
221}
222
223pub struct YoloManager {
225 config: YoloConfig,
226 enabled: AtomicBool,
227 operation_count: AtomicUsize,
228 start_time: RwLock<Option<std::time::Instant>>,
229 audit_log: RwLock<Vec<AuditEntry>>,
230}
231
232impl YoloManager {
233 pub fn new(config: YoloConfig) -> Self {
235 let enabled = config.enabled;
236 Self {
237 config,
238 enabled: AtomicBool::new(enabled),
239 operation_count: AtomicUsize::new(0),
240 start_time: RwLock::new(if enabled {
241 Some(std::time::Instant::now())
242 } else {
243 None
244 }),
245 audit_log: RwLock::new(Vec::new()),
246 }
247 }
248
249 pub fn is_active(&self) -> bool {
251 if !self.enabled.load(Ordering::SeqCst) {
252 return false;
253 }
254
255 if self.config.max_operations > 0
257 && self.operation_count.load(Ordering::SeqCst) >= self.config.max_operations
258 {
259 return false;
260 }
261
262 if self.config.max_hours > 0.0 {
264 if let Ok(start) = self.start_time.read() {
265 if let Some(start_time) = *start {
266 let hours = start_time.elapsed().as_secs_f64() / 3600.0;
267 if hours >= self.config.max_hours {
268 return false;
269 }
270 }
271 }
272 }
273
274 true
275 }
276
277 pub fn enable(&self) {
279 self.enabled.store(true, Ordering::SeqCst);
280 if let Ok(mut start) = self.start_time.write() {
281 *start = Some(std::time::Instant::now());
282 }
283 self.operation_count.store(0, Ordering::SeqCst);
284 }
285
286 pub fn disable(&self) {
288 self.enabled.store(false, Ordering::SeqCst);
289 }
290
291 pub fn should_auto_approve(&self, tool_name: &str, args: &serde_json::Value) -> YoloDecision {
293 if !self.is_active() {
294 return YoloDecision::RequireConfirmation("YOLO mode not active".to_string());
295 }
296
297 let args_str = serde_json::to_string(args).unwrap_or_default();
299 if self.config.is_forbidden(&args_str) {
300 return YoloDecision::Block("Operation is in forbidden list".to_string());
301 }
302
303 if let Some(path) = extract_path(args) {
305 if self.config.is_protected_path(&path) {
306 return YoloDecision::Block(format!("Path '{}' is protected", path));
307 }
308 }
309
310 if tool_name == "git_push" && !self.config.allow_git_push {
312 return YoloDecision::RequireConfirmation("Git push requires confirmation".to_string());
313 }
314
315 if tool_name == "shell_exec" {
317 if let Some(cmd) = args.get("command").and_then(|c| c.as_str()) {
318 if is_destructive_command(cmd) && !self.config.allow_destructive_shell {
319 return YoloDecision::RequireConfirmation(
320 "Destructive shell command requires confirmation".to_string(),
321 );
322 }
323 }
324 }
325
326 YoloDecision::AutoApprove
327 }
328
329 pub fn record_operation(
331 &self,
332 tool_name: &str,
333 args: &serde_json::Value,
334 auto_approved: bool,
335 result: AuditResult,
336 duration_ms: u64,
337 ) {
338 let op_id = self.operation_count.fetch_add(1, Ordering::SeqCst);
339
340 let entry = AuditEntry {
341 timestamp: Utc::now(),
342 operation_id: op_id,
343 tool_name: tool_name.to_string(),
344 arguments_summary: summarize_args(args),
345 auto_approved,
346 result,
347 duration_ms,
348 };
349
350 let _file_guard = if self.config.audit_log_path.is_some() {
352 Some(AUDIT_FILE_LOCK.lock().unwrap_or_else(|e| e.into_inner()))
353 } else {
354 None
355 };
356
357 if let Ok(mut log) = self.audit_log.write() {
359 log.push(entry.clone());
360 }
361
362 if let Some(ref path) = self.config.audit_log_path {
364 let _ = append_to_audit_file(path, &entry);
365 }
366
367 if self.config.status_interval > 0
369 && op_id > 0
370 && op_id.is_multiple_of(self.config.status_interval)
371 {
372 self.print_status();
373 }
374 }
375
376 pub fn operation_count(&self) -> usize {
378 self.operation_count.load(Ordering::SeqCst)
379 }
380
381 pub fn elapsed_hours(&self) -> f64 {
383 if let Ok(start) = self.start_time.read() {
384 if let Some(start_time) = *start {
385 return start_time.elapsed().as_secs_f64() / 3600.0;
386 }
387 }
388 0.0
389 }
390
391 pub fn print_status(&self) {
393 let ops = self.operation_count();
394 let hours = self.elapsed_hours();
395 let success_count = self
396 .audit_log
397 .read()
398 .map(|log| {
399 log.iter()
400 .filter(|e| matches!(e.result, AuditResult::Success))
401 .count()
402 })
403 .unwrap_or(0);
404 let failed_count = self
405 .audit_log
406 .read()
407 .map(|log| {
408 log.iter()
409 .filter(|e| matches!(e.result, AuditResult::Failed(_)))
410 .count()
411 })
412 .unwrap_or(0);
413
414 eprintln!("\n╔══════════════════════════════════════╗");
415 eprintln!("║ YOLO MODE STATUS UPDATE ║");
416 eprintln!("╠══════════════════════════════════════╣");
417 eprintln!("║ Operations: {:<6} | Time: {:.1}h ║", ops, hours);
418 eprintln!(
419 "║ Success: {:<4} | Failed: {:<4} ║",
420 success_count, failed_count
421 );
422 eprintln!("╚══════════════════════════════════════╝\n");
423 }
424
425 pub fn audit_summary(&self) -> AuditSummary {
427 let log = self.audit_log.read().unwrap_or_else(|e| e.into_inner());
428
429 let mut tools_used: std::collections::HashMap<String, usize> =
430 std::collections::HashMap::new();
431 let mut success = 0;
432 let mut failed = 0;
433 let mut blocked = 0;
434 let mut total_duration_ms = 0u64;
435
436 for entry in log.iter() {
437 *tools_used.entry(entry.tool_name.clone()).or_insert(0) += 1;
438 total_duration_ms += entry.duration_ms;
439 match &entry.result {
440 AuditResult::Success => success += 1,
441 AuditResult::Failed(_) => failed += 1,
442 AuditResult::Blocked(_) => blocked += 1,
443 }
444 }
445
446 AuditSummary {
447 total_operations: log.len(),
448 success,
449 failed,
450 blocked,
451 tools_used,
452 total_duration_ms,
453 elapsed_hours: self.elapsed_hours(),
454 }
455 }
456
457 pub fn export_audit_log(&self, path: &std::path::Path) -> std::io::Result<()> {
459 let log = self.audit_log.read().unwrap_or_else(|e| e.into_inner());
460 let json = serde_json::to_string_pretty(&*log).unwrap_or_default();
461 fs::write(path, json)
462 }
463}
464
465#[derive(Debug, Clone, PartialEq)]
467pub enum YoloDecision {
468 AutoApprove,
470 RequireConfirmation(String),
472 Block(String),
474}
475
476#[derive(Debug, Clone, Serialize)]
478pub struct AuditSummary {
479 pub total_operations: usize,
480 pub success: usize,
481 pub failed: usize,
482 pub blocked: usize,
483 pub tools_used: std::collections::HashMap<String, usize>,
484 pub total_duration_ms: u64,
485 pub elapsed_hours: f64,
486}
487
488impl std::fmt::Display for AuditSummary {
489 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490 writeln!(f, "YOLO Mode Audit Summary")?;
491 writeln!(f, "======================")?;
492 writeln!(f, "Total Operations: {}", self.total_operations)?;
493 writeln!(f, " - Success: {}", self.success)?;
494 writeln!(f, " - Failed: {}", self.failed)?;
495 writeln!(f, " - Blocked: {}", self.blocked)?;
496 writeln!(f, "Elapsed Time: {:.2} hours", self.elapsed_hours)?;
497 writeln!(
498 f,
499 "Total Duration: {:.1}s",
500 self.total_duration_ms as f64 / 1000.0
501 )?;
502 writeln!(f, "\nTools Used:")?;
503 for (tool, count) in &self.tools_used {
504 writeln!(f, " - {}: {}", tool, count)?;
505 }
506 Ok(())
507 }
508}
509
510fn extract_path(args: &serde_json::Value) -> Option<String> {
512 match args {
513 serde_json::Value::Object(map) => {
514 for (k, v) in map {
515 if k == "path" || k == "file" || k == "directory" {
516 if let Some(s) = v.as_str() {
517 return Some(s.to_string());
518 }
519 }
520 if let Some(res) = extract_path(v) {
521 return Some(res);
522 }
523 }
524 None
525 }
526 serde_json::Value::Array(arr) => {
527 for v in arr {
528 if let Some(res) = extract_path(v) {
529 return Some(res);
530 }
531 }
532 None
533 }
534 _ => None,
535 }
536}
537
538static DESTRUCTIVE_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
543 let patterns = [
544 "rm -rf",
545 "rm -r",
546 "rmdir",
547 "git push -f",
548 "git push --force",
549 "git reset --hard",
550 "git clean -f",
551 "DROP TABLE",
552 "DROP DATABASE",
553 "DELETE FROM",
554 "TRUNCATE",
555 "> /dev/",
556 "dd if=",
557 ];
558 patterns
559 .iter()
560 .filter_map(|p| {
561 let re_pattern = build_boundary_pattern(p);
562 Regex::new(&re_pattern).ok()
563 })
564 .collect()
565});
566
567fn is_destructive_command(cmd: &str) -> bool {
569 let normalized = normalize_input(cmd);
570 DESTRUCTIVE_PATTERNS
571 .iter()
572 .any(|re| re.is_match(&normalized))
573}
574
575fn summarize_args(args: &serde_json::Value) -> String {
577 let mut summary = serde_json::Map::new();
578
579 if let Some(obj) = args.as_object() {
580 for (key, value) in obj {
581 let summarized = match value {
582 serde_json::Value::String(s) if s.len() > 100 => {
583 serde_json::Value::String(format!(
584 "{}... ({} chars)",
585 s.chars().take(100).collect::<String>(),
586 s.len()
587 ))
588 }
589 other => other.clone(),
590 };
591 summary.insert(key.clone(), summarized);
592 }
593 }
594
595 serde_json::to_string(&summary).unwrap_or_else(|_| "{}".to_string())
596}
597
598fn append_to_audit_file(path: &PathBuf, entry: &AuditEntry) -> std::io::Result<()> {
602 let json = serde_json::to_string(entry).unwrap_or_default();
603 let line = format!("{}\n", json);
604
605 let mut file = OpenOptions::new().create(true).append(true).open(path)?;
606 file.write_all(line.as_bytes())?;
607 file.flush()
608}
609
610static AUDIT_FILE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616
617 #[test]
618 fn test_yolo_config_default() {
619 let config = YoloConfig::default();
620 assert!(!config.enabled);
621 assert!(config.allow_git_push);
622 }
623
624 #[test]
625 fn test_yolo_config_for_coding() {
626 let config = YoloConfig::for_coding();
627 assert!(config.enabled);
628 assert!(!config.allow_git_push); }
630
631 #[test]
632 fn test_is_forbidden() {
633 let config = YoloConfig::default();
634 assert!(config.is_forbidden("rm -rf /"));
635 assert!(config.is_forbidden("sudo rm -rf /"));
636 assert!(!config.is_forbidden("rm file.txt"));
637 }
638
639 #[test]
640 fn test_is_protected_path() {
641 let config = YoloConfig::default();
642 assert!(config.is_protected_path("/etc/passwd"));
643 assert!(config.is_protected_path("/usr/bin/bash"));
644 assert!(!config.is_protected_path("/home/user/project"));
645 }
646
647 #[test]
648 fn test_yolo_manager_inactive_by_default() {
649 let config = YoloConfig::default();
650 let manager = YoloManager::new(config);
651 assert!(!manager.is_active());
652 }
653
654 #[test]
655 fn test_yolo_manager_enable_disable() {
656 let config = YoloConfig {
657 enabled: true,
658 ..Default::default()
659 };
660 let manager = YoloManager::new(config);
661
662 assert!(manager.is_active());
663 manager.disable();
664 assert!(!manager.is_active());
665 manager.enable();
666 assert!(manager.is_active());
667 }
668
669 #[test]
670 fn test_auto_approve_when_active() {
671 let config = YoloConfig::fully_autonomous();
672 let manager = YoloManager::new(config);
673
674 let args = serde_json::json!({"path": "/home/user/test.txt"});
675 let decision = manager.should_auto_approve("file_read", &args);
676
677 assert_eq!(decision, YoloDecision::AutoApprove);
678 }
679
680 #[test]
681 fn test_block_forbidden_operation() {
682 let config = YoloConfig::fully_autonomous();
683 let manager = YoloManager::new(config);
684
685 let args = serde_json::json!({"command": "rm -rf /"});
686 let decision = manager.should_auto_approve("shell_exec", &args);
687
688 assert!(matches!(decision, YoloDecision::Block(_)));
689 }
690
691 #[test]
692 fn test_block_protected_path() {
693 let config = YoloConfig::fully_autonomous();
694 let manager = YoloManager::new(config);
695
696 let args = serde_json::json!({"path": "/etc/passwd"});
697 let decision = manager.should_auto_approve("file_write", &args);
698
699 assert!(matches!(decision, YoloDecision::Block(_)));
700 }
701
702 #[test]
703 fn test_require_confirmation_git_push() {
704 let config = YoloConfig::for_coding(); let manager = YoloManager::new(config);
706
707 let args = serde_json::json!({"branch": "main"});
708 let decision = manager.should_auto_approve("git_push", &args);
709
710 assert!(matches!(decision, YoloDecision::RequireConfirmation(_)));
711 }
712
713 #[test]
714 fn test_operation_counting() {
715 let config = YoloConfig::fully_autonomous();
716 let manager = YoloManager::new(config);
717
718 assert_eq!(manager.operation_count(), 0);
719
720 manager.record_operation(
721 "file_read",
722 &serde_json::json!({"path": "test.txt"}),
723 true,
724 AuditResult::Success,
725 100,
726 );
727
728 assert_eq!(manager.operation_count(), 1);
729 }
730
731 #[test]
732 fn test_max_operations_limit() {
733 let mut config = YoloConfig::fully_autonomous();
734 config.max_operations = 2;
735 let manager = YoloManager::new(config);
736
737 assert!(manager.is_active());
738
739 manager.record_operation("t1", &serde_json::json!({}), true, AuditResult::Success, 0);
740 assert!(manager.is_active());
741
742 manager.record_operation("t2", &serde_json::json!({}), true, AuditResult::Success, 0);
743 assert!(!manager.is_active()); }
745
746 #[test]
747 fn test_audit_summary() {
748 let config = YoloConfig::fully_autonomous();
749 let manager = YoloManager::new(config);
750
751 manager.record_operation(
752 "file_read",
753 &serde_json::json!({}),
754 true,
755 AuditResult::Success,
756 50,
757 );
758 manager.record_operation(
759 "file_write",
760 &serde_json::json!({}),
761 true,
762 AuditResult::Success,
763 100,
764 );
765 manager.record_operation(
766 "shell_exec",
767 &serde_json::json!({}),
768 true,
769 AuditResult::Failed("error".to_string()),
770 200,
771 );
772
773 let summary = manager.audit_summary();
774
775 assert_eq!(summary.total_operations, 3);
776 assert_eq!(summary.success, 2);
777 assert_eq!(summary.failed, 1);
778 assert_eq!(summary.total_duration_ms, 350);
779 }
780
781 #[test]
782 fn test_is_destructive_command() {
783 assert!(is_destructive_command("rm -rf /tmp/test"));
784 assert!(is_destructive_command("git push --force"));
785 assert!(is_destructive_command("DROP TABLE users"));
786 assert!(!is_destructive_command("ls -la"));
787 assert!(!is_destructive_command("cargo test"));
788 }
789
790 #[test]
791 fn test_summarize_args_truncates() {
792 let long_content = "x".repeat(200);
793 let args = serde_json::json!({"content": long_content});
794 let summary = summarize_args(&args);
795
796 assert!(summary.len() < 250);
797 assert!(summary.contains("200 chars"));
798 }
799
800 #[test]
801 fn test_expand_home() {
802 if std::env::var("HOME").is_ok() {
804 let expanded = expand_home("~/test");
805 assert!(!expanded.starts_with("~"));
806 assert!(expanded.ends_with("/test"));
807 }
808 }
809
810 #[test]
811 fn test_yolo_config_default_values() {
812 let config = YoloConfig::default();
813 assert!(!config.enabled);
814 assert_eq!(config.max_operations, 0);
815 assert!((config.max_hours - 0.0).abs() < f64::EPSILON);
816 assert!(config.allow_git_push);
817 assert!(!config.allow_destructive_shell);
818 assert!(config.audit_log_path.is_none());
819 assert_eq!(config.status_interval, 100);
820 }
821
822 #[test]
823 fn test_yolo_config_for_coding_values() {
824 let config = YoloConfig::for_coding();
825 assert!(config.enabled);
826 assert!(!config.allow_git_push);
827 assert!(!config.allow_destructive_shell);
828 assert_eq!(config.status_interval, 50);
829 }
830
831 #[test]
832 fn test_yolo_config_fully_autonomous() {
833 let config = YoloConfig::fully_autonomous();
834 assert!(config.enabled);
835 assert!(config.allow_git_push);
836 assert!(!config.allow_destructive_shell);
837 }
838
839 #[test]
840 fn test_yolo_config_with_destructive_shell() {
841 let config = YoloConfig::for_coding().with_destructive_shell(true);
842 assert!(config.allow_destructive_shell);
843
844 let config2 = YoloConfig::for_coding().with_destructive_shell(false);
845 assert!(!config2.allow_destructive_shell);
846 }
847
848 #[test]
849 fn test_yolo_config_with_git_push() {
850 let config = YoloConfig::for_coding().with_git_push(true);
851 assert!(config.allow_git_push);
852
853 let config2 = YoloConfig::fully_autonomous().with_git_push(false);
854 assert!(!config2.allow_git_push);
855 }
856
857 #[test]
858 fn test_is_forbidden_case_insensitive() {
859 let config = YoloConfig::default();
860 assert!(config.is_forbidden("RM -RF /"));
861 assert!(config.is_forbidden("DD IF=/DEV/ZERO"));
862 assert!(!config.is_forbidden("ls -la"));
863 }
864
865 #[test]
866 fn test_yolo_decision_eq() {
867 assert_eq!(YoloDecision::AutoApprove, YoloDecision::AutoApprove);
868 assert_ne!(
869 YoloDecision::AutoApprove,
870 YoloDecision::Block("x".to_string())
871 );
872 }
873
874 #[test]
875 fn test_yolo_decision_debug() {
876 let decision = YoloDecision::RequireConfirmation("test".to_string());
877 let debug_str = format!("{:?}", decision);
878 assert!(debug_str.contains("RequireConfirmation"));
879 }
880
881 #[test]
882 fn test_audit_result_variants() {
883 let success = AuditResult::Success;
884 let failed = AuditResult::Failed("error".to_string());
885 let blocked = AuditResult::Blocked("protected".to_string());
886
887 let _ = format!("{:?}", success);
888 let _ = format!("{:?}", failed);
889 let _ = format!("{:?}", blocked);
890 }
891
892 #[test]
893 fn test_audit_entry_clone() {
894 let entry = AuditEntry {
895 timestamp: Utc::now(),
896 operation_id: 1,
897 tool_name: "test".to_string(),
898 arguments_summary: "args".to_string(),
899 auto_approved: true,
900 result: AuditResult::Success,
901 duration_ms: 100,
902 };
903
904 let cloned = entry.clone();
905 assert_eq!(entry.operation_id, cloned.operation_id);
906 assert_eq!(entry.tool_name, cloned.tool_name);
907 }
908
909 #[test]
910 fn test_audit_entry_serde() {
911 let entry = AuditEntry {
912 timestamp: Utc::now(),
913 operation_id: 1,
914 tool_name: "file_read".to_string(),
915 arguments_summary: "path: test.txt".to_string(),
916 auto_approved: true,
917 result: AuditResult::Success,
918 duration_ms: 50,
919 };
920
921 let json = serde_json::to_string(&entry).unwrap();
922 assert!(json.contains("file_read"));
923 assert!(json.contains("operation_id"));
924
925 let parsed: AuditEntry = serde_json::from_str(&json).unwrap();
926 assert_eq!(parsed.tool_name, entry.tool_name);
927 }
928
929 #[test]
930 fn test_yolo_config_clone() {
931 let config = YoloConfig::fully_autonomous();
932 let cloned = config.clone();
933 assert_eq!(config.enabled, cloned.enabled);
934 assert_eq!(config.allow_git_push, cloned.allow_git_push);
935 }
936
937 #[test]
938 fn test_yolo_config_serde() {
939 let config = YoloConfig::for_coding();
940 let json = serde_json::to_string(&config).unwrap();
941 assert!(json.contains("enabled"));
942
943 let parsed: YoloConfig = serde_json::from_str(&json).unwrap();
944 assert_eq!(parsed.enabled, config.enabled);
945 }
946
947 #[test]
948 fn test_audit_summary_fields() {
949 let summary = AuditSummary {
950 total_operations: 10,
951 success: 8,
952 failed: 1,
953 blocked: 1,
954 tools_used: std::collections::HashMap::new(),
955 total_duration_ms: 5000,
956 elapsed_hours: 1.5,
957 };
958
959 let debug_str = format!("{:?}", summary);
960 assert!(debug_str.contains("total_operations"));
961 }
962
963 #[test]
964 fn test_require_confirmation_destructive_shell() {
965 let config = YoloConfig::fully_autonomous().with_destructive_shell(false);
966 let manager = YoloManager::new(config);
967
968 let args = serde_json::json!({"command": "rm -rf ./test"});
969 let decision = manager.should_auto_approve("shell_exec", &args);
970
971 assert!(matches!(decision, YoloDecision::RequireConfirmation(_)));
972 }
973
974 #[test]
975 fn test_allow_destructive_shell_when_enabled() {
976 let config = YoloConfig::fully_autonomous().with_destructive_shell(true);
977 let manager = YoloManager::new(config);
978
979 let args = serde_json::json!({"command": "rm -rf ./test_dir"});
981 let decision = manager.should_auto_approve("shell_exec", &args);
982
983 assert_eq!(decision, YoloDecision::AutoApprove);
986 }
987
988 #[test]
989 fn test_yolo_manager_with_audit_log_path() {
990 let config = YoloConfig {
991 enabled: true,
992 audit_log_path: Some(PathBuf::from("/tmp/test_audit.log")),
993 ..Default::default()
994 };
995 let manager = YoloManager::new(config);
996 assert!(manager.is_active());
997 }
998
999 #[test]
1000 fn test_protected_paths_include_ssh() {
1001 let config = YoloConfig::default();
1002 if std::env::var("HOME").is_ok() {
1004 let expanded = expand_home("~/.ssh/id_rsa");
1005 assert!(
1006 config.is_protected_path(&expanded) || config.is_protected_path("~/.ssh/id_rsa")
1007 );
1008 }
1009 }
1010
1011 #[test]
1012 fn test_expand_home_no_tilde() {
1013 let path = "/absolute/path";
1014 let expanded = expand_home(path);
1015 assert_eq!(expanded, path);
1016 }
1017}