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 = "src")]
237 pub sources: String,
238
239 #[arg(long)]
241 pub exclude: Option<String>,
242
243 #[arg(long)]
245 pub template: Option<String>,
246
247 #[arg(long)]
249 pub list_templates: bool,
250
251 #[arg(long)]
253 pub suppressions: Option<PathBuf>,
254
255 #[arg(long)]
257 pub show_suppressed: bool,
258
259 #[arg(long)]
261 pub generate_suppressions: bool,
262
263 #[arg(long)]
265 pub notify: bool,
266}
267
268#[derive(Debug, clap::Args)]
269pub struct DeployArgs {
270 #[command(flatten)]
271 pub shared: SharedFlags,
272
273 pub contract: Option<String>,
275
276 #[arg(long)]
278 pub force: bool,
279
280 #[arg(long)]
282 pub args: Option<String>,
283
284 #[arg(long)]
286 pub salt: Option<String>,
287
288 #[arg(long)]
290 pub verify: bool,
291
292 #[arg(long)]
294 pub notify: bool,
295}
296
297#[derive(Debug, clap::Args)]
298pub struct DeploySafeArgs {
299 #[command(flatten)]
300 pub shared: SharedFlags,
301
302 pub contract: Option<String>,
304
305 #[arg(long)]
307 pub args: Option<String>,
308
309 #[arg(long)]
311 pub verify: bool,
312
313 #[arg(long)]
315 pub notify: bool,
316}
317
318#[derive(Debug, clap::Args)]
319pub struct FuzzArgs {
320 #[command(flatten)]
321 pub shared: SharedFlags,
322
323 #[arg(long, default_value = "10000")]
325 pub runs: u32,
326
327 #[arg(long)]
329 pub seed: Option<u64>,
330
331 #[arg(long)]
333 pub test: Option<String>,
334
335 pub contract: Option<String>,
337}
338
339#[derive(Debug, clap::Args)]
340pub struct InvariantArgs {
341 #[command(flatten)]
342 pub shared: SharedFlags,
343
344 #[arg(long, default_value = "1000")]
346 pub runs: u32,
347
348 #[arg(long, default_value = "100")]
350 pub depth: u32,
351
352 pub contract: Option<String>,
354
355 #[arg(long)]
357 pub fail_on_revert: bool,
358}
359
360#[derive(Debug, clap::Args)]
361pub struct SimulateArgs {
362 #[command(flatten)]
363 pub shared: SharedFlags,
364
365 #[arg(long, default_value = "100")]
367 pub blocks: u32,
368
369 #[arg(long)]
371 pub deployer: Option<String>,
372
373 #[arg(long)]
375 pub mev: bool,
376
377 pub contract: Option<String>,
379}
380
381#[derive(Debug, clap::Args)]
382pub struct GasArgs {
383 #[command(flatten)]
384 pub shared: SharedFlags,
385
386 pub contract: Option<String>,
388
389 #[arg(long)]
391 pub diff: Option<String>,
392
393 #[arg(long)]
395 pub all: bool,
396
397 #[arg(long, default_value = "50000")]
399 pub warn_threshold: u64,
400}
401
402#[derive(Debug, clap::Args)]
403pub struct ReportArgs {
404 #[command(flatten)]
405 pub shared: SharedFlags,
406
407 pub input: Option<PathBuf>,
409
410 #[arg(long, default_value = "markdown")]
412 pub format: String,
413
414 #[arg(long)]
416 pub output: Option<PathBuf>,
417
418 #[arg(long)]
420 pub exploit_paths: bool,
421
422 #[arg(long)]
424 pub summary: bool,
425}
426
427#[derive(Debug, clap::Args)]
428pub struct VerifyArgs {
429 #[command(flatten)]
430 pub shared: SharedFlags,
431
432 pub address: Option<String>,
434
435 pub name: Option<String>,
437
438 #[arg(long)]
440 pub api_key: Option<String>,
441
442 #[arg(long)]
444 pub constructor_args: Option<String>,
445
446 #[arg(long)]
448 pub all: bool,
449}
450
451#[derive(Debug, clap::Args)]
452pub struct DoctorArgs {
453 #[command(flatten)]
454 pub shared: SharedFlags,
455
456 #[arg(long)]
458 pub fix: bool,
459
460 #[arg(long, short)]
462 pub verbose: bool,
463
464 #[arg(long)]
466 pub check: Option<String>,
467
468 #[arg(long)]
470 pub sync: bool,
471
472 #[arg(long)]
474 pub dry_run: bool,
475
476 #[arg(long)]
478 pub diff: bool,
479}
480
481#[derive(Debug, clap::Args)]
482pub struct WatchArgs {
483 #[command(flatten)]
484 pub shared: SharedFlags,
485
486 #[arg(long, default_value = "src")]
488 pub dirs: String,
489
490 #[arg(long, default_value = "500")]
492 pub debounce_ms: u64,
493
494 #[arg(long)]
496 pub exclude: Option<String>,
497
498 #[arg(long)]
500 pub full: bool,
501}
502
503#[derive(Debug, clap::Args)]
504pub struct CiArgs {
505 #[command(flatten)]
506 pub shared: SharedFlags,
507
508 #[arg(long, default_value = "github")]
510 pub platform: String,
511
512 #[arg(long, default_value = ".github/workflows")]
514 pub output: PathBuf,
515
516 #[arg(long)]
518 pub include_deploy: bool,
519
520 #[arg(long)]
522 pub overwrite: bool,
523}
524
525#[derive(Debug, clap::Args)]
526pub struct BenchmarkArgs {
527 #[command(flatten)]
528 pub shared: SharedFlags,
529
530 #[arg(long, default_value = "10")]
532 pub iterations: u32,
533
534 #[arg(long)]
536 pub compare: Option<PathBuf>,
537
538 #[arg(long)]
540 pub save: Option<PathBuf>,
541
542 #[arg(long)]
544 pub module: Option<String>,
545
546 #[arg(long, default_value = "3")]
548 pub warmup: u32,
549}
550
551#[derive(Debug, clap::Args)]
552pub struct SbomArgs {
553 #[command(flatten)]
554 pub shared: SharedFlags,
555
556 #[arg(long, default_value = "cyclonedx")]
558 pub format: String,
559
560 #[arg(long, short)]
562 pub output: Option<PathBuf>,
563
564 #[arg(long)]
566 pub ci: bool,
567}
568
569#[derive(Debug, clap::Args)]
570pub struct ScanArgs {
571 #[command(flatten)]
572 pub shared: SharedFlags,
573
574 #[arg(long, default_value = "1")]
576 pub depth: u32,
577
578 #[arg(long)]
580 pub update: bool,
581
582 #[arg(long)]
584 pub vulnerable_only: bool,
585
586 #[arg(long)]
588 pub fail_fast: bool,
589}
590
591#[derive(Debug, clap::Args)]
592pub struct UpgradeCheckArgs {
593 #[command(flatten)]
594 pub shared: SharedFlags,
595
596 pub proxy: Option<String>,
598
599 pub implementation: Option<String>,
601
602 #[arg(long)]
604 pub all: bool,
605
606 #[arg(long)]
608 pub storage_collision: bool,
609
610 #[arg(long)]
612 pub uups: bool,
613}
614
615#[derive(Debug, clap::Args)]
616pub struct PluginArgs {
617 #[command(flatten)]
618 pub shared: SharedFlags,
619
620 #[command(subcommand)]
622 pub action: Option<PluginAction>,
623}
624
625#[derive(Debug, Subcommand)]
626pub enum PluginAction {
627 List,
629 Install {
631 name: String,
632 source: Option<String>,
633 },
634 Remove { name: String },
636 Enable { name: String },
638 Disable { name: String },
640 New { name: String },
642}
643
644#[derive(Debug, clap::Args)]
645pub struct ChainArgs {
646 #[command(flatten)]
647 pub shared: SharedFlags,
648
649 #[command(subcommand)]
651 pub action: Option<ChainAction>,
652}
653
654#[derive(Debug, Subcommand)]
655pub enum ChainAction {
656 List,
658 Info { chain: String },
660 Add {
662 name: String,
663 rpc_url: Option<String>,
664 chain_id: Option<u64>,
665 },
666 Remove { name: String },
668 Test {
670 chain: String,
671 rpc_url: Option<String>,
672 },
673}
674
675#[derive(Debug, clap::Args)]
676pub struct SecurityArgs {
677 #[command(flatten)]
678 pub shared: SharedFlags,
679
680 #[command(subcommand)]
682 pub action: Option<SecurityAction>,
683}
684
685#[derive(Debug, Subcommand)]
686pub enum SecurityAction {
687 Config,
689 Threshold { score: u8 },
691 Enable { check: String },
693 Disable { check: String },
695 List,
697 Info { check: String },
699}
700
701#[derive(Debug, clap::Args)]
704pub struct ImportArgs {
705 #[command(flatten)]
706 pub shared: SharedFlags,
707
708 #[arg(long, default_value = "slither")]
710 pub from: String,
711
712 pub input: Option<PathBuf>,
714
715 #[arg(long)]
717 pub findings: Option<PathBuf>,
718
719 #[arg(long, short)]
721 pub output: Option<PathBuf>,
722}
723
724#[derive(Debug, clap::Args)]
725pub struct NotifyArgs {
726 #[command(flatten)]
727 pub shared: SharedFlags,
728
729 #[arg(long)]
731 pub webhook: Option<String>,
732
733 #[arg(long)]
735 pub kind: Option<String>,
736
737 #[arg(long)]
739 pub findings: Option<PathBuf>,
740
741 #[arg(long, default_value = "Forge Guard Audit")]
743 pub title: String,
744
745 #[arg(long)]
747 pub message: Option<String>,
748
749 #[arg(long)]
751 pub severity: Option<String>,
752
753 #[arg(long)]
755 pub on_critical: bool,
756
757 #[arg(long)]
759 pub on_high: bool,
760
761 #[arg(long)]
763 pub dry_run: bool,
764}
765
766#[cfg(test)]
767mod tests {
768 use super::*;
769 use clap::Parser;
770
771 #[test]
772 fn test_cli_parse_audit() {
773 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
774 assert!(matches!(cli.command, Commands::Audit(_)));
775 }
776
777 #[test]
778 fn test_cli_parse_deploy() {
779 let cli = Cli::try_parse_from(["forge-guard", "deploy"]).unwrap();
780 assert!(matches!(cli.command, Commands::Deploy(_)));
781 }
782
783 #[test]
784 fn test_cli_parse_deploy_safe() {
785 let cli = Cli::try_parse_from(["forge-guard", "deploy-safe"]).unwrap();
786 assert!(matches!(cli.command, Commands::DeploySafe(_)));
787 }
788
789 #[test]
790 fn test_cli_parse_fuzz() {
791 let cli = Cli::try_parse_from(["forge-guard", "fuzz"]).unwrap();
792 assert!(matches!(cli.command, Commands::Fuzz(_)));
793 }
794
795 #[test]
796 fn test_cli_parse_invariant() {
797 let cli = Cli::try_parse_from(["forge-guard", "invariant"]).unwrap();
798 assert!(matches!(cli.command, Commands::Invariant(_)));
799 }
800
801 #[test]
802 fn test_cli_parse_simulate() {
803 let cli = Cli::try_parse_from(["forge-guard", "simulate"]).unwrap();
804 assert!(matches!(cli.command, Commands::Simulate(_)));
805 }
806
807 #[test]
808 fn test_cli_parse_gas() {
809 let cli = Cli::try_parse_from(["forge-guard", "gas"]).unwrap();
810 assert!(matches!(cli.command, Commands::Gas(_)));
811 }
812
813 #[test]
814 fn test_cli_parse_report() {
815 let cli = Cli::try_parse_from(["forge-guard", "report"]).unwrap();
816 assert!(matches!(cli.command, Commands::Report(_)));
817 }
818
819 #[test]
820 fn test_cli_parse_verify() {
821 let cli = Cli::try_parse_from(["forge-guard", "verify"]).unwrap();
822 assert!(matches!(cli.command, Commands::Verify(_)));
823 }
824
825 #[test]
826 fn test_cli_parse_doctor() {
827 let cli = Cli::try_parse_from(["forge-guard", "doctor"]).unwrap();
828 assert!(matches!(cli.command, Commands::Doctor(_)));
829 }
830
831 #[test]
832 fn test_cli_parse_watch() {
833 let cli = Cli::try_parse_from(["forge-guard", "watch"]).unwrap();
834 assert!(matches!(cli.command, Commands::Watch(_)));
835 }
836
837 #[test]
838 fn test_cli_parse_ci() {
839 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
840 assert!(matches!(cli.command, Commands::Ci(_)));
841 }
842
843 #[test]
844 fn test_cli_parse_benchmark() {
845 let cli = Cli::try_parse_from(["forge-guard", "benchmark"]).unwrap();
846 assert!(matches!(cli.command, Commands::Benchmark(_)));
847 }
848
849 #[test]
850 fn test_cli_parse_scan() {
851 let cli = Cli::try_parse_from(["forge-guard", "scan"]).unwrap();
852 assert!(matches!(cli.command, Commands::Scan(_)));
853 }
854
855 #[test]
856 fn test_cli_parse_upgrade_check() {
857 let cli = Cli::try_parse_from(["forge-guard", "upgrade-check"]).unwrap();
858 assert!(matches!(cli.command, Commands::UpgradeCheck(_)));
859 }
860
861 #[test]
862 fn test_cli_parse_plugins() {
863 let cli = Cli::try_parse_from(["forge-guard", "plugins"]).unwrap();
864 assert!(matches!(cli.command, Commands::Plugins(_)));
865 }
866
867 #[test]
868 fn test_cli_parse_chain() {
869 let cli = Cli::try_parse_from(["forge-guard", "chain"]).unwrap();
870 assert!(matches!(cli.command, Commands::Chain(_)));
871 }
872
873 #[test]
874 fn test_cli_parse_security() {
875 let cli = Cli::try_parse_from(["forge-guard", "security"]).unwrap();
876 assert!(matches!(cli.command, Commands::Security(_)));
877 }
878
879 #[test]
880 fn test_cli_parse_install_hook() {
881 let cli = Cli::try_parse_from(["forge-guard", "install-hook"]).unwrap();
882 assert!(matches!(cli.command, Commands::InstallHook(_)));
883 }
884
885 #[test]
886 fn test_cli_parse_install_hook_uninstall() {
887 let cli = Cli::try_parse_from(["forge-guard", "install-hook", "--uninstall"]).unwrap();
888 assert!(matches!(cli.command, Commands::InstallHook(_)));
889 }
890
891 #[test]
892 fn test_cli_parse_doctor_sync() {
893 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--sync"]).unwrap();
894 if let Commands::Doctor(args) = cli.command {
895 assert!(args.sync);
896 } else {
897 panic!("Expected Doctor command");
898 }
899 }
900
901 #[test]
902 fn test_cli_parse_doctor_dry_run() {
903 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--sync", "--dry-run"]).unwrap();
904 if let Commands::Doctor(args) = cli.command {
905 assert!(args.sync);
906 assert!(args.dry_run);
907 } else {
908 panic!("Expected Doctor command");
909 }
910 }
911
912 #[test]
913 fn test_cli_parse_audit_template() {
914 let cli = Cli::try_parse_from(["forge-guard", "audit", "--template", "defi"]).unwrap();
915 if let Commands::Audit(args) = cli.command {
916 assert_eq!(args.template.as_deref(), Some("defi"));
917 } else {
918 panic!("Expected Audit command");
919 }
920 }
921
922 #[test]
923 fn test_cli_parse_audit_list_templates() {
924 let cli = Cli::try_parse_from(["forge-guard", "audit", "--list-templates"]).unwrap();
925 if let Commands::Audit(args) = cli.command {
926 assert!(args.list_templates);
927 } else {
928 panic!("Expected Audit command");
929 }
930 }
931
932 #[test]
933 fn test_shared_flags_defaults() {
934 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
935 if let Commands::Audit(args) = cli.command {
936 assert_eq!(args.shared.chain, "ethereum");
937 assert!(!args.shared.json);
938 assert!(!args.shared.markdown);
939 assert!(!args.shared.strict);
940 assert!(!args.shared.offline);
941 assert!(!args.shared.production);
942 assert!(!args.shared.report);
943 assert_eq!(args.shared.parallelism, 4);
944 } else {
945 panic!("Expected Audit command");
946 }
947 }
948
949 #[test]
950 fn test_shared_flags_custom() {
951 let cli = Cli::try_parse_from([
952 "forge-guard",
953 "audit",
954 "--chain",
955 "base",
956 "--json",
957 "--strict",
958 "--offline",
959 "--production",
960 "--parallelism",
961 "8",
962 "--project",
963 "/tmp/project",
964 ])
965 .unwrap();
966 if let Commands::Audit(args) = cli.command {
967 assert_eq!(args.shared.chain, "base");
968 assert!(args.shared.json);
969 assert!(args.shared.strict);
970 assert!(args.shared.offline);
971 assert!(args.shared.production);
972 assert_eq!(args.shared.parallelism, 8);
973 assert_eq!(
974 args.shared.project,
975 std::path::PathBuf::from("/tmp/project")
976 );
977 } else {
978 panic!("Expected Audit command");
979 }
980 }
981
982 #[test]
983 fn test_audit_args_defaults() {
984 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
985 if let Commands::Audit(args) = cli.command {
986 assert!(!args.full);
987 assert!(!args.quick);
988 assert!(!args.summary);
989 assert!(!args.ai);
990 assert_eq!(args.ai_provider, "openai");
991 assert_eq!(args.ai_model, "gpt-5");
992 assert!(args.ai_api_key.is_none());
993 assert!(!args.exploit);
994 assert!(!args.gas);
995 assert!(!args.all_chains);
996 assert_eq!(args.sources, "src");
997 assert!(args.exclude.is_none());
998 } else {
999 panic!("Expected Audit command");
1000 }
1001 }
1002
1003 #[test]
1004 fn test_audit_args_full() {
1005 let cli = Cli::try_parse_from([
1006 "forge-guard",
1007 "audit",
1008 "--full",
1009 "--chain",
1010 "arbitrum",
1011 "--json",
1012 "--report",
1013 "--exploit",
1014 "--gas",
1015 ])
1016 .unwrap();
1017 if let Commands::Audit(args) = cli.command {
1018 assert!(args.full);
1019 assert!(args.exploit);
1020 assert!(args.gas);
1021 assert!(args.shared.json);
1022 assert!(args.shared.report);
1023 assert_eq!(args.shared.chain, "arbitrum");
1024 } else {
1025 panic!("Expected Audit command");
1026 }
1027 }
1028
1029 #[test]
1030 fn test_audit_ai_args() {
1031 let cli = Cli::try_parse_from([
1032 "forge-guard",
1033 "audit",
1034 "--ai",
1035 "--ai-provider",
1036 "claude",
1037 "--ai-model",
1038 "claude-5-sonnet-20260701",
1039 "--ai-full",
1040 ])
1041 .unwrap();
1042 if let Commands::Audit(args) = cli.command {
1043 assert!(args.ai);
1044 assert_eq!(args.ai_provider, "claude");
1045 assert_eq!(args.ai_model, "claude-5-sonnet-20260701");
1046 assert!(args.ai_full);
1047 } else {
1048 panic!("Expected Audit command");
1049 }
1050 }
1051
1052 #[test]
1053 fn test_deploy_args() {
1054 let cli = Cli::try_parse_from([
1055 "forge-guard",
1056 "deploy",
1057 "MyContract",
1058 "--force",
1059 "--salt",
1060 "0xabc",
1061 "--verify",
1062 "--args",
1063 "arg1,arg2",
1064 ])
1065 .unwrap();
1066 if let Commands::Deploy(args) = cli.command {
1067 assert_eq!(args.contract.as_deref(), Some("MyContract"));
1068 assert!(args.force);
1069 assert_eq!(args.salt.as_deref(), Some("0xabc"));
1070 assert!(args.verify);
1071 assert_eq!(args.args.as_deref(), Some("arg1,arg2"));
1072 } else {
1073 panic!("Expected Deploy command");
1074 }
1075 }
1076
1077 #[test]
1078 fn test_fuzz_args() {
1079 let cli = Cli::try_parse_from([
1080 "forge-guard",
1081 "fuzz",
1082 "--runs",
1083 "50000",
1084 "--seed",
1085 "42",
1086 "--test",
1087 "testFuzzDeposit",
1088 "Vault",
1089 ])
1090 .unwrap();
1091 if let Commands::Fuzz(args) = cli.command {
1092 assert_eq!(args.runs, 50_000);
1093 assert_eq!(args.seed, Some(42));
1094 assert_eq!(args.test.as_deref(), Some("testFuzzDeposit"));
1095 assert_eq!(args.contract.as_deref(), Some("Vault"));
1096 } else {
1097 panic!("Expected Fuzz command");
1098 }
1099 }
1100
1101 #[test]
1102 fn test_simulate_args() {
1103 let cli = Cli::try_parse_from([
1104 "forge-guard",
1105 "simulate",
1106 "MyContract",
1107 "--blocks",
1108 "200",
1109 "--mev",
1110 ])
1111 .unwrap();
1112 if let Commands::Simulate(args) = cli.command {
1113 assert_eq!(args.contract.as_deref(), Some("MyContract"));
1114 assert_eq!(args.blocks, 200);
1115 assert!(args.mev);
1116 } else {
1117 panic!("Expected Simulate command");
1118 }
1119 }
1120
1121 #[test]
1122 fn test_invariant_args() {
1123 let cli = Cli::try_parse_from([
1124 "forge-guard",
1125 "invariant",
1126 "--runs",
1127 "2000",
1128 "--depth",
1129 "150",
1130 "--fail-on-revert",
1131 ])
1132 .unwrap();
1133 if let Commands::Invariant(args) = cli.command {
1134 assert_eq!(args.runs, 2000);
1135 assert_eq!(args.depth, 150);
1136 assert!(args.fail_on_revert);
1137 } else {
1138 panic!("Expected Invariant command");
1139 }
1140 }
1141
1142 #[test]
1143 fn test_ci_args_defaults() {
1144 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
1145 if let Commands::Ci(args) = cli.command {
1146 assert_eq!(args.platform, "github");
1147 assert_eq!(args.output, std::path::PathBuf::from(".github/workflows"));
1148 assert!(!args.include_deploy);
1149 assert!(!args.overwrite);
1150 } else {
1151 panic!("Expected Ci command");
1152 }
1153 }
1154
1155 #[test]
1156 fn test_ci_args_custom() {
1157 let cli = Cli::try_parse_from([
1158 "forge-guard",
1159 "ci",
1160 "--platform",
1161 "gitlab",
1162 "--include-deploy",
1163 "--overwrite",
1164 "--output",
1165 ".gitlab",
1166 ])
1167 .unwrap();
1168 if let Commands::Ci(args) = cli.command {
1169 assert_eq!(args.platform, "gitlab");
1170 assert!(args.include_deploy);
1171 assert!(args.overwrite);
1172 assert_eq!(args.output, std::path::PathBuf::from(".gitlab"));
1173 } else {
1174 panic!("Expected Ci command");
1175 }
1176 }
1177
1178 #[test]
1179 fn test_benchmark_args() {
1180 let cli = Cli::try_parse_from([
1181 "forge-guard",
1182 "benchmark",
1183 "--iterations",
1184 "50",
1185 "--warmup",
1186 "5",
1187 "--module",
1188 "pattern_matching",
1189 ])
1190 .unwrap();
1191 if let Commands::Benchmark(args) = cli.command {
1192 assert_eq!(args.iterations, 50);
1193 assert_eq!(args.warmup, 5);
1194 assert_eq!(args.module.as_deref(), Some("pattern_matching"));
1195 } else {
1196 panic!("Expected Benchmark command");
1197 }
1198 }
1199
1200 #[test]
1201 fn test_gas_args() {
1202 let cli =
1203 Cli::try_parse_from(["forge-guard", "gas", "--all", "--warn-threshold", "100000"])
1204 .unwrap();
1205 if let Commands::Gas(args) = cli.command {
1206 assert!(args.all);
1207 assert_eq!(args.warn_threshold, 100000);
1208 assert!(args.contract.is_none());
1209 } else {
1210 panic!("Expected Gas command");
1211 }
1212 }
1213
1214 #[test]
1215 fn test_scan_args() {
1216 let cli = Cli::try_parse_from([
1217 "forge-guard",
1218 "scan",
1219 "--depth",
1220 "2",
1221 "--update",
1222 "--vulnerable-only",
1223 "--fail-fast",
1224 ])
1225 .unwrap();
1226 if let Commands::Scan(args) = cli.command {
1227 assert_eq!(args.depth, 2);
1228 assert!(args.update);
1229 assert!(args.vulnerable_only);
1230 assert!(args.fail_fast);
1231 } else {
1232 panic!("Expected Scan command");
1233 }
1234 }
1235
1236 #[test]
1237 fn test_upgrade_check_args() {
1238 let cli = Cli::try_parse_from([
1239 "forge-guard",
1240 "upgrade-check",
1241 "--storage-collision",
1242 "--uups",
1243 "--all",
1244 ])
1245 .unwrap();
1246 if let Commands::UpgradeCheck(args) = cli.command {
1247 assert!(args.storage_collision);
1248 assert!(args.uups);
1249 assert!(args.all);
1250 } else {
1251 panic!("Expected UpgradeCheck command");
1252 }
1253 }
1254
1255 #[test]
1256 fn test_doctor_args() {
1257 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--fix", "--verbose"]).unwrap();
1258 if let Commands::Doctor(args) = cli.command {
1259 assert!(args.fix);
1260 assert!(args.verbose);
1261 assert!(args.check.is_none());
1262 } else {
1263 panic!("Expected Doctor command");
1264 }
1265 }
1266
1267 #[test]
1268 fn test_verify_args() {
1269 let cli = Cli::try_parse_from([
1270 "forge-guard",
1271 "verify",
1272 "0x1234",
1273 "MyContract",
1274 "--api-key",
1275 "test-key",
1276 "--all",
1277 ])
1278 .unwrap();
1279 if let Commands::Verify(args) = cli.command {
1280 assert_eq!(args.address.as_deref(), Some("0x1234"));
1281 assert_eq!(args.name.as_deref(), Some("MyContract"));
1282 assert_eq!(args.api_key.as_deref(), Some("test-key"));
1283 assert!(args.all);
1284 } else {
1285 panic!("Expected Verify command");
1286 }
1287 }
1288
1289 #[test]
1290 fn test_watch_args() {
1291 let cli = Cli::try_parse_from([
1292 "forge-guard",
1293 "watch",
1294 "--dirs",
1295 "src,test-contracts",
1296 "--debounce-ms",
1297 "1000",
1298 "--full",
1299 ])
1300 .unwrap();
1301 if let Commands::Watch(args) = cli.command {
1302 assert_eq!(args.dirs, "src,test-contracts");
1303 assert_eq!(args.debounce_ms, 1000);
1304 assert!(args.full);
1305 } else {
1306 panic!("Expected Watch command");
1307 }
1308 }
1309
1310 #[test]
1311 fn test_plugins_list() {
1312 let cli = Cli::try_parse_from(["forge-guard", "plugins", "list"]).unwrap();
1313 if let Commands::Plugins(args) = cli.command {
1314 assert!(matches!(args.action, Some(PluginAction::List)));
1315 } else {
1316 panic!("Expected Plugins command");
1317 }
1318 }
1319
1320 #[test]
1321 fn test_plugins_install() {
1322 let cli = Cli::try_parse_from(["forge-guard", "plugins", "install", "my-plugin"]).unwrap();
1323 if let Commands::Plugins(args) = cli.command {
1324 assert!(matches!(args.action, Some(PluginAction::Install { .. })));
1325 } else {
1326 panic!("Expected Plugins command");
1327 }
1328 }
1329
1330 #[test]
1331 fn test_plugins_remove() {
1332 let cli = Cli::try_parse_from(["forge-guard", "plugins", "remove", "bad-plugin"]).unwrap();
1333 if let Commands::Plugins(args) = cli.command {
1334 assert!(matches!(args.action, Some(PluginAction::Remove { .. })));
1335 } else {
1336 panic!("Expected Plugins command");
1337 }
1338 }
1339
1340 #[test]
1341 fn test_plugins_enable() {
1342 let cli = Cli::try_parse_from(["forge-guard", "plugins", "enable", "my-plugin"]).unwrap();
1343 if let Commands::Plugins(args) = cli.command {
1344 assert!(matches!(args.action, Some(PluginAction::Enable { .. })));
1345 } else {
1346 panic!("Expected Plugins command");
1347 }
1348 }
1349
1350 #[test]
1351 fn test_plugins_disable() {
1352 let cli = Cli::try_parse_from(["forge-guard", "plugins", "disable", "my-plugin"]).unwrap();
1353 if let Commands::Plugins(args) = cli.command {
1354 assert!(matches!(args.action, Some(PluginAction::Disable { .. })));
1355 } else {
1356 panic!("Expected Plugins command");
1357 }
1358 }
1359
1360 #[test]
1361 fn test_plugins_new() {
1362 let cli =
1363 Cli::try_parse_from(["forge-guard", "plugins", "new", "my-awesome-plugin"]).unwrap();
1364 if let Commands::Plugins(args) = cli.command {
1365 assert!(matches!(args.action, Some(PluginAction::New { .. })));
1366 } else {
1367 panic!("Expected Plugins command");
1368 }
1369 }
1370
1371 #[test]
1372 fn test_chain_list() {
1373 let cli = Cli::try_parse_from(["forge-guard", "chain", "list"]).unwrap();
1374 if let Commands::Chain(args) = cli.command {
1375 assert!(matches!(args.action, Some(ChainAction::List)));
1376 } else {
1377 panic!("Expected Chain command");
1378 }
1379 }
1380
1381 #[test]
1382 fn test_chain_info() {
1383 let cli = Cli::try_parse_from(["forge-guard", "chain", "info", "base"]).unwrap();
1384 if let Commands::Chain(args) = cli.command {
1385 assert!(matches!(args.action, Some(ChainAction::Info { .. })));
1386 } else {
1387 panic!("Expected Chain command");
1388 }
1389 }
1390
1391 #[test]
1392 fn test_chain_add() {
1393 let cli = Cli::try_parse_from([
1394 "forge-guard",
1395 "chain",
1396 "add",
1397 "my-chain",
1398 "https://rpc.my-chain.io",
1399 "99999",
1400 ])
1401 .unwrap();
1402 if let Commands::Chain(args) = cli.command {
1403 assert!(matches!(args.action, Some(ChainAction::Add { .. })));
1404 } else {
1405 panic!("Expected Chain command");
1406 }
1407 }
1408
1409 #[test]
1410 fn test_security_list() {
1411 let cli = Cli::try_parse_from(["forge-guard", "security", "list"]).unwrap();
1412 if let Commands::Security(args) = cli.command {
1413 assert!(matches!(args.action, Some(SecurityAction::List)));
1414 } else {
1415 panic!("Expected Security command");
1416 }
1417 }
1418
1419 #[test]
1420 fn test_security_threshold() {
1421 let cli = Cli::try_parse_from(["forge-guard", "security", "threshold", "85"]).unwrap();
1422 if let Commands::Security(args) = cli.command {
1423 assert!(matches!(
1424 args.action,
1425 Some(SecurityAction::Threshold { .. })
1426 ));
1427 } else {
1428 panic!("Expected Security command");
1429 }
1430 }
1431
1432 #[test]
1433 fn test_markdown_flag() {
1434 let cli = Cli::try_parse_from(["forge-guard", "audit", "--markdown", "--report"]).unwrap();
1435 if let Commands::Audit(args) = cli.command {
1436 assert!(args.shared.markdown);
1437 assert!(args.shared.report);
1438 } else {
1439 panic!("Expected Audit command");
1440 }
1441 }
1442
1443 #[test]
1444 fn test_quick_audit() {
1445 let cli = Cli::try_parse_from(["forge-guard", "audit", "--quick", "--summary"]).unwrap();
1446 if let Commands::Audit(args) = cli.command {
1447 assert!(args.quick);
1448 assert!(args.summary);
1449 } else {
1450 panic!("Expected Audit command");
1451 }
1452 }
1453
1454 #[test]
1455 fn test_all_chains_flag() {
1456 let cli = Cli::try_parse_from(["forge-guard", "audit", "--all-chains"]).unwrap();
1457 if let Commands::Audit(args) = cli.command {
1458 assert!(args.all_chains);
1459 } else {
1460 panic!("Expected Audit command");
1461 }
1462 }
1463
1464 #[test]
1465 fn test_deploy_safe_args() {
1466 let cli =
1467 Cli::try_parse_from(["forge-guard", "deploy-safe", "SecureVault", "--verify"]).unwrap();
1468 if let Commands::DeploySafe(args) = cli.command {
1469 assert_eq!(args.contract.as_deref(), Some("SecureVault"));
1470 assert!(args.verify);
1471 } else {
1472 panic!("Expected DeploySafe command");
1473 }
1474 }
1475
1476 #[test]
1477 fn test_report_args_json() {
1478 let cli = Cli::try_parse_from(["forge-guard", "report", "--format", "json"]).unwrap();
1479 if let Commands::Report(args) = cli.command {
1480 assert_eq!(args.format, "json");
1481 assert!(!args.summary);
1482 } else {
1483 panic!("Expected Report command");
1484 }
1485 }
1486
1487 #[test]
1488 fn test_verify_all_without_address() {
1489 let cli = Cli::try_parse_from(["forge-guard", "verify", "--all"]).unwrap();
1490 if let Commands::Verify(args) = cli.command {
1491 assert!(args.all);
1492 assert!(args.address.is_none());
1493 } else {
1494 panic!("Expected Verify command");
1495 }
1496 }
1497
1498 #[test]
1499 fn test_report_summary() {
1500 let cli = Cli::try_parse_from(["forge-guard", "report", "--summary"]).unwrap();
1501 if let Commands::Report(args) = cli.command {
1502 assert!(args.summary);
1503 } else {
1504 panic!("Expected Report command");
1505 }
1506 }
1507
1508 #[test]
1509 fn test_scan_vulnerable_only() {
1510 let cli = Cli::try_parse_from(["forge-guard", "scan", "--vulnerable-only"]).unwrap();
1511 if let Commands::Scan(args) = cli.command {
1512 assert!(args.vulnerable_only);
1513 } else {
1514 panic!("Expected Scan command");
1515 }
1516 }
1517
1518 #[test]
1519 fn test_doctor_check_category() {
1520 let cli =
1521 Cli::try_parse_from(["forge-guard", "doctor", "--check", "dependencies"]).unwrap();
1522 if let Commands::Doctor(args) = cli.command {
1523 assert_eq!(args.check.as_deref(), Some("dependencies"));
1524 } else {
1525 panic!("Expected Doctor command");
1526 }
1527 }
1528
1529 #[test]
1530 fn test_gas_contract_specific() {
1531 let cli =
1532 Cli::try_parse_from(["forge-guard", "gas", "Vault", "--diff", "prev.json"]).unwrap();
1533 if let Commands::Gas(args) = cli.command {
1534 assert_eq!(args.contract.as_deref(), Some("Vault"));
1535 assert_eq!(args.diff.as_deref(), Some("prev.json"));
1536 } else {
1537 panic!("Expected Gas command");
1538 }
1539 }
1540
1541 #[test]
1542 fn test_verify_with_constructor_args() {
1543 let cli = Cli::try_parse_from([
1544 "forge-guard",
1545 "verify",
1546 "0xabc",
1547 "Token",
1548 "--constructor-args",
1549 "0x0001",
1550 ])
1551 .unwrap();
1552 if let Commands::Verify(args) = cli.command {
1553 assert_eq!(args.constructor_args.as_deref(), Some("0x0001"));
1554 } else {
1555 panic!("Expected Verify command");
1556 }
1557 }
1558
1559 #[test]
1560 fn test_upgrade_check_proxy() {
1561 let cli =
1562 Cli::try_parse_from(["forge-guard", "upgrade-check", "0xproxy", "0ximpl"]).unwrap();
1563 if let Commands::UpgradeCheck(args) = cli.command {
1564 assert_eq!(args.proxy.as_deref(), Some("0xproxy"));
1565 assert_eq!(args.implementation.as_deref(), Some("0ximpl"));
1566 } else {
1567 panic!("Expected UpgradeCheck command");
1568 }
1569 }
1570
1571 #[test]
1572 fn test_benchmark_save_and_compare() {
1573 let cli = Cli::try_parse_from([
1574 "forge-guard",
1575 "benchmark",
1576 "--save",
1577 "results.json",
1578 "--compare",
1579 "baseline.json",
1580 ])
1581 .unwrap();
1582 if let Commands::Benchmark(args) = cli.command {
1583 assert_eq!(
1584 args.save.as_deref(),
1585 Some(std::path::Path::new("results.json"))
1586 );
1587 assert_eq!(
1588 args.compare.as_deref(),
1589 Some(std::path::Path::new("baseline.json"))
1590 );
1591 } else {
1592 panic!("Expected Benchmark command");
1593 }
1594 }
1595
1596 #[test]
1597 fn test_watch_exclude() {
1598 let cli = Cli::try_parse_from(["forge-guard", "watch", "--exclude", "*.test.sol"]).unwrap();
1599 if let Commands::Watch(args) = cli.command {
1600 assert_eq!(args.exclude.as_deref(), Some("*.test.sol"));
1601 } else {
1602 panic!("Expected Watch command");
1603 }
1604 }
1605
1606 #[test]
1607 fn test_simulate_deployer() {
1608 let cli =
1609 Cli::try_parse_from(["forge-guard", "simulate", "--deployer", "0xdeployer"]).unwrap();
1610 if let Commands::Simulate(args) = cli.command {
1611 assert_eq!(args.deployer.as_deref(), Some("0xdeployer"));
1612 } else {
1613 panic!("Expected Simulate command");
1614 }
1615 }
1616
1617 #[test]
1618 fn test_security_disable_check() {
1619 let cli = Cli::try_parse_from(["forge-guard", "security", "disable", "FA-H-001"]).unwrap();
1620 if let Commands::Security(args) = cli.command {
1621 assert!(matches!(args.action, Some(SecurityAction::Disable { .. })));
1622 } else {
1623 panic!("Expected Security command");
1624 }
1625 }
1626
1627 #[test]
1628 fn test_from_env_try_parse() {
1629 let cli = Cli::try_parse_from(["forge-guard", "audit", "--report", "--json"]).unwrap();
1632 assert!(matches!(cli.command, Commands::Audit(_)));
1633 if let Commands::Audit(args) = cli.command {
1634 assert!(args.shared.report);
1635 assert!(args.shared.json);
1636 }
1637 }
1638
1639 #[test]
1640 fn test_cli_parse_import() {
1641 let cli = Cli::try_parse_from(["forge-guard", "import", "results.json"]).unwrap();
1642 assert!(matches!(cli.command, Commands::Import(_)));
1643 }
1644
1645 #[test]
1646 fn test_import_args_from_slither() {
1647 let cli = Cli::try_parse_from([
1648 "forge-guard",
1649 "import",
1650 "--from",
1651 "mythril",
1652 "mythril_out.json",
1653 "--findings",
1654 "reports/audit.json",
1655 ])
1656 .unwrap();
1657 if let Commands::Import(args) = cli.command {
1658 assert_eq!(args.from, "mythril");
1659 assert_eq!(
1660 args.input.as_deref(),
1661 Some(std::path::Path::new("mythril_out.json"))
1662 );
1663 assert_eq!(
1664 args.findings.as_deref(),
1665 Some(std::path::Path::new("reports/audit.json"))
1666 );
1667 } else {
1668 panic!("Expected Import command");
1669 }
1670 }
1671
1672 #[test]
1673 fn test_import_args_defaults() {
1674 let cli = Cli::try_parse_from(["forge-guard", "import"]).unwrap();
1675 if let Commands::Import(args) = cli.command {
1676 assert_eq!(args.from, "slither");
1677 assert!(args.input.is_none());
1678 assert!(args.findings.is_none());
1679 assert!(args.output.is_none());
1680 } else {
1681 panic!("Expected Import command");
1682 }
1683 }
1684
1685 #[test]
1686 fn test_cli_parse_notify() {
1687 let cli = Cli::try_parse_from(["forge-guard", "notify", "--dry-run"]).unwrap();
1688 assert!(matches!(cli.command, Commands::Notify(_)));
1689 }
1690
1691 #[test]
1692 fn test_notify_args() {
1693 let cli = Cli::try_parse_from([
1694 "forge-guard",
1695 "notify",
1696 "--webhook",
1697 "https://hooks.slack.com/services/T/B/X",
1698 "--findings",
1699 "reports/audit.json",
1700 "--on-critical",
1701 "--title",
1702 "Nightly audit",
1703 ])
1704 .unwrap();
1705 if let Commands::Notify(args) = cli.command {
1706 assert_eq!(
1707 args.webhook.as_deref(),
1708 Some("https://hooks.slack.com/services/T/B/X")
1709 );
1710 assert_eq!(
1711 args.findings.as_deref(),
1712 Some(std::path::Path::new("reports/audit.json"))
1713 );
1714 assert!(args.on_critical);
1715 assert!(!args.on_high);
1716 assert!(!args.dry_run);
1717 assert_eq!(args.title, "Nightly audit");
1718 } else {
1719 panic!("Expected Notify command");
1720 }
1721 }
1722
1723 #[test]
1724 fn test_notify_defaults() {
1725 let cli = Cli::try_parse_from(["forge-guard", "notify"]).unwrap();
1726 if let Commands::Notify(args) = cli.command {
1727 assert_eq!(args.title, "Forge Guard Audit");
1728 assert!(args.webhook.is_none());
1729 assert!(args.kind.is_none());
1730 assert!(args.severity.is_none());
1731 assert!(!args.dry_run);
1732 } else {
1733 panic!("Expected Notify command");
1734 }
1735 }
1736
1737 #[test]
1738 fn test_audit_suppression_flags() {
1739 let cli = Cli::try_parse_from([
1740 "forge-guard",
1741 "audit",
1742 "--suppressions",
1743 ".forge-guard-suppressions",
1744 "--show-suppressed",
1745 ])
1746 .unwrap();
1747 if let Commands::Audit(args) = cli.command {
1748 assert_eq!(
1749 args.suppressions.as_deref(),
1750 Some(std::path::Path::new(".forge-guard-suppressions"))
1751 );
1752 assert!(args.show_suppressed);
1753 assert!(!args.generate_suppressions);
1754 assert!(!args.notify);
1755 } else {
1756 panic!("Expected Audit command");
1757 }
1758 }
1759
1760 #[test]
1761 fn test_audit_generate_suppressions_and_notify() {
1762 let cli = Cli::try_parse_from([
1763 "forge-guard",
1764 "audit",
1765 "--generate-suppressions",
1766 "--notify",
1767 ])
1768 .unwrap();
1769 if let Commands::Audit(args) = cli.command {
1770 assert!(args.generate_suppressions);
1771 assert!(args.notify);
1772 assert!(args.suppressions.is_none());
1773 } else {
1774 panic!("Expected Audit command");
1775 }
1776 }
1777
1778 #[test]
1779 fn test_deploy_notify_flag() {
1780 let cli = Cli::try_parse_from(["forge-guard", "deploy", "--notify"]).unwrap();
1781 if let Commands::Deploy(args) = cli.command {
1782 assert!(args.notify);
1783 } else {
1784 panic!("Expected Deploy command");
1785 }
1786 }
1787
1788 #[test]
1789 fn test_deploy_safe_notify_flag() {
1790 let cli = Cli::try_parse_from(["forge-guard", "deploy-safe", "--notify"]).unwrap();
1791 if let Commands::DeploySafe(args) = cli.command {
1792 assert!(args.notify);
1793 } else {
1794 panic!("Expected DeploySafe command");
1795 }
1796 }
1797}