1mod audit;
4mod benchmark;
5mod chain;
6mod ci;
7mod deploy;
8mod deploy_safe;
9mod doctor;
10mod fuzz;
11mod gas;
12mod import;
13mod install_hook;
14mod invariant;
15mod notify;
16mod plugins;
17mod report;
18mod sbom;
19mod scan;
20mod security;
21mod simulate;
22mod upgrade_check;
23mod verify;
24mod watch;
25
26use clap::{Parser, Subcommand};
27use std::path::PathBuf;
28
29#[derive(Parser, Debug)]
31#[command(
32 name = "forge-guard",
33 version,
34 about = "Pre-deployment smart contract auditing framework for Foundry",
35 long_about = "The most comprehensive pre-deployment smart contract auditing framework for Foundry.\n\nTransforms security auditing from an optional step into a mandatory pre-deployment process.\nBlocks unsafe deployments by default while providing detailed vulnerability reports.",
36 author
37)]
38pub struct Cli {
39 #[command(subcommand)]
40 pub command: Commands,
41}
42
43impl Cli {
44 pub fn from_env() -> Self {
46 Self::parse()
47 }
48
49 pub fn run(&self) -> anyhow::Result<()> {
51 use anyhow::Context;
52 match &self.command {
53 Commands::Audit(args) => audit::run(args).context("Audit failed"),
54 Commands::Deploy(args) => deploy::run(args).context("Deploy failed"),
55 Commands::DeploySafe(args) => deploy_safe::run(args).context("Safe deploy failed"),
56 Commands::Fuzz(args) => fuzz::run(args).context("Fuzzing failed"),
57 Commands::Invariant(args) => invariant::run(args).context("Invariant test failed"),
58 Commands::Simulate(args) => simulate::run(args).context("Simulation failed"),
59 Commands::Gas(args) => gas::run(args).context("Gas analysis failed"),
60 Commands::Report(args) => report::run(args).context("Report generation failed"),
61 Commands::Verify(args) => verify::run(args).context("Verification failed"),
62 Commands::Doctor(args) => doctor::run(args).context("Doctor analysis failed"),
63 Commands::Watch(args) => watch::run(args).context("Watch failed"),
64 Commands::Ci(args) => ci::run(args).context("CI generation failed"),
65 Commands::Benchmark(args) => benchmark::run(args).context("Benchmark failed"),
66 Commands::Scan(args) => scan::run(args).context("Scan failed"),
67 Commands::UpgradeCheck(args) => {
68 upgrade_check::run(args).context("Upgrade check failed")
69 }
70 Commands::Plugins(args) => plugins::run(args).context("Plugin operation failed"),
71 Commands::Chain(args) => chain::run(args).context("Chain operation failed"),
72 Commands::Sbom(args) => sbom::run(args).context("SBOM generation failed"),
73 Commands::InstallHook(args) => install_hook::run(args).context("Install hook failed"),
74 Commands::Security(args) => security::run(args).context("Security operation failed"),
75 Commands::Import(args) => import::run(args).context("Import failed"),
76 Commands::Notify(args) => notify::run(args).context("Notification failed"),
77 }
78 }
79}
80
81#[derive(Subcommand, Debug)]
83pub enum Commands {
84 Audit(AuditArgs),
86 Deploy(DeployArgs),
88 #[command(name = "deploy-safe")]
90 DeploySafe(DeploySafeArgs),
91 Fuzz(FuzzArgs),
93 Invariant(InvariantArgs),
95 Simulate(SimulateArgs),
97 Gas(GasArgs),
99 Report(ReportArgs),
101 Verify(VerifyArgs),
103 Doctor(DoctorArgs),
105 Watch(WatchArgs),
107 Ci(CiArgs),
109 Benchmark(BenchmarkArgs),
111 Scan(ScanArgs),
113 #[command(name = "upgrade-check")]
115 UpgradeCheck(UpgradeCheckArgs),
116 Plugins(PluginArgs),
118 Chain(ChainArgs),
120 Sbom(SbomArgs),
122 #[command(name = "install-hook")]
124 InstallHook(install_hook::InstallHookArgs),
125
126 Security(SecurityArgs),
128 Import(ImportArgs),
130 Notify(NotifyArgs),
132}
133
134#[derive(Debug, Default, clap::Args)]
138pub struct SharedFlags {
139 #[arg(long, global = true, default_value = "ethereum")]
141 pub chain: String,
142
143 #[arg(long, global = true, default_value = ".")]
145 pub project: PathBuf,
146
147 #[arg(long, global = true)]
149 pub json: bool,
150
151 #[arg(long, global = true)]
153 pub markdown: bool,
154
155 #[arg(long, global = true)]
157 pub html: bool,
158
159 #[arg(long, global = true)]
161 pub strict: bool,
162
163 #[arg(long, global = true)]
165 pub offline: bool,
166
167 #[arg(long, global = true)]
169 pub production: bool,
170
171 #[arg(long, global = true)]
173 pub report: bool,
174
175 #[arg(long, global = true, default_value = "4")]
177 pub parallelism: usize,
178}
179
180#[derive(Debug, clap::Args)]
183pub struct AuditArgs {
184 #[command(flatten)]
185 pub shared: SharedFlags,
186
187 #[arg(long, short)]
189 pub full: bool,
190
191 #[arg(long)]
193 pub quick: bool,
194
195 #[arg(long)]
197 pub summary: bool,
198
199 #[arg(long)]
201 pub ai: bool,
202
203 #[arg(long, default_value = "openai")]
205 pub ai_provider: String,
206
207 #[arg(long, default_value = "gpt-5")]
209 pub ai_model: String,
210
211 #[arg(long)]
213 pub ai_api_key: Option<String>,
214
215 #[arg(long)]
217 pub ollama_endpoint: Option<String>,
218
219 #[arg(long)]
221 pub ai_full: bool,
222
223 #[arg(long)]
225 pub exploit: bool,
226
227 #[arg(long)]
229 pub gas: bool,
230
231 #[arg(long)]
233 pub all_chains: bool,
234
235 #[arg(long, default_value = "4")]
237 pub max_parallel_chains: usize,
238
239 #[arg(long, default_value = "src")]
241 pub sources: String,
242
243 #[arg(long)]
245 pub exclude: Option<String>,
246
247 #[arg(long)]
249 pub template: Option<String>,
250
251 #[arg(long)]
253 pub list_templates: bool,
254
255 #[arg(long)]
257 pub suppressions: Option<PathBuf>,
258
259 #[arg(long)]
261 pub show_suppressed: bool,
262
263 #[arg(long)]
265 pub generate_suppressions: bool,
266
267 #[arg(long)]
269 pub notify: bool,
270
271 #[arg(long)]
274 pub enable_history: bool,
275}
276
277#[derive(Debug, clap::Args)]
278pub struct DeployArgs {
279 #[command(flatten)]
280 pub shared: SharedFlags,
281
282 pub contract: Option<String>,
284
285 #[arg(long)]
287 pub force: bool,
288
289 #[arg(long)]
291 pub args: Option<String>,
292
293 #[arg(long)]
295 pub salt: Option<String>,
296
297 #[arg(long)]
299 pub verify: bool,
300
301 #[arg(long)]
303 pub notify: bool,
304}
305
306#[derive(Debug, clap::Args)]
307pub struct DeploySafeArgs {
308 #[command(flatten)]
309 pub shared: SharedFlags,
310
311 pub contract: Option<String>,
313
314 #[arg(long)]
316 pub args: Option<String>,
317
318 #[arg(long)]
320 pub verify: bool,
321
322 #[arg(long)]
324 pub notify: bool,
325}
326
327#[derive(Debug, clap::Args)]
328pub struct FuzzArgs {
329 #[command(flatten)]
330 pub shared: SharedFlags,
331
332 #[arg(long, default_value = "10000")]
334 pub runs: u32,
335
336 #[arg(long)]
338 pub seed: Option<u64>,
339
340 #[arg(long)]
342 pub test: Option<String>,
343
344 pub contract: Option<String>,
346}
347
348#[derive(Debug, clap::Args)]
349pub struct InvariantArgs {
350 #[command(flatten)]
351 pub shared: SharedFlags,
352
353 #[arg(long, default_value = "1000")]
355 pub runs: u32,
356
357 #[arg(long, default_value = "100")]
359 pub depth: u32,
360
361 pub contract: Option<String>,
363
364 #[arg(long)]
366 pub fail_on_revert: bool,
367}
368
369#[derive(Debug, clap::Args)]
370pub struct SimulateArgs {
371 #[command(flatten)]
372 pub shared: SharedFlags,
373
374 #[arg(long, default_value = "100")]
376 pub blocks: u32,
377
378 #[arg(long)]
380 pub deployer: Option<String>,
381
382 #[arg(long)]
384 pub mev: bool,
385
386 pub contract: Option<String>,
388}
389
390#[derive(Debug, clap::Args)]
391pub struct GasArgs {
392 #[command(flatten)]
393 pub shared: SharedFlags,
394
395 pub contract: Option<String>,
397
398 #[arg(long)]
400 pub diff: Option<String>,
401
402 #[arg(long)]
404 pub all: bool,
405
406 #[arg(long, default_value = "50000")]
408 pub warn_threshold: u64,
409}
410
411#[derive(Debug, clap::Args)]
412pub struct ReportArgs {
413 #[command(flatten)]
414 pub shared: SharedFlags,
415
416 pub input: Option<PathBuf>,
418
419 #[arg(long, default_value = "markdown")]
421 pub format: String,
422
423 #[arg(long)]
425 pub output: Option<PathBuf>,
426
427 #[arg(long)]
429 pub exploit_paths: bool,
430
431 #[arg(long)]
433 pub summary: bool,
434
435 #[arg(long)]
437 pub history: bool,
438
439 #[arg(long)]
441 pub regression: bool,
442}
443
444#[derive(Debug, clap::Args)]
445pub struct VerifyArgs {
446 #[command(flatten)]
447 pub shared: SharedFlags,
448
449 pub address: Option<String>,
451
452 pub name: Option<String>,
454
455 #[arg(long)]
457 pub api_key: Option<String>,
458
459 #[arg(long)]
461 pub constructor_args: Option<String>,
462
463 #[arg(long)]
465 pub all: bool,
466}
467
468#[derive(Debug, clap::Args)]
469pub struct DoctorArgs {
470 #[command(flatten)]
471 pub shared: SharedFlags,
472
473 #[arg(long)]
475 pub fix: bool,
476
477 #[arg(long, short)]
479 pub verbose: bool,
480
481 #[arg(long)]
483 pub check: Option<String>,
484
485 #[arg(long)]
487 pub sync: bool,
488
489 #[arg(long)]
491 pub dry_run: bool,
492
493 #[arg(long)]
495 pub diff: bool,
496}
497
498#[derive(Debug, clap::Args)]
499pub struct WatchArgs {
500 #[command(flatten)]
501 pub shared: SharedFlags,
502
503 #[arg(long, default_value = "src")]
505 pub dirs: String,
506
507 #[arg(long, default_value = "500")]
509 pub debounce_ms: u64,
510
511 #[arg(long)]
513 pub exclude: Option<String>,
514
515 #[arg(long)]
517 pub full: bool,
518}
519
520#[derive(Debug, clap::Args)]
521pub struct CiArgs {
522 #[command(flatten)]
523 pub shared: SharedFlags,
524
525 #[arg(long, default_value = "github")]
527 pub platform: String,
528
529 #[arg(long, default_value = ".github/workflows")]
531 pub output: PathBuf,
532
533 #[arg(long)]
535 pub include_deploy: bool,
536
537 #[arg(long)]
539 pub overwrite: bool,
540}
541
542#[derive(Debug, clap::Args)]
543pub struct BenchmarkArgs {
544 #[command(flatten)]
545 pub shared: SharedFlags,
546
547 #[arg(long, default_value = "10")]
549 pub iterations: u32,
550
551 #[arg(long)]
553 pub compare: Option<PathBuf>,
554
555 #[arg(long)]
557 pub save: Option<PathBuf>,
558
559 #[arg(long)]
561 pub module: Option<String>,
562
563 #[arg(long, default_value = "3")]
565 pub warmup: u32,
566}
567
568#[derive(Debug, clap::Args)]
569pub struct SbomArgs {
570 #[command(flatten)]
571 pub shared: SharedFlags,
572
573 #[arg(long, default_value = "cyclonedx")]
575 pub format: String,
576
577 #[arg(long, short)]
579 pub output: Option<PathBuf>,
580
581 #[arg(long)]
583 pub ci: bool,
584}
585
586#[derive(Debug, clap::Args)]
587pub struct ScanArgs {
588 #[command(flatten)]
589 pub shared: SharedFlags,
590
591 #[arg(long, default_value = "1")]
593 pub depth: u32,
594
595 #[arg(long)]
597 pub update: bool,
598
599 #[arg(long)]
601 pub vulnerable_only: bool,
602
603 #[arg(long)]
605 pub fail_fast: bool,
606}
607
608#[derive(Debug, clap::Args)]
609pub struct UpgradeCheckArgs {
610 #[command(flatten)]
611 pub shared: SharedFlags,
612
613 pub proxy: Option<String>,
615
616 pub implementation: Option<String>,
618
619 #[arg(long)]
621 pub all: bool,
622
623 #[arg(long)]
625 pub storage_collision: bool,
626
627 #[arg(long)]
629 pub uups: bool,
630}
631
632#[derive(Debug, clap::Args)]
633pub struct PluginArgs {
634 #[command(flatten)]
635 pub shared: SharedFlags,
636
637 #[command(subcommand)]
639 pub action: Option<PluginAction>,
640}
641
642#[derive(Debug, Subcommand)]
643pub enum PluginAction {
644 List,
646 Install {
648 name: String,
649 source: Option<String>,
650 },
651 Remove { name: String },
653 Enable { name: String },
655 Disable { name: String },
657 New { name: String },
659}
660
661#[derive(Debug, clap::Args)]
662pub struct ChainArgs {
663 #[command(flatten)]
664 pub shared: SharedFlags,
665
666 #[command(subcommand)]
668 pub action: Option<ChainAction>,
669}
670
671#[derive(Debug, Subcommand)]
672pub enum ChainAction {
673 List,
675 Info { chain: String },
677 Add {
679 name: String,
680 rpc_url: Option<String>,
681 chain_id: Option<u64>,
682 },
683 Remove { name: String },
685 Test {
687 chain: String,
688 rpc_url: Option<String>,
689 },
690}
691
692#[derive(Debug, clap::Args)]
693pub struct SecurityArgs {
694 #[command(flatten)]
695 pub shared: SharedFlags,
696
697 #[command(subcommand)]
699 pub action: Option<SecurityAction>,
700}
701
702#[derive(Debug, Subcommand)]
703pub enum SecurityAction {
704 Config,
706 Threshold { score: u8 },
708 Enable { check: String },
710 Disable { check: String },
712 List,
714 Info { check: String },
716}
717
718#[derive(Debug, clap::Args)]
721pub struct ImportArgs {
722 #[command(flatten)]
723 pub shared: SharedFlags,
724
725 #[arg(long, default_value = "slither")]
727 pub from: String,
728
729 pub input: Option<PathBuf>,
731
732 #[arg(long)]
734 pub findings: Option<PathBuf>,
735
736 #[arg(long, short)]
738 pub output: Option<PathBuf>,
739}
740
741#[derive(Debug, clap::Args)]
742pub struct NotifyArgs {
743 #[command(flatten)]
744 pub shared: SharedFlags,
745
746 #[arg(long)]
748 pub webhook: Option<String>,
749
750 #[arg(long)]
752 pub kind: Option<String>,
753
754 #[arg(long)]
756 pub findings: Option<PathBuf>,
757
758 #[arg(long, default_value = "Forge Guard Audit")]
760 pub title: String,
761
762 #[arg(long)]
764 pub message: Option<String>,
765
766 #[arg(long)]
768 pub severity: Option<String>,
769
770 #[arg(long)]
772 pub on_critical: bool,
773
774 #[arg(long)]
776 pub on_high: bool,
777
778 #[arg(long)]
780 pub dry_run: bool,
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786 use clap::Parser;
787
788 #[test]
789 fn test_cli_parse_audit() {
790 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
791 assert!(matches!(cli.command, Commands::Audit(_)));
792 }
793
794 #[test]
795 fn test_cli_parse_deploy() {
796 let cli = Cli::try_parse_from(["forge-guard", "deploy"]).unwrap();
797 assert!(matches!(cli.command, Commands::Deploy(_)));
798 }
799
800 #[test]
801 fn test_cli_parse_deploy_safe() {
802 let cli = Cli::try_parse_from(["forge-guard", "deploy-safe"]).unwrap();
803 assert!(matches!(cli.command, Commands::DeploySafe(_)));
804 }
805
806 #[test]
807 fn test_cli_parse_fuzz() {
808 let cli = Cli::try_parse_from(["forge-guard", "fuzz"]).unwrap();
809 assert!(matches!(cli.command, Commands::Fuzz(_)));
810 }
811
812 #[test]
813 fn test_cli_parse_invariant() {
814 let cli = Cli::try_parse_from(["forge-guard", "invariant"]).unwrap();
815 assert!(matches!(cli.command, Commands::Invariant(_)));
816 }
817
818 #[test]
819 fn test_cli_parse_simulate() {
820 let cli = Cli::try_parse_from(["forge-guard", "simulate"]).unwrap();
821 assert!(matches!(cli.command, Commands::Simulate(_)));
822 }
823
824 #[test]
825 fn test_cli_parse_gas() {
826 let cli = Cli::try_parse_from(["forge-guard", "gas"]).unwrap();
827 assert!(matches!(cli.command, Commands::Gas(_)));
828 }
829
830 #[test]
831 fn test_cli_parse_report() {
832 let cli = Cli::try_parse_from(["forge-guard", "report"]).unwrap();
833 assert!(matches!(cli.command, Commands::Report(_)));
834 }
835
836 #[test]
837 fn test_cli_parse_verify() {
838 let cli = Cli::try_parse_from(["forge-guard", "verify"]).unwrap();
839 assert!(matches!(cli.command, Commands::Verify(_)));
840 }
841
842 #[test]
843 fn test_cli_parse_doctor() {
844 let cli = Cli::try_parse_from(["forge-guard", "doctor"]).unwrap();
845 assert!(matches!(cli.command, Commands::Doctor(_)));
846 }
847
848 #[test]
849 fn test_cli_parse_watch() {
850 let cli = Cli::try_parse_from(["forge-guard", "watch"]).unwrap();
851 assert!(matches!(cli.command, Commands::Watch(_)));
852 }
853
854 #[test]
855 fn test_cli_parse_ci() {
856 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
857 assert!(matches!(cli.command, Commands::Ci(_)));
858 }
859
860 #[test]
861 fn test_cli_parse_benchmark() {
862 let cli = Cli::try_parse_from(["forge-guard", "benchmark"]).unwrap();
863 assert!(matches!(cli.command, Commands::Benchmark(_)));
864 }
865
866 #[test]
867 fn test_cli_parse_scan() {
868 let cli = Cli::try_parse_from(["forge-guard", "scan"]).unwrap();
869 assert!(matches!(cli.command, Commands::Scan(_)));
870 }
871
872 #[test]
873 fn test_cli_parse_upgrade_check() {
874 let cli = Cli::try_parse_from(["forge-guard", "upgrade-check"]).unwrap();
875 assert!(matches!(cli.command, Commands::UpgradeCheck(_)));
876 }
877
878 #[test]
879 fn test_cli_parse_plugins() {
880 let cli = Cli::try_parse_from(["forge-guard", "plugins"]).unwrap();
881 assert!(matches!(cli.command, Commands::Plugins(_)));
882 }
883
884 #[test]
885 fn test_cli_parse_chain() {
886 let cli = Cli::try_parse_from(["forge-guard", "chain"]).unwrap();
887 assert!(matches!(cli.command, Commands::Chain(_)));
888 }
889
890 #[test]
891 fn test_cli_parse_security() {
892 let cli = Cli::try_parse_from(["forge-guard", "security"]).unwrap();
893 assert!(matches!(cli.command, Commands::Security(_)));
894 }
895
896 #[test]
897 fn test_cli_parse_install_hook() {
898 let cli = Cli::try_parse_from(["forge-guard", "install-hook"]).unwrap();
899 assert!(matches!(cli.command, Commands::InstallHook(_)));
900 }
901
902 #[test]
903 fn test_cli_parse_install_hook_uninstall() {
904 let cli = Cli::try_parse_from(["forge-guard", "install-hook", "--uninstall"]).unwrap();
905 assert!(matches!(cli.command, Commands::InstallHook(_)));
906 }
907
908 #[test]
909 fn test_cli_parse_doctor_sync() {
910 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--sync"]).unwrap();
911 if let Commands::Doctor(args) = cli.command {
912 assert!(args.sync);
913 } else {
914 panic!("Expected Doctor command");
915 }
916 }
917
918 #[test]
919 fn test_cli_parse_doctor_dry_run() {
920 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--sync", "--dry-run"]).unwrap();
921 if let Commands::Doctor(args) = cli.command {
922 assert!(args.sync);
923 assert!(args.dry_run);
924 } else {
925 panic!("Expected Doctor command");
926 }
927 }
928
929 #[test]
930 fn test_cli_parse_audit_template() {
931 let cli = Cli::try_parse_from(["forge-guard", "audit", "--template", "defi"]).unwrap();
932 if let Commands::Audit(args) = cli.command {
933 assert_eq!(args.template.as_deref(), Some("defi"));
934 } else {
935 panic!("Expected Audit command");
936 }
937 }
938
939 #[test]
940 fn test_cli_parse_audit_list_templates() {
941 let cli = Cli::try_parse_from(["forge-guard", "audit", "--list-templates"]).unwrap();
942 if let Commands::Audit(args) = cli.command {
943 assert!(args.list_templates);
944 } else {
945 panic!("Expected Audit command");
946 }
947 }
948
949 #[test]
950 fn test_shared_flags_defaults() {
951 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
952 if let Commands::Audit(args) = cli.command {
953 assert_eq!(args.shared.chain, "ethereum");
954 assert!(!args.shared.json);
955 assert!(!args.shared.markdown);
956 assert!(!args.shared.strict);
957 assert!(!args.shared.offline);
958 assert!(!args.shared.production);
959 assert!(!args.shared.report);
960 assert_eq!(args.shared.parallelism, 4);
961 } else {
962 panic!("Expected Audit command");
963 }
964 }
965
966 #[test]
967 fn test_shared_flags_custom() {
968 let cli = Cli::try_parse_from([
969 "forge-guard",
970 "audit",
971 "--chain",
972 "base",
973 "--json",
974 "--strict",
975 "--offline",
976 "--production",
977 "--parallelism",
978 "8",
979 "--project",
980 "/tmp/project",
981 ])
982 .unwrap();
983 if let Commands::Audit(args) = cli.command {
984 assert_eq!(args.shared.chain, "base");
985 assert!(args.shared.json);
986 assert!(args.shared.strict);
987 assert!(args.shared.offline);
988 assert!(args.shared.production);
989 assert_eq!(args.shared.parallelism, 8);
990 assert_eq!(
991 args.shared.project,
992 std::path::PathBuf::from("/tmp/project")
993 );
994 } else {
995 panic!("Expected Audit command");
996 }
997 }
998
999 #[test]
1000 fn test_audit_args_defaults() {
1001 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
1002 if let Commands::Audit(args) = cli.command {
1003 assert!(!args.full);
1004 assert!(!args.quick);
1005 assert!(!args.summary);
1006 assert!(!args.ai);
1007 assert_eq!(args.ai_provider, "openai");
1008 assert_eq!(args.ai_model, "gpt-5");
1009 assert!(args.ai_api_key.is_none());
1010 assert!(!args.exploit);
1011 assert!(!args.gas);
1012 assert!(!args.all_chains);
1013 assert_eq!(args.sources, "src");
1014 assert!(args.exclude.is_none());
1015 } else {
1016 panic!("Expected Audit command");
1017 }
1018 }
1019
1020 #[test]
1021 fn test_audit_args_full() {
1022 let cli = Cli::try_parse_from([
1023 "forge-guard",
1024 "audit",
1025 "--full",
1026 "--chain",
1027 "arbitrum",
1028 "--json",
1029 "--report",
1030 "--exploit",
1031 "--gas",
1032 ])
1033 .unwrap();
1034 if let Commands::Audit(args) = cli.command {
1035 assert!(args.full);
1036 assert!(args.exploit);
1037 assert!(args.gas);
1038 assert!(args.shared.json);
1039 assert!(args.shared.report);
1040 assert_eq!(args.shared.chain, "arbitrum");
1041 } else {
1042 panic!("Expected Audit command");
1043 }
1044 }
1045
1046 #[test]
1047 fn test_audit_ai_args() {
1048 let cli = Cli::try_parse_from([
1049 "forge-guard",
1050 "audit",
1051 "--ai",
1052 "--ai-provider",
1053 "claude",
1054 "--ai-model",
1055 "claude-5-sonnet-20260701",
1056 "--ai-full",
1057 ])
1058 .unwrap();
1059 if let Commands::Audit(args) = cli.command {
1060 assert!(args.ai);
1061 assert_eq!(args.ai_provider, "claude");
1062 assert_eq!(args.ai_model, "claude-5-sonnet-20260701");
1063 assert!(args.ai_full);
1064 } else {
1065 panic!("Expected Audit command");
1066 }
1067 }
1068
1069 #[test]
1070 fn test_deploy_args() {
1071 let cli = Cli::try_parse_from([
1072 "forge-guard",
1073 "deploy",
1074 "MyContract",
1075 "--force",
1076 "--salt",
1077 "0xabc",
1078 "--verify",
1079 "--args",
1080 "arg1,arg2",
1081 ])
1082 .unwrap();
1083 if let Commands::Deploy(args) = cli.command {
1084 assert_eq!(args.contract.as_deref(), Some("MyContract"));
1085 assert!(args.force);
1086 assert_eq!(args.salt.as_deref(), Some("0xabc"));
1087 assert!(args.verify);
1088 assert_eq!(args.args.as_deref(), Some("arg1,arg2"));
1089 } else {
1090 panic!("Expected Deploy command");
1091 }
1092 }
1093
1094 #[test]
1095 fn test_fuzz_args() {
1096 let cli = Cli::try_parse_from([
1097 "forge-guard",
1098 "fuzz",
1099 "--runs",
1100 "50000",
1101 "--seed",
1102 "42",
1103 "--test",
1104 "testFuzzDeposit",
1105 "Vault",
1106 ])
1107 .unwrap();
1108 if let Commands::Fuzz(args) = cli.command {
1109 assert_eq!(args.runs, 50_000);
1110 assert_eq!(args.seed, Some(42));
1111 assert_eq!(args.test.as_deref(), Some("testFuzzDeposit"));
1112 assert_eq!(args.contract.as_deref(), Some("Vault"));
1113 } else {
1114 panic!("Expected Fuzz command");
1115 }
1116 }
1117
1118 #[test]
1119 fn test_simulate_args() {
1120 let cli = Cli::try_parse_from([
1121 "forge-guard",
1122 "simulate",
1123 "MyContract",
1124 "--blocks",
1125 "200",
1126 "--mev",
1127 ])
1128 .unwrap();
1129 if let Commands::Simulate(args) = cli.command {
1130 assert_eq!(args.contract.as_deref(), Some("MyContract"));
1131 assert_eq!(args.blocks, 200);
1132 assert!(args.mev);
1133 } else {
1134 panic!("Expected Simulate command");
1135 }
1136 }
1137
1138 #[test]
1139 fn test_invariant_args() {
1140 let cli = Cli::try_parse_from([
1141 "forge-guard",
1142 "invariant",
1143 "--runs",
1144 "2000",
1145 "--depth",
1146 "150",
1147 "--fail-on-revert",
1148 ])
1149 .unwrap();
1150 if let Commands::Invariant(args) = cli.command {
1151 assert_eq!(args.runs, 2000);
1152 assert_eq!(args.depth, 150);
1153 assert!(args.fail_on_revert);
1154 } else {
1155 panic!("Expected Invariant command");
1156 }
1157 }
1158
1159 #[test]
1160 fn test_ci_args_defaults() {
1161 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
1162 if let Commands::Ci(args) = cli.command {
1163 assert_eq!(args.platform, "github");
1164 assert_eq!(args.output, std::path::PathBuf::from(".github/workflows"));
1165 assert!(!args.include_deploy);
1166 assert!(!args.overwrite);
1167 } else {
1168 panic!("Expected Ci command");
1169 }
1170 }
1171
1172 #[test]
1173 fn test_ci_args_custom() {
1174 let cli = Cli::try_parse_from([
1175 "forge-guard",
1176 "ci",
1177 "--platform",
1178 "gitlab",
1179 "--include-deploy",
1180 "--overwrite",
1181 "--output",
1182 ".gitlab",
1183 ])
1184 .unwrap();
1185 if let Commands::Ci(args) = cli.command {
1186 assert_eq!(args.platform, "gitlab");
1187 assert!(args.include_deploy);
1188 assert!(args.overwrite);
1189 assert_eq!(args.output, std::path::PathBuf::from(".gitlab"));
1190 } else {
1191 panic!("Expected Ci command");
1192 }
1193 }
1194
1195 #[test]
1196 fn test_benchmark_args() {
1197 let cli = Cli::try_parse_from([
1198 "forge-guard",
1199 "benchmark",
1200 "--iterations",
1201 "50",
1202 "--warmup",
1203 "5",
1204 "--module",
1205 "pattern_matching",
1206 ])
1207 .unwrap();
1208 if let Commands::Benchmark(args) = cli.command {
1209 assert_eq!(args.iterations, 50);
1210 assert_eq!(args.warmup, 5);
1211 assert_eq!(args.module.as_deref(), Some("pattern_matching"));
1212 } else {
1213 panic!("Expected Benchmark command");
1214 }
1215 }
1216
1217 #[test]
1218 fn test_gas_args() {
1219 let cli =
1220 Cli::try_parse_from(["forge-guard", "gas", "--all", "--warn-threshold", "100000"])
1221 .unwrap();
1222 if let Commands::Gas(args) = cli.command {
1223 assert!(args.all);
1224 assert_eq!(args.warn_threshold, 100000);
1225 assert!(args.contract.is_none());
1226 } else {
1227 panic!("Expected Gas command");
1228 }
1229 }
1230
1231 #[test]
1232 fn test_scan_args() {
1233 let cli = Cli::try_parse_from([
1234 "forge-guard",
1235 "scan",
1236 "--depth",
1237 "2",
1238 "--update",
1239 "--vulnerable-only",
1240 "--fail-fast",
1241 ])
1242 .unwrap();
1243 if let Commands::Scan(args) = cli.command {
1244 assert_eq!(args.depth, 2);
1245 assert!(args.update);
1246 assert!(args.vulnerable_only);
1247 assert!(args.fail_fast);
1248 } else {
1249 panic!("Expected Scan command");
1250 }
1251 }
1252
1253 #[test]
1254 fn test_upgrade_check_args() {
1255 let cli = Cli::try_parse_from([
1256 "forge-guard",
1257 "upgrade-check",
1258 "--storage-collision",
1259 "--uups",
1260 "--all",
1261 ])
1262 .unwrap();
1263 if let Commands::UpgradeCheck(args) = cli.command {
1264 assert!(args.storage_collision);
1265 assert!(args.uups);
1266 assert!(args.all);
1267 } else {
1268 panic!("Expected UpgradeCheck command");
1269 }
1270 }
1271
1272 #[test]
1273 fn test_doctor_args() {
1274 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--fix", "--verbose"]).unwrap();
1275 if let Commands::Doctor(args) = cli.command {
1276 assert!(args.fix);
1277 assert!(args.verbose);
1278 assert!(args.check.is_none());
1279 } else {
1280 panic!("Expected Doctor command");
1281 }
1282 }
1283
1284 #[test]
1285 fn test_verify_args() {
1286 let cli = Cli::try_parse_from([
1287 "forge-guard",
1288 "verify",
1289 "0x1234",
1290 "MyContract",
1291 "--api-key",
1292 "test-key",
1293 "--all",
1294 ])
1295 .unwrap();
1296 if let Commands::Verify(args) = cli.command {
1297 assert_eq!(args.address.as_deref(), Some("0x1234"));
1298 assert_eq!(args.name.as_deref(), Some("MyContract"));
1299 assert_eq!(args.api_key.as_deref(), Some("test-key"));
1300 assert!(args.all);
1301 } else {
1302 panic!("Expected Verify command");
1303 }
1304 }
1305
1306 #[test]
1307 fn test_watch_args() {
1308 let cli = Cli::try_parse_from([
1309 "forge-guard",
1310 "watch",
1311 "--dirs",
1312 "src,test-contracts",
1313 "--debounce-ms",
1314 "1000",
1315 "--full",
1316 ])
1317 .unwrap();
1318 if let Commands::Watch(args) = cli.command {
1319 assert_eq!(args.dirs, "src,test-contracts");
1320 assert_eq!(args.debounce_ms, 1000);
1321 assert!(args.full);
1322 } else {
1323 panic!("Expected Watch command");
1324 }
1325 }
1326
1327 #[test]
1328 fn test_plugins_list() {
1329 let cli = Cli::try_parse_from(["forge-guard", "plugins", "list"]).unwrap();
1330 if let Commands::Plugins(args) = cli.command {
1331 assert!(matches!(args.action, Some(PluginAction::List)));
1332 } else {
1333 panic!("Expected Plugins command");
1334 }
1335 }
1336
1337 #[test]
1338 fn test_plugins_install() {
1339 let cli = Cli::try_parse_from(["forge-guard", "plugins", "install", "my-plugin"]).unwrap();
1340 if let Commands::Plugins(args) = cli.command {
1341 assert!(matches!(args.action, Some(PluginAction::Install { .. })));
1342 } else {
1343 panic!("Expected Plugins command");
1344 }
1345 }
1346
1347 #[test]
1348 fn test_plugins_remove() {
1349 let cli = Cli::try_parse_from(["forge-guard", "plugins", "remove", "bad-plugin"]).unwrap();
1350 if let Commands::Plugins(args) = cli.command {
1351 assert!(matches!(args.action, Some(PluginAction::Remove { .. })));
1352 } else {
1353 panic!("Expected Plugins command");
1354 }
1355 }
1356
1357 #[test]
1358 fn test_plugins_enable() {
1359 let cli = Cli::try_parse_from(["forge-guard", "plugins", "enable", "my-plugin"]).unwrap();
1360 if let Commands::Plugins(args) = cli.command {
1361 assert!(matches!(args.action, Some(PluginAction::Enable { .. })));
1362 } else {
1363 panic!("Expected Plugins command");
1364 }
1365 }
1366
1367 #[test]
1368 fn test_plugins_disable() {
1369 let cli = Cli::try_parse_from(["forge-guard", "plugins", "disable", "my-plugin"]).unwrap();
1370 if let Commands::Plugins(args) = cli.command {
1371 assert!(matches!(args.action, Some(PluginAction::Disable { .. })));
1372 } else {
1373 panic!("Expected Plugins command");
1374 }
1375 }
1376
1377 #[test]
1378 fn test_plugins_new() {
1379 let cli =
1380 Cli::try_parse_from(["forge-guard", "plugins", "new", "my-awesome-plugin"]).unwrap();
1381 if let Commands::Plugins(args) = cli.command {
1382 assert!(matches!(args.action, Some(PluginAction::New { .. })));
1383 } else {
1384 panic!("Expected Plugins command");
1385 }
1386 }
1387
1388 #[test]
1389 fn test_chain_list() {
1390 let cli = Cli::try_parse_from(["forge-guard", "chain", "list"]).unwrap();
1391 if let Commands::Chain(args) = cli.command {
1392 assert!(matches!(args.action, Some(ChainAction::List)));
1393 } else {
1394 panic!("Expected Chain command");
1395 }
1396 }
1397
1398 #[test]
1399 fn test_chain_info() {
1400 let cli = Cli::try_parse_from(["forge-guard", "chain", "info", "base"]).unwrap();
1401 if let Commands::Chain(args) = cli.command {
1402 assert!(matches!(args.action, Some(ChainAction::Info { .. })));
1403 } else {
1404 panic!("Expected Chain command");
1405 }
1406 }
1407
1408 #[test]
1409 fn test_chain_add() {
1410 let cli = Cli::try_parse_from([
1411 "forge-guard",
1412 "chain",
1413 "add",
1414 "my-chain",
1415 "https://rpc.my-chain.io",
1416 "99999",
1417 ])
1418 .unwrap();
1419 if let Commands::Chain(args) = cli.command {
1420 assert!(matches!(args.action, Some(ChainAction::Add { .. })));
1421 } else {
1422 panic!("Expected Chain command");
1423 }
1424 }
1425
1426 #[test]
1427 fn test_security_list() {
1428 let cli = Cli::try_parse_from(["forge-guard", "security", "list"]).unwrap();
1429 if let Commands::Security(args) = cli.command {
1430 assert!(matches!(args.action, Some(SecurityAction::List)));
1431 } else {
1432 panic!("Expected Security command");
1433 }
1434 }
1435
1436 #[test]
1437 fn test_security_threshold() {
1438 let cli = Cli::try_parse_from(["forge-guard", "security", "threshold", "85"]).unwrap();
1439 if let Commands::Security(args) = cli.command {
1440 assert!(matches!(
1441 args.action,
1442 Some(SecurityAction::Threshold { .. })
1443 ));
1444 } else {
1445 panic!("Expected Security command");
1446 }
1447 }
1448
1449 #[test]
1450 fn test_markdown_flag() {
1451 let cli = Cli::try_parse_from(["forge-guard", "audit", "--markdown", "--report"]).unwrap();
1452 if let Commands::Audit(args) = cli.command {
1453 assert!(args.shared.markdown);
1454 assert!(args.shared.report);
1455 } else {
1456 panic!("Expected Audit command");
1457 }
1458 }
1459
1460 #[test]
1461 fn test_quick_audit() {
1462 let cli = Cli::try_parse_from(["forge-guard", "audit", "--quick", "--summary"]).unwrap();
1463 if let Commands::Audit(args) = cli.command {
1464 assert!(args.quick);
1465 assert!(args.summary);
1466 } else {
1467 panic!("Expected Audit command");
1468 }
1469 }
1470
1471 #[test]
1472 fn test_enable_history_flag() {
1473 let cli = Cli::try_parse_from(["forge-guard", "audit", "--enable-history"]).unwrap();
1474 if let Commands::Audit(args) = cli.command {
1475 assert!(args.enable_history);
1476 } else {
1477 panic!("Expected Audit command");
1478 }
1479 }
1480
1481 #[test]
1482 fn test_report_history_flag() {
1483 let cli = Cli::try_parse_from(["forge-guard", "report", "--history"]).unwrap();
1484 if let Commands::Report(args) = cli.command {
1485 assert!(args.history);
1486 assert!(!args.regression);
1487 } else {
1488 panic!("Expected Report command");
1489 }
1490 }
1491
1492 #[test]
1493 fn test_report_regression_flag() {
1494 let cli = Cli::try_parse_from(["forge-guard", "report", "--regression"]).unwrap();
1495 if let Commands::Report(args) = cli.command {
1496 assert!(args.regression);
1497 assert!(!args.history);
1498 } else {
1499 panic!("Expected Report command");
1500 }
1501 }
1502
1503 #[test]
1504 fn test_all_chains_flag() {
1505 let cli = Cli::try_parse_from(["forge-guard", "audit", "--all-chains"]).unwrap();
1506 if let Commands::Audit(args) = cli.command {
1507 assert!(args.all_chains);
1508 assert_eq!(args.max_parallel_chains, 4);
1509 } else {
1510 panic!("Expected Audit command");
1511 }
1512 }
1513
1514 #[test]
1515 fn test_max_parallel_chains_default() {
1516 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
1517 if let Commands::Audit(args) = cli.command {
1518 assert_eq!(args.max_parallel_chains, 4);
1519 } else {
1520 panic!("Expected Audit command");
1521 }
1522 }
1523
1524 #[test]
1525 fn test_max_parallel_chains_custom() {
1526 let cli = Cli::try_parse_from([
1527 "forge-guard",
1528 "audit",
1529 "--all-chains",
1530 "--max-parallel-chains",
1531 "2",
1532 ])
1533 .unwrap();
1534 if let Commands::Audit(args) = cli.command {
1535 assert!(args.all_chains);
1536 assert_eq!(args.max_parallel_chains, 2);
1537 } else {
1538 panic!("Expected Audit command");
1539 }
1540 }
1541
1542 #[test]
1543 fn test_deploy_safe_args() {
1544 let cli =
1545 Cli::try_parse_from(["forge-guard", "deploy-safe", "SecureVault", "--verify"]).unwrap();
1546 if let Commands::DeploySafe(args) = cli.command {
1547 assert_eq!(args.contract.as_deref(), Some("SecureVault"));
1548 assert!(args.verify);
1549 } else {
1550 panic!("Expected DeploySafe command");
1551 }
1552 }
1553
1554 #[test]
1555 fn test_report_args_json() {
1556 let cli = Cli::try_parse_from(["forge-guard", "report", "--format", "json"]).unwrap();
1557 if let Commands::Report(args) = cli.command {
1558 assert_eq!(args.format, "json");
1559 assert!(!args.summary);
1560 } else {
1561 panic!("Expected Report command");
1562 }
1563 }
1564
1565 #[test]
1566 fn test_verify_all_without_address() {
1567 let cli = Cli::try_parse_from(["forge-guard", "verify", "--all"]).unwrap();
1568 if let Commands::Verify(args) = cli.command {
1569 assert!(args.all);
1570 assert!(args.address.is_none());
1571 } else {
1572 panic!("Expected Verify command");
1573 }
1574 }
1575
1576 #[test]
1577 fn test_report_summary() {
1578 let cli = Cli::try_parse_from(["forge-guard", "report", "--summary"]).unwrap();
1579 if let Commands::Report(args) = cli.command {
1580 assert!(args.summary);
1581 } else {
1582 panic!("Expected Report command");
1583 }
1584 }
1585
1586 #[test]
1587 fn test_scan_vulnerable_only() {
1588 let cli = Cli::try_parse_from(["forge-guard", "scan", "--vulnerable-only"]).unwrap();
1589 if let Commands::Scan(args) = cli.command {
1590 assert!(args.vulnerable_only);
1591 } else {
1592 panic!("Expected Scan command");
1593 }
1594 }
1595
1596 #[test]
1597 fn test_doctor_check_category() {
1598 let cli =
1599 Cli::try_parse_from(["forge-guard", "doctor", "--check", "dependencies"]).unwrap();
1600 if let Commands::Doctor(args) = cli.command {
1601 assert_eq!(args.check.as_deref(), Some("dependencies"));
1602 } else {
1603 panic!("Expected Doctor command");
1604 }
1605 }
1606
1607 #[test]
1608 fn test_gas_contract_specific() {
1609 let cli =
1610 Cli::try_parse_from(["forge-guard", "gas", "Vault", "--diff", "prev.json"]).unwrap();
1611 if let Commands::Gas(args) = cli.command {
1612 assert_eq!(args.contract.as_deref(), Some("Vault"));
1613 assert_eq!(args.diff.as_deref(), Some("prev.json"));
1614 } else {
1615 panic!("Expected Gas command");
1616 }
1617 }
1618
1619 #[test]
1620 fn test_verify_with_constructor_args() {
1621 let cli = Cli::try_parse_from([
1622 "forge-guard",
1623 "verify",
1624 "0xabc",
1625 "Token",
1626 "--constructor-args",
1627 "0x0001",
1628 ])
1629 .unwrap();
1630 if let Commands::Verify(args) = cli.command {
1631 assert_eq!(args.constructor_args.as_deref(), Some("0x0001"));
1632 } else {
1633 panic!("Expected Verify command");
1634 }
1635 }
1636
1637 #[test]
1638 fn test_upgrade_check_proxy() {
1639 let cli =
1640 Cli::try_parse_from(["forge-guard", "upgrade-check", "0xproxy", "0ximpl"]).unwrap();
1641 if let Commands::UpgradeCheck(args) = cli.command {
1642 assert_eq!(args.proxy.as_deref(), Some("0xproxy"));
1643 assert_eq!(args.implementation.as_deref(), Some("0ximpl"));
1644 } else {
1645 panic!("Expected UpgradeCheck command");
1646 }
1647 }
1648
1649 #[test]
1650 fn test_benchmark_save_and_compare() {
1651 let cli = Cli::try_parse_from([
1652 "forge-guard",
1653 "benchmark",
1654 "--save",
1655 "results.json",
1656 "--compare",
1657 "baseline.json",
1658 ])
1659 .unwrap();
1660 if let Commands::Benchmark(args) = cli.command {
1661 assert_eq!(
1662 args.save.as_deref(),
1663 Some(std::path::Path::new("results.json"))
1664 );
1665 assert_eq!(
1666 args.compare.as_deref(),
1667 Some(std::path::Path::new("baseline.json"))
1668 );
1669 } else {
1670 panic!("Expected Benchmark command");
1671 }
1672 }
1673
1674 #[test]
1675 fn test_watch_exclude() {
1676 let cli = Cli::try_parse_from(["forge-guard", "watch", "--exclude", "*.test.sol"]).unwrap();
1677 if let Commands::Watch(args) = cli.command {
1678 assert_eq!(args.exclude.as_deref(), Some("*.test.sol"));
1679 } else {
1680 panic!("Expected Watch command");
1681 }
1682 }
1683
1684 #[test]
1685 fn test_simulate_deployer() {
1686 let cli =
1687 Cli::try_parse_from(["forge-guard", "simulate", "--deployer", "0xdeployer"]).unwrap();
1688 if let Commands::Simulate(args) = cli.command {
1689 assert_eq!(args.deployer.as_deref(), Some("0xdeployer"));
1690 } else {
1691 panic!("Expected Simulate command");
1692 }
1693 }
1694
1695 #[test]
1696 fn test_security_disable_check() {
1697 let cli = Cli::try_parse_from(["forge-guard", "security", "disable", "FA-H-001"]).unwrap();
1698 if let Commands::Security(args) = cli.command {
1699 assert!(matches!(args.action, Some(SecurityAction::Disable { .. })));
1700 } else {
1701 panic!("Expected Security command");
1702 }
1703 }
1704
1705 #[test]
1706 fn test_from_env_try_parse() {
1707 let cli = Cli::try_parse_from(["forge-guard", "audit", "--report", "--json"]).unwrap();
1710 assert!(matches!(cli.command, Commands::Audit(_)));
1711 if let Commands::Audit(args) = cli.command {
1712 assert!(args.shared.report);
1713 assert!(args.shared.json);
1714 }
1715 }
1716
1717 #[test]
1718 fn test_cli_parse_import() {
1719 let cli = Cli::try_parse_from(["forge-guard", "import", "results.json"]).unwrap();
1720 assert!(matches!(cli.command, Commands::Import(_)));
1721 }
1722
1723 #[test]
1724 fn test_import_args_from_slither() {
1725 let cli = Cli::try_parse_from([
1726 "forge-guard",
1727 "import",
1728 "--from",
1729 "mythril",
1730 "mythril_out.json",
1731 "--findings",
1732 "reports/audit.json",
1733 ])
1734 .unwrap();
1735 if let Commands::Import(args) = cli.command {
1736 assert_eq!(args.from, "mythril");
1737 assert_eq!(
1738 args.input.as_deref(),
1739 Some(std::path::Path::new("mythril_out.json"))
1740 );
1741 assert_eq!(
1742 args.findings.as_deref(),
1743 Some(std::path::Path::new("reports/audit.json"))
1744 );
1745 } else {
1746 panic!("Expected Import command");
1747 }
1748 }
1749
1750 #[test]
1751 fn test_import_args_defaults() {
1752 let cli = Cli::try_parse_from(["forge-guard", "import"]).unwrap();
1753 if let Commands::Import(args) = cli.command {
1754 assert_eq!(args.from, "slither");
1755 assert!(args.input.is_none());
1756 assert!(args.findings.is_none());
1757 assert!(args.output.is_none());
1758 } else {
1759 panic!("Expected Import command");
1760 }
1761 }
1762
1763 #[test]
1764 fn test_cli_parse_notify() {
1765 let cli = Cli::try_parse_from(["forge-guard", "notify", "--dry-run"]).unwrap();
1766 assert!(matches!(cli.command, Commands::Notify(_)));
1767 }
1768
1769 #[test]
1770 fn test_notify_args() {
1771 let cli = Cli::try_parse_from([
1772 "forge-guard",
1773 "notify",
1774 "--webhook",
1775 "https://hooks.slack.com/services/T/B/X",
1776 "--findings",
1777 "reports/audit.json",
1778 "--on-critical",
1779 "--title",
1780 "Nightly audit",
1781 ])
1782 .unwrap();
1783 if let Commands::Notify(args) = cli.command {
1784 assert_eq!(
1785 args.webhook.as_deref(),
1786 Some("https://hooks.slack.com/services/T/B/X")
1787 );
1788 assert_eq!(
1789 args.findings.as_deref(),
1790 Some(std::path::Path::new("reports/audit.json"))
1791 );
1792 assert!(args.on_critical);
1793 assert!(!args.on_high);
1794 assert!(!args.dry_run);
1795 assert_eq!(args.title, "Nightly audit");
1796 } else {
1797 panic!("Expected Notify command");
1798 }
1799 }
1800
1801 #[test]
1802 fn test_notify_defaults() {
1803 let cli = Cli::try_parse_from(["forge-guard", "notify"]).unwrap();
1804 if let Commands::Notify(args) = cli.command {
1805 assert_eq!(args.title, "Forge Guard Audit");
1806 assert!(args.webhook.is_none());
1807 assert!(args.kind.is_none());
1808 assert!(args.severity.is_none());
1809 assert!(!args.dry_run);
1810 } else {
1811 panic!("Expected Notify command");
1812 }
1813 }
1814
1815 #[test]
1816 fn test_audit_suppression_flags() {
1817 let cli = Cli::try_parse_from([
1818 "forge-guard",
1819 "audit",
1820 "--suppressions",
1821 ".forge-guard-suppressions",
1822 "--show-suppressed",
1823 ])
1824 .unwrap();
1825 if let Commands::Audit(args) = cli.command {
1826 assert_eq!(
1827 args.suppressions.as_deref(),
1828 Some(std::path::Path::new(".forge-guard-suppressions"))
1829 );
1830 assert!(args.show_suppressed);
1831 assert!(!args.generate_suppressions);
1832 assert!(!args.notify);
1833 } else {
1834 panic!("Expected Audit command");
1835 }
1836 }
1837
1838 #[test]
1839 fn test_audit_generate_suppressions_and_notify() {
1840 let cli = Cli::try_parse_from([
1841 "forge-guard",
1842 "audit",
1843 "--generate-suppressions",
1844 "--notify",
1845 ])
1846 .unwrap();
1847 if let Commands::Audit(args) = cli.command {
1848 assert!(args.generate_suppressions);
1849 assert!(args.notify);
1850 assert!(args.suppressions.is_none());
1851 } else {
1852 panic!("Expected Audit command");
1853 }
1854 }
1855
1856 #[test]
1857 fn test_deploy_notify_flag() {
1858 let cli = Cli::try_parse_from(["forge-guard", "deploy", "--notify"]).unwrap();
1859 if let Commands::Deploy(args) = cli.command {
1860 assert!(args.notify);
1861 } else {
1862 panic!("Expected Deploy command");
1863 }
1864 }
1865
1866 #[test]
1867 fn test_deploy_safe_notify_flag() {
1868 let cli = Cli::try_parse_from(["forge-guard", "deploy-safe", "--notify"]).unwrap();
1869 if let Commands::DeploySafe(args) = cli.command {
1870 assert!(args.notify);
1871 } else {
1872 panic!("Expected DeploySafe command");
1873 }
1874 }
1875}