1pub(crate) mod 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 Commands::Dashboard(args) => crate::dashboard::run(args).context("Dashboard failed"),
78 }
79 }
80}
81
82#[derive(Subcommand, Debug)]
84pub enum Commands {
85 Audit(AuditArgs),
87 Deploy(DeployArgs),
89 #[command(name = "deploy-safe")]
91 DeploySafe(DeploySafeArgs),
92 Fuzz(FuzzArgs),
94 Invariant(InvariantArgs),
96 Simulate(SimulateArgs),
98 Gas(GasArgs),
100 Report(ReportArgs),
102 Verify(VerifyArgs),
104 Doctor(DoctorArgs),
106 Watch(WatchArgs),
108 Ci(CiArgs),
110 Benchmark(BenchmarkArgs),
112 Scan(ScanArgs),
114 #[command(name = "upgrade-check")]
116 UpgradeCheck(UpgradeCheckArgs),
117 Plugins(PluginArgs),
119 Chain(ChainArgs),
121 Sbom(SbomArgs),
123 #[command(name = "install-hook")]
125 InstallHook(install_hook::InstallHookArgs),
126
127 Security(SecurityArgs),
129 Import(ImportArgs),
131 Notify(NotifyArgs),
133 Dashboard(DashboardArgs),
135}
136
137#[derive(Debug, Default, Clone, clap::Args)]
141pub struct SharedFlags {
142 #[arg(long, global = true, default_value = "ethereum")]
144 pub chain: String,
145
146 #[arg(long, global = true, default_value = ".")]
148 pub project: PathBuf,
149
150 #[arg(long, global = true)]
152 pub json: bool,
153
154 #[arg(long, global = true)]
156 pub markdown: bool,
157
158 #[arg(long, global = true)]
160 pub html: bool,
161
162 #[arg(long, global = true)]
164 pub strict: bool,
165
166 #[arg(long, global = true)]
168 pub offline: bool,
169
170 #[arg(long, global = true)]
172 pub production: bool,
173
174 #[arg(long, global = true)]
176 pub report: bool,
177
178 #[arg(long, global = true, default_value = "4")]
180 pub parallelism: usize,
181}
182
183#[derive(Debug, clap::Args)]
186pub struct AuditArgs {
187 #[command(flatten)]
188 pub shared: SharedFlags,
189
190 #[arg(long, short)]
192 pub full: bool,
193
194 #[arg(long)]
196 pub quick: bool,
197
198 #[arg(long)]
200 pub summary: bool,
201
202 #[arg(long)]
204 pub ai: bool,
205
206 #[arg(long, default_value = "openai")]
208 pub ai_provider: String,
209
210 #[arg(long, default_value = "gpt-5")]
212 pub ai_model: String,
213
214 #[arg(long)]
216 pub ai_api_key: Option<String>,
217
218 #[arg(long)]
220 pub ollama_endpoint: Option<String>,
221
222 #[arg(long)]
224 pub ai_full: bool,
225
226 #[arg(long)]
228 pub exploit: bool,
229
230 #[arg(long)]
232 pub gas: bool,
233
234 #[arg(long)]
236 pub all_chains: bool,
237
238 #[arg(long, default_value = "4")]
240 pub max_parallel_chains: usize,
241
242 #[arg(long, default_value = "src")]
244 pub sources: String,
245
246 #[arg(long)]
248 pub exclude: Option<String>,
249
250 #[arg(long)]
252 pub template: Option<String>,
253
254 #[arg(long)]
256 pub list_templates: bool,
257
258 #[arg(long)]
260 pub suppressions: Option<PathBuf>,
261
262 #[arg(long)]
264 pub show_suppressed: bool,
265
266 #[arg(long)]
268 pub generate_suppressions: bool,
269
270 #[arg(long)]
272 pub notify: bool,
273
274 #[arg(long)]
277 pub enable_history: bool,
278}
279
280#[derive(Debug, clap::Args)]
281pub struct DeployArgs {
282 #[command(flatten)]
283 pub shared: SharedFlags,
284
285 pub contract: Option<String>,
287
288 #[arg(long)]
290 pub force: bool,
291
292 #[arg(long)]
294 pub args: Option<String>,
295
296 #[arg(long)]
298 pub salt: Option<String>,
299
300 #[arg(long)]
302 pub verify: bool,
303
304 #[arg(long)]
306 pub notify: bool,
307}
308
309#[derive(Debug, clap::Args)]
310pub struct DeploySafeArgs {
311 #[command(flatten)]
312 pub shared: SharedFlags,
313
314 pub contract: Option<String>,
316
317 #[arg(long)]
319 pub args: Option<String>,
320
321 #[arg(long)]
323 pub verify: bool,
324
325 #[arg(long)]
327 pub notify: bool,
328}
329
330#[derive(Debug, clap::Args)]
331pub struct FuzzArgs {
332 #[command(flatten)]
333 pub shared: SharedFlags,
334
335 #[arg(long, default_value = "10000")]
337 pub runs: u32,
338
339 #[arg(long)]
341 pub seed: Option<u64>,
342
343 #[arg(long)]
345 pub test: Option<String>,
346
347 pub contract: Option<String>,
349}
350
351#[derive(Debug, clap::Args)]
352pub struct InvariantArgs {
353 #[command(flatten)]
354 pub shared: SharedFlags,
355
356 #[arg(long, default_value = "1000")]
358 pub runs: u32,
359
360 #[arg(long, default_value = "100")]
362 pub depth: u32,
363
364 pub contract: Option<String>,
366
367 #[arg(long)]
369 pub fail_on_revert: bool,
370}
371
372#[derive(Debug, clap::Args)]
373pub struct SimulateArgs {
374 #[command(flatten)]
375 pub shared: SharedFlags,
376
377 #[arg(long, default_value = "100")]
379 pub blocks: u32,
380
381 #[arg(long)]
383 pub deployer: Option<String>,
384
385 #[arg(long)]
387 pub mev: bool,
388
389 pub contract: Option<String>,
391}
392
393#[derive(Debug, clap::Args)]
394pub struct GasArgs {
395 #[command(flatten)]
396 pub shared: SharedFlags,
397
398 pub contract: Option<String>,
400
401 #[arg(long)]
403 pub diff: Option<String>,
404
405 #[arg(long)]
407 pub all: bool,
408
409 #[arg(long, default_value = "50000")]
411 pub warn_threshold: u64,
412}
413
414#[derive(Debug, clap::Args)]
415pub struct ReportArgs {
416 #[command(flatten)]
417 pub shared: SharedFlags,
418
419 pub input: Option<PathBuf>,
421
422 #[arg(long, default_value = "markdown")]
424 pub format: String,
425
426 #[arg(long)]
428 pub output: Option<PathBuf>,
429
430 #[arg(long)]
432 pub exploit_paths: bool,
433
434 #[arg(long)]
436 pub summary: bool,
437
438 #[arg(long)]
440 pub history: bool,
441
442 #[arg(long)]
444 pub regression: bool,
445}
446
447#[derive(Debug, clap::Args)]
448pub struct VerifyArgs {
449 #[command(flatten)]
450 pub shared: SharedFlags,
451
452 pub address: Option<String>,
454
455 pub name: Option<String>,
457
458 #[arg(long)]
460 pub api_key: Option<String>,
461
462 #[arg(long)]
464 pub constructor_args: Option<String>,
465
466 #[arg(long)]
468 pub all: bool,
469}
470
471#[derive(Debug, clap::Args)]
472pub struct DoctorArgs {
473 #[command(flatten)]
474 pub shared: SharedFlags,
475
476 #[arg(long)]
478 pub fix: bool,
479
480 #[arg(long, short)]
482 pub verbose: bool,
483
484 #[arg(long)]
486 pub check: Option<String>,
487
488 #[arg(long)]
490 pub sync: bool,
491
492 #[arg(long)]
494 pub dry_run: bool,
495
496 #[arg(long)]
498 pub diff: bool,
499}
500
501#[derive(Debug, clap::Args)]
502pub struct WatchArgs {
503 #[command(flatten)]
504 pub shared: SharedFlags,
505
506 #[arg(long, default_value = "src")]
508 pub dirs: String,
509
510 #[arg(long, default_value = "500")]
512 pub debounce_ms: u64,
513
514 #[arg(long)]
516 pub exclude: Option<String>,
517
518 #[arg(long)]
520 pub full: bool,
521}
522
523#[derive(Debug, clap::Args)]
524pub struct DashboardArgs {
525 #[command(flatten)]
526 pub shared: SharedFlags,
527
528 #[arg(long, default_value = "9090")]
530 pub port: u16,
531
532 #[arg(long, default_value = "127.0.0.1")]
534 pub host: String,
535
536 #[arg(long)]
538 pub watch: bool,
539
540 #[arg(long, default_value = "src")]
542 pub dirs: String,
543
544 #[arg(long, default_value = "500")]
546 pub debounce_ms: u64,
547
548 #[arg(long)]
550 pub open: bool,
551
552 #[arg(long)]
554 pub full: bool,
555}
556
557#[derive(Debug, clap::Args)]
558pub struct CiArgs {
559 #[command(flatten)]
560 pub shared: SharedFlags,
561
562 #[arg(long, default_value = "github")]
564 pub platform: String,
565
566 #[arg(long, default_value = ".github/workflows")]
568 pub output: PathBuf,
569
570 #[arg(long)]
572 pub include_deploy: bool,
573
574 #[arg(long)]
576 pub overwrite: bool,
577}
578
579#[derive(Debug, clap::Args)]
580pub struct BenchmarkArgs {
581 #[command(flatten)]
582 pub shared: SharedFlags,
583
584 #[arg(long, default_value = "10")]
586 pub iterations: u32,
587
588 #[arg(long)]
590 pub compare: Option<PathBuf>,
591
592 #[arg(long)]
594 pub save: Option<PathBuf>,
595
596 #[arg(long)]
598 pub module: Option<String>,
599
600 #[arg(long, default_value = "3")]
602 pub warmup: u32,
603}
604
605#[derive(Debug, clap::Args)]
606pub struct SbomArgs {
607 #[command(flatten)]
608 pub shared: SharedFlags,
609
610 #[arg(long, default_value = "cyclonedx")]
612 pub format: String,
613
614 #[arg(long, short)]
616 pub output: Option<PathBuf>,
617
618 #[arg(long)]
620 pub ci: bool,
621}
622
623#[derive(Debug, clap::Args)]
624pub struct ScanArgs {
625 #[command(flatten)]
626 pub shared: SharedFlags,
627
628 #[arg(long, default_value = "1")]
630 pub depth: u32,
631
632 #[arg(long)]
634 pub update: bool,
635
636 #[arg(long)]
638 pub vulnerable_only: bool,
639
640 #[arg(long)]
642 pub fail_fast: bool,
643}
644
645#[derive(Debug, clap::Args)]
646pub struct UpgradeCheckArgs {
647 #[command(flatten)]
648 pub shared: SharedFlags,
649
650 pub proxy: Option<String>,
652
653 pub implementation: Option<String>,
655
656 #[arg(long)]
658 pub all: bool,
659
660 #[arg(long)]
662 pub storage_collision: bool,
663
664 #[arg(long)]
666 pub uups: bool,
667}
668
669#[derive(Debug, clap::Args)]
670pub struct PluginArgs {
671 #[command(flatten)]
672 pub shared: SharedFlags,
673
674 #[command(subcommand)]
676 pub action: Option<PluginAction>,
677}
678
679#[derive(Debug, Subcommand)]
680pub enum PluginAction {
681 List,
683 Install {
685 name: String,
686 source: Option<String>,
687 },
688 Remove { name: String },
690 Enable { name: String },
692 Disable { name: String },
694 New { name: String },
696}
697
698#[derive(Debug, clap::Args)]
699pub struct ChainArgs {
700 #[command(flatten)]
701 pub shared: SharedFlags,
702
703 #[command(subcommand)]
705 pub action: Option<ChainAction>,
706}
707
708#[derive(Debug, Subcommand)]
709pub enum ChainAction {
710 List,
712 Info { chain: String },
714 Add {
716 name: String,
717 rpc_url: Option<String>,
718 chain_id: Option<u64>,
719 },
720 Remove { name: String },
722 Test {
724 chain: String,
725 rpc_url: Option<String>,
726 },
727}
728
729#[derive(Debug, clap::Args)]
730pub struct SecurityArgs {
731 #[command(flatten)]
732 pub shared: SharedFlags,
733
734 #[command(subcommand)]
736 pub action: Option<SecurityAction>,
737}
738
739#[derive(Debug, Subcommand)]
740pub enum SecurityAction {
741 Config,
743 Threshold { score: u8 },
745 Enable { check: String },
747 Disable { check: String },
749 List,
751 Info { check: String },
753}
754
755#[derive(Debug, clap::Args)]
758pub struct ImportArgs {
759 #[command(flatten)]
760 pub shared: SharedFlags,
761
762 #[arg(long, default_value = "slither")]
764 pub from: String,
765
766 pub input: Option<PathBuf>,
768
769 #[arg(long)]
771 pub findings: Option<PathBuf>,
772
773 #[arg(long, short)]
775 pub output: Option<PathBuf>,
776}
777
778#[derive(Debug, clap::Args)]
779pub struct NotifyArgs {
780 #[command(flatten)]
781 pub shared: SharedFlags,
782
783 #[arg(long)]
785 pub webhook: Option<String>,
786
787 #[arg(long)]
789 pub kind: Option<String>,
790
791 #[arg(long)]
793 pub findings: Option<PathBuf>,
794
795 #[arg(long, default_value = "Forge Guard Audit")]
797 pub title: String,
798
799 #[arg(long)]
801 pub message: Option<String>,
802
803 #[arg(long)]
805 pub severity: Option<String>,
806
807 #[arg(long)]
809 pub on_critical: bool,
810
811 #[arg(long)]
813 pub on_high: bool,
814
815 #[arg(long)]
817 pub dry_run: bool,
818}
819
820#[cfg(test)]
821mod tests {
822 use super::*;
823 use clap::Parser;
824
825 #[test]
826 fn test_cli_parse_audit() {
827 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
828 assert!(matches!(cli.command, Commands::Audit(_)));
829 }
830
831 #[test]
832 fn test_cli_parse_deploy() {
833 let cli = Cli::try_parse_from(["forge-guard", "deploy"]).unwrap();
834 assert!(matches!(cli.command, Commands::Deploy(_)));
835 }
836
837 #[test]
838 fn test_cli_parse_deploy_safe() {
839 let cli = Cli::try_parse_from(["forge-guard", "deploy-safe"]).unwrap();
840 assert!(matches!(cli.command, Commands::DeploySafe(_)));
841 }
842
843 #[test]
844 fn test_cli_parse_fuzz() {
845 let cli = Cli::try_parse_from(["forge-guard", "fuzz"]).unwrap();
846 assert!(matches!(cli.command, Commands::Fuzz(_)));
847 }
848
849 #[test]
850 fn test_cli_parse_invariant() {
851 let cli = Cli::try_parse_from(["forge-guard", "invariant"]).unwrap();
852 assert!(matches!(cli.command, Commands::Invariant(_)));
853 }
854
855 #[test]
856 fn test_cli_parse_simulate() {
857 let cli = Cli::try_parse_from(["forge-guard", "simulate"]).unwrap();
858 assert!(matches!(cli.command, Commands::Simulate(_)));
859 }
860
861 #[test]
862 fn test_cli_parse_gas() {
863 let cli = Cli::try_parse_from(["forge-guard", "gas"]).unwrap();
864 assert!(matches!(cli.command, Commands::Gas(_)));
865 }
866
867 #[test]
868 fn test_cli_parse_report() {
869 let cli = Cli::try_parse_from(["forge-guard", "report"]).unwrap();
870 assert!(matches!(cli.command, Commands::Report(_)));
871 }
872
873 #[test]
874 fn test_cli_parse_verify() {
875 let cli = Cli::try_parse_from(["forge-guard", "verify"]).unwrap();
876 assert!(matches!(cli.command, Commands::Verify(_)));
877 }
878
879 #[test]
880 fn test_cli_parse_doctor() {
881 let cli = Cli::try_parse_from(["forge-guard", "doctor"]).unwrap();
882 assert!(matches!(cli.command, Commands::Doctor(_)));
883 }
884
885 #[test]
886 fn test_cli_parse_watch() {
887 let cli = Cli::try_parse_from(["forge-guard", "watch"]).unwrap();
888 assert!(matches!(cli.command, Commands::Watch(_)));
889 }
890
891 #[test]
892 fn test_cli_parse_dashboard() {
893 let cli = Cli::try_parse_from(["forge-guard", "dashboard"]).unwrap();
894 assert!(matches!(cli.command, Commands::Dashboard(_)));
895 }
896
897 #[test]
898 fn test_cli_parse_dashboard_with_flags() {
899 let cli = Cli::try_parse_from([
900 "forge-guard",
901 "dashboard",
902 "--port",
903 "8080",
904 "--host",
905 "0.0.0.0",
906 "--watch",
907 "--dirs",
908 "src,contracts",
909 "--debounce-ms",
910 "250",
911 "--open",
912 ])
913 .unwrap();
914 if let Commands::Dashboard(args) = cli.command {
915 assert_eq!(args.port, 8080);
916 assert_eq!(args.host, "0.0.0.0");
917 assert!(args.watch);
918 assert_eq!(args.dirs, "src,contracts");
919 assert_eq!(args.debounce_ms, 250);
920 assert!(args.open);
921 } else {
922 panic!("expected Dashboard command");
923 }
924 }
925
926 #[test]
927 fn test_cli_parse_dashboard_defaults() {
928 let cli = Cli::try_parse_from(["forge-guard", "dashboard"]).unwrap();
929 if let Commands::Dashboard(args) = cli.command {
930 assert_eq!(args.port, 9090, "default port should be 9090");
931 assert_eq!(args.host, "127.0.0.1", "default host should be 127.0.0.1");
932 assert!(!args.watch);
933 assert_eq!(args.dirs, "src");
934 assert_eq!(args.debounce_ms, 500);
935 assert!(!args.open);
936 assert!(!args.full);
937 } else {
938 panic!("expected Dashboard command");
939 }
940 }
941
942 #[test]
943 fn test_cli_parse_ci() {
944 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
945 assert!(matches!(cli.command, Commands::Ci(_)));
946 }
947
948 #[test]
949 fn test_cli_parse_benchmark() {
950 let cli = Cli::try_parse_from(["forge-guard", "benchmark"]).unwrap();
951 assert!(matches!(cli.command, Commands::Benchmark(_)));
952 }
953
954 #[test]
955 fn test_cli_parse_scan() {
956 let cli = Cli::try_parse_from(["forge-guard", "scan"]).unwrap();
957 assert!(matches!(cli.command, Commands::Scan(_)));
958 }
959
960 #[test]
961 fn test_cli_parse_upgrade_check() {
962 let cli = Cli::try_parse_from(["forge-guard", "upgrade-check"]).unwrap();
963 assert!(matches!(cli.command, Commands::UpgradeCheck(_)));
964 }
965
966 #[test]
967 fn test_cli_parse_plugins() {
968 let cli = Cli::try_parse_from(["forge-guard", "plugins"]).unwrap();
969 assert!(matches!(cli.command, Commands::Plugins(_)));
970 }
971
972 #[test]
973 fn test_cli_parse_chain() {
974 let cli = Cli::try_parse_from(["forge-guard", "chain"]).unwrap();
975 assert!(matches!(cli.command, Commands::Chain(_)));
976 }
977
978 #[test]
979 fn test_cli_parse_security() {
980 let cli = Cli::try_parse_from(["forge-guard", "security"]).unwrap();
981 assert!(matches!(cli.command, Commands::Security(_)));
982 }
983
984 #[test]
985 fn test_cli_parse_install_hook() {
986 let cli = Cli::try_parse_from(["forge-guard", "install-hook"]).unwrap();
987 assert!(matches!(cli.command, Commands::InstallHook(_)));
988 }
989
990 #[test]
991 fn test_cli_parse_install_hook_uninstall() {
992 let cli = Cli::try_parse_from(["forge-guard", "install-hook", "--uninstall"]).unwrap();
993 assert!(matches!(cli.command, Commands::InstallHook(_)));
994 }
995
996 #[test]
997 fn test_cli_parse_doctor_sync() {
998 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--sync"]).unwrap();
999 if let Commands::Doctor(args) = cli.command {
1000 assert!(args.sync);
1001 } else {
1002 panic!("Expected Doctor command");
1003 }
1004 }
1005
1006 #[test]
1007 fn test_cli_parse_doctor_dry_run() {
1008 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--sync", "--dry-run"]).unwrap();
1009 if let Commands::Doctor(args) = cli.command {
1010 assert!(args.sync);
1011 assert!(args.dry_run);
1012 } else {
1013 panic!("Expected Doctor command");
1014 }
1015 }
1016
1017 #[test]
1018 fn test_cli_parse_audit_template() {
1019 let cli = Cli::try_parse_from(["forge-guard", "audit", "--template", "defi"]).unwrap();
1020 if let Commands::Audit(args) = cli.command {
1021 assert_eq!(args.template.as_deref(), Some("defi"));
1022 } else {
1023 panic!("Expected Audit command");
1024 }
1025 }
1026
1027 #[test]
1028 fn test_cli_parse_audit_list_templates() {
1029 let cli = Cli::try_parse_from(["forge-guard", "audit", "--list-templates"]).unwrap();
1030 if let Commands::Audit(args) = cli.command {
1031 assert!(args.list_templates);
1032 } else {
1033 panic!("Expected Audit command");
1034 }
1035 }
1036
1037 #[test]
1038 fn test_shared_flags_defaults() {
1039 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
1040 if let Commands::Audit(args) = cli.command {
1041 assert_eq!(args.shared.chain, "ethereum");
1042 assert!(!args.shared.json);
1043 assert!(!args.shared.markdown);
1044 assert!(!args.shared.strict);
1045 assert!(!args.shared.offline);
1046 assert!(!args.shared.production);
1047 assert!(!args.shared.report);
1048 assert_eq!(args.shared.parallelism, 4);
1049 } else {
1050 panic!("Expected Audit command");
1051 }
1052 }
1053
1054 #[test]
1055 fn test_shared_flags_custom() {
1056 let cli = Cli::try_parse_from([
1057 "forge-guard",
1058 "audit",
1059 "--chain",
1060 "base",
1061 "--json",
1062 "--strict",
1063 "--offline",
1064 "--production",
1065 "--parallelism",
1066 "8",
1067 "--project",
1068 "/tmp/project",
1069 ])
1070 .unwrap();
1071 if let Commands::Audit(args) = cli.command {
1072 assert_eq!(args.shared.chain, "base");
1073 assert!(args.shared.json);
1074 assert!(args.shared.strict);
1075 assert!(args.shared.offline);
1076 assert!(args.shared.production);
1077 assert_eq!(args.shared.parallelism, 8);
1078 assert_eq!(
1079 args.shared.project,
1080 std::path::PathBuf::from("/tmp/project")
1081 );
1082 } else {
1083 panic!("Expected Audit command");
1084 }
1085 }
1086
1087 #[test]
1088 fn test_audit_args_defaults() {
1089 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
1090 if let Commands::Audit(args) = cli.command {
1091 assert!(!args.full);
1092 assert!(!args.quick);
1093 assert!(!args.summary);
1094 assert!(!args.ai);
1095 assert_eq!(args.ai_provider, "openai");
1096 assert_eq!(args.ai_model, "gpt-5");
1097 assert!(args.ai_api_key.is_none());
1098 assert!(!args.exploit);
1099 assert!(!args.gas);
1100 assert!(!args.all_chains);
1101 assert_eq!(args.sources, "src");
1102 assert!(args.exclude.is_none());
1103 } else {
1104 panic!("Expected Audit command");
1105 }
1106 }
1107
1108 #[test]
1109 fn test_audit_args_full() {
1110 let cli = Cli::try_parse_from([
1111 "forge-guard",
1112 "audit",
1113 "--full",
1114 "--chain",
1115 "arbitrum",
1116 "--json",
1117 "--report",
1118 "--exploit",
1119 "--gas",
1120 ])
1121 .unwrap();
1122 if let Commands::Audit(args) = cli.command {
1123 assert!(args.full);
1124 assert!(args.exploit);
1125 assert!(args.gas);
1126 assert!(args.shared.json);
1127 assert!(args.shared.report);
1128 assert_eq!(args.shared.chain, "arbitrum");
1129 } else {
1130 panic!("Expected Audit command");
1131 }
1132 }
1133
1134 #[test]
1135 fn test_audit_ai_args() {
1136 let cli = Cli::try_parse_from([
1137 "forge-guard",
1138 "audit",
1139 "--ai",
1140 "--ai-provider",
1141 "claude",
1142 "--ai-model",
1143 "claude-5-sonnet-20260701",
1144 "--ai-full",
1145 ])
1146 .unwrap();
1147 if let Commands::Audit(args) = cli.command {
1148 assert!(args.ai);
1149 assert_eq!(args.ai_provider, "claude");
1150 assert_eq!(args.ai_model, "claude-5-sonnet-20260701");
1151 assert!(args.ai_full);
1152 } else {
1153 panic!("Expected Audit command");
1154 }
1155 }
1156
1157 #[test]
1158 fn test_deploy_args() {
1159 let cli = Cli::try_parse_from([
1160 "forge-guard",
1161 "deploy",
1162 "MyContract",
1163 "--force",
1164 "--salt",
1165 "0xabc",
1166 "--verify",
1167 "--args",
1168 "arg1,arg2",
1169 ])
1170 .unwrap();
1171 if let Commands::Deploy(args) = cli.command {
1172 assert_eq!(args.contract.as_deref(), Some("MyContract"));
1173 assert!(args.force);
1174 assert_eq!(args.salt.as_deref(), Some("0xabc"));
1175 assert!(args.verify);
1176 assert_eq!(args.args.as_deref(), Some("arg1,arg2"));
1177 } else {
1178 panic!("Expected Deploy command");
1179 }
1180 }
1181
1182 #[test]
1183 fn test_fuzz_args() {
1184 let cli = Cli::try_parse_from([
1185 "forge-guard",
1186 "fuzz",
1187 "--runs",
1188 "50000",
1189 "--seed",
1190 "42",
1191 "--test",
1192 "testFuzzDeposit",
1193 "Vault",
1194 ])
1195 .unwrap();
1196 if let Commands::Fuzz(args) = cli.command {
1197 assert_eq!(args.runs, 50_000);
1198 assert_eq!(args.seed, Some(42));
1199 assert_eq!(args.test.as_deref(), Some("testFuzzDeposit"));
1200 assert_eq!(args.contract.as_deref(), Some("Vault"));
1201 } else {
1202 panic!("Expected Fuzz command");
1203 }
1204 }
1205
1206 #[test]
1207 fn test_simulate_args() {
1208 let cli = Cli::try_parse_from([
1209 "forge-guard",
1210 "simulate",
1211 "MyContract",
1212 "--blocks",
1213 "200",
1214 "--mev",
1215 ])
1216 .unwrap();
1217 if let Commands::Simulate(args) = cli.command {
1218 assert_eq!(args.contract.as_deref(), Some("MyContract"));
1219 assert_eq!(args.blocks, 200);
1220 assert!(args.mev);
1221 } else {
1222 panic!("Expected Simulate command");
1223 }
1224 }
1225
1226 #[test]
1227 fn test_invariant_args() {
1228 let cli = Cli::try_parse_from([
1229 "forge-guard",
1230 "invariant",
1231 "--runs",
1232 "2000",
1233 "--depth",
1234 "150",
1235 "--fail-on-revert",
1236 ])
1237 .unwrap();
1238 if let Commands::Invariant(args) = cli.command {
1239 assert_eq!(args.runs, 2000);
1240 assert_eq!(args.depth, 150);
1241 assert!(args.fail_on_revert);
1242 } else {
1243 panic!("Expected Invariant command");
1244 }
1245 }
1246
1247 #[test]
1248 fn test_ci_args_defaults() {
1249 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
1250 if let Commands::Ci(args) = cli.command {
1251 assert_eq!(args.platform, "github");
1252 assert_eq!(args.output, std::path::PathBuf::from(".github/workflows"));
1253 assert!(!args.include_deploy);
1254 assert!(!args.overwrite);
1255 } else {
1256 panic!("Expected Ci command");
1257 }
1258 }
1259
1260 #[test]
1261 fn test_ci_args_custom() {
1262 let cli = Cli::try_parse_from([
1263 "forge-guard",
1264 "ci",
1265 "--platform",
1266 "gitlab",
1267 "--include-deploy",
1268 "--overwrite",
1269 "--output",
1270 ".gitlab",
1271 ])
1272 .unwrap();
1273 if let Commands::Ci(args) = cli.command {
1274 assert_eq!(args.platform, "gitlab");
1275 assert!(args.include_deploy);
1276 assert!(args.overwrite);
1277 assert_eq!(args.output, std::path::PathBuf::from(".gitlab"));
1278 } else {
1279 panic!("Expected Ci command");
1280 }
1281 }
1282
1283 #[test]
1284 fn test_benchmark_args() {
1285 let cli = Cli::try_parse_from([
1286 "forge-guard",
1287 "benchmark",
1288 "--iterations",
1289 "50",
1290 "--warmup",
1291 "5",
1292 "--module",
1293 "pattern_matching",
1294 ])
1295 .unwrap();
1296 if let Commands::Benchmark(args) = cli.command {
1297 assert_eq!(args.iterations, 50);
1298 assert_eq!(args.warmup, 5);
1299 assert_eq!(args.module.as_deref(), Some("pattern_matching"));
1300 } else {
1301 panic!("Expected Benchmark command");
1302 }
1303 }
1304
1305 #[test]
1306 fn test_gas_args() {
1307 let cli =
1308 Cli::try_parse_from(["forge-guard", "gas", "--all", "--warn-threshold", "100000"])
1309 .unwrap();
1310 if let Commands::Gas(args) = cli.command {
1311 assert!(args.all);
1312 assert_eq!(args.warn_threshold, 100000);
1313 assert!(args.contract.is_none());
1314 } else {
1315 panic!("Expected Gas command");
1316 }
1317 }
1318
1319 #[test]
1320 fn test_scan_args() {
1321 let cli = Cli::try_parse_from([
1322 "forge-guard",
1323 "scan",
1324 "--depth",
1325 "2",
1326 "--update",
1327 "--vulnerable-only",
1328 "--fail-fast",
1329 ])
1330 .unwrap();
1331 if let Commands::Scan(args) = cli.command {
1332 assert_eq!(args.depth, 2);
1333 assert!(args.update);
1334 assert!(args.vulnerable_only);
1335 assert!(args.fail_fast);
1336 } else {
1337 panic!("Expected Scan command");
1338 }
1339 }
1340
1341 #[test]
1342 fn test_upgrade_check_args() {
1343 let cli = Cli::try_parse_from([
1344 "forge-guard",
1345 "upgrade-check",
1346 "--storage-collision",
1347 "--uups",
1348 "--all",
1349 ])
1350 .unwrap();
1351 if let Commands::UpgradeCheck(args) = cli.command {
1352 assert!(args.storage_collision);
1353 assert!(args.uups);
1354 assert!(args.all);
1355 } else {
1356 panic!("Expected UpgradeCheck command");
1357 }
1358 }
1359
1360 #[test]
1361 fn test_doctor_args() {
1362 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--fix", "--verbose"]).unwrap();
1363 if let Commands::Doctor(args) = cli.command {
1364 assert!(args.fix);
1365 assert!(args.verbose);
1366 assert!(args.check.is_none());
1367 } else {
1368 panic!("Expected Doctor command");
1369 }
1370 }
1371
1372 #[test]
1373 fn test_verify_args() {
1374 let cli = Cli::try_parse_from([
1375 "forge-guard",
1376 "verify",
1377 "0x1234",
1378 "MyContract",
1379 "--api-key",
1380 "test-key",
1381 "--all",
1382 ])
1383 .unwrap();
1384 if let Commands::Verify(args) = cli.command {
1385 assert_eq!(args.address.as_deref(), Some("0x1234"));
1386 assert_eq!(args.name.as_deref(), Some("MyContract"));
1387 assert_eq!(args.api_key.as_deref(), Some("test-key"));
1388 assert!(args.all);
1389 } else {
1390 panic!("Expected Verify command");
1391 }
1392 }
1393
1394 #[test]
1395 fn test_watch_args() {
1396 let cli = Cli::try_parse_from([
1397 "forge-guard",
1398 "watch",
1399 "--dirs",
1400 "src,test-contracts",
1401 "--debounce-ms",
1402 "1000",
1403 "--full",
1404 ])
1405 .unwrap();
1406 if let Commands::Watch(args) = cli.command {
1407 assert_eq!(args.dirs, "src,test-contracts");
1408 assert_eq!(args.debounce_ms, 1000);
1409 assert!(args.full);
1410 } else {
1411 panic!("Expected Watch command");
1412 }
1413 }
1414
1415 #[test]
1416 fn test_plugins_list() {
1417 let cli = Cli::try_parse_from(["forge-guard", "plugins", "list"]).unwrap();
1418 if let Commands::Plugins(args) = cli.command {
1419 assert!(matches!(args.action, Some(PluginAction::List)));
1420 } else {
1421 panic!("Expected Plugins command");
1422 }
1423 }
1424
1425 #[test]
1426 fn test_plugins_install() {
1427 let cli = Cli::try_parse_from(["forge-guard", "plugins", "install", "my-plugin"]).unwrap();
1428 if let Commands::Plugins(args) = cli.command {
1429 assert!(matches!(args.action, Some(PluginAction::Install { .. })));
1430 } else {
1431 panic!("Expected Plugins command");
1432 }
1433 }
1434
1435 #[test]
1436 fn test_plugins_remove() {
1437 let cli = Cli::try_parse_from(["forge-guard", "plugins", "remove", "bad-plugin"]).unwrap();
1438 if let Commands::Plugins(args) = cli.command {
1439 assert!(matches!(args.action, Some(PluginAction::Remove { .. })));
1440 } else {
1441 panic!("Expected Plugins command");
1442 }
1443 }
1444
1445 #[test]
1446 fn test_plugins_enable() {
1447 let cli = Cli::try_parse_from(["forge-guard", "plugins", "enable", "my-plugin"]).unwrap();
1448 if let Commands::Plugins(args) = cli.command {
1449 assert!(matches!(args.action, Some(PluginAction::Enable { .. })));
1450 } else {
1451 panic!("Expected Plugins command");
1452 }
1453 }
1454
1455 #[test]
1456 fn test_plugins_disable() {
1457 let cli = Cli::try_parse_from(["forge-guard", "plugins", "disable", "my-plugin"]).unwrap();
1458 if let Commands::Plugins(args) = cli.command {
1459 assert!(matches!(args.action, Some(PluginAction::Disable { .. })));
1460 } else {
1461 panic!("Expected Plugins command");
1462 }
1463 }
1464
1465 #[test]
1466 fn test_plugins_new() {
1467 let cli =
1468 Cli::try_parse_from(["forge-guard", "plugins", "new", "my-awesome-plugin"]).unwrap();
1469 if let Commands::Plugins(args) = cli.command {
1470 assert!(matches!(args.action, Some(PluginAction::New { .. })));
1471 } else {
1472 panic!("Expected Plugins command");
1473 }
1474 }
1475
1476 #[test]
1477 fn test_chain_list() {
1478 let cli = Cli::try_parse_from(["forge-guard", "chain", "list"]).unwrap();
1479 if let Commands::Chain(args) = cli.command {
1480 assert!(matches!(args.action, Some(ChainAction::List)));
1481 } else {
1482 panic!("Expected Chain command");
1483 }
1484 }
1485
1486 #[test]
1487 fn test_chain_info() {
1488 let cli = Cli::try_parse_from(["forge-guard", "chain", "info", "base"]).unwrap();
1489 if let Commands::Chain(args) = cli.command {
1490 assert!(matches!(args.action, Some(ChainAction::Info { .. })));
1491 } else {
1492 panic!("Expected Chain command");
1493 }
1494 }
1495
1496 #[test]
1497 fn test_chain_add() {
1498 let cli = Cli::try_parse_from([
1499 "forge-guard",
1500 "chain",
1501 "add",
1502 "my-chain",
1503 "https://rpc.my-chain.io",
1504 "99999",
1505 ])
1506 .unwrap();
1507 if let Commands::Chain(args) = cli.command {
1508 assert!(matches!(args.action, Some(ChainAction::Add { .. })));
1509 } else {
1510 panic!("Expected Chain command");
1511 }
1512 }
1513
1514 #[test]
1515 fn test_security_list() {
1516 let cli = Cli::try_parse_from(["forge-guard", "security", "list"]).unwrap();
1517 if let Commands::Security(args) = cli.command {
1518 assert!(matches!(args.action, Some(SecurityAction::List)));
1519 } else {
1520 panic!("Expected Security command");
1521 }
1522 }
1523
1524 #[test]
1525 fn test_security_threshold() {
1526 let cli = Cli::try_parse_from(["forge-guard", "security", "threshold", "85"]).unwrap();
1527 if let Commands::Security(args) = cli.command {
1528 assert!(matches!(
1529 args.action,
1530 Some(SecurityAction::Threshold { .. })
1531 ));
1532 } else {
1533 panic!("Expected Security command");
1534 }
1535 }
1536
1537 #[test]
1538 fn test_markdown_flag() {
1539 let cli = Cli::try_parse_from(["forge-guard", "audit", "--markdown", "--report"]).unwrap();
1540 if let Commands::Audit(args) = cli.command {
1541 assert!(args.shared.markdown);
1542 assert!(args.shared.report);
1543 } else {
1544 panic!("Expected Audit command");
1545 }
1546 }
1547
1548 #[test]
1549 fn test_quick_audit() {
1550 let cli = Cli::try_parse_from(["forge-guard", "audit", "--quick", "--summary"]).unwrap();
1551 if let Commands::Audit(args) = cli.command {
1552 assert!(args.quick);
1553 assert!(args.summary);
1554 } else {
1555 panic!("Expected Audit command");
1556 }
1557 }
1558
1559 #[test]
1560 fn test_enable_history_flag() {
1561 let cli = Cli::try_parse_from(["forge-guard", "audit", "--enable-history"]).unwrap();
1562 if let Commands::Audit(args) = cli.command {
1563 assert!(args.enable_history);
1564 } else {
1565 panic!("Expected Audit command");
1566 }
1567 }
1568
1569 #[test]
1570 fn test_report_history_flag() {
1571 let cli = Cli::try_parse_from(["forge-guard", "report", "--history"]).unwrap();
1572 if let Commands::Report(args) = cli.command {
1573 assert!(args.history);
1574 assert!(!args.regression);
1575 } else {
1576 panic!("Expected Report command");
1577 }
1578 }
1579
1580 #[test]
1581 fn test_report_regression_flag() {
1582 let cli = Cli::try_parse_from(["forge-guard", "report", "--regression"]).unwrap();
1583 if let Commands::Report(args) = cli.command {
1584 assert!(args.regression);
1585 assert!(!args.history);
1586 } else {
1587 panic!("Expected Report command");
1588 }
1589 }
1590
1591 #[test]
1592 fn test_all_chains_flag() {
1593 let cli = Cli::try_parse_from(["forge-guard", "audit", "--all-chains"]).unwrap();
1594 if let Commands::Audit(args) = cli.command {
1595 assert!(args.all_chains);
1596 assert_eq!(args.max_parallel_chains, 4);
1597 } else {
1598 panic!("Expected Audit command");
1599 }
1600 }
1601
1602 #[test]
1603 fn test_max_parallel_chains_default() {
1604 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
1605 if let Commands::Audit(args) = cli.command {
1606 assert_eq!(args.max_parallel_chains, 4);
1607 } else {
1608 panic!("Expected Audit command");
1609 }
1610 }
1611
1612 #[test]
1613 fn test_max_parallel_chains_custom() {
1614 let cli = Cli::try_parse_from([
1615 "forge-guard",
1616 "audit",
1617 "--all-chains",
1618 "--max-parallel-chains",
1619 "2",
1620 ])
1621 .unwrap();
1622 if let Commands::Audit(args) = cli.command {
1623 assert!(args.all_chains);
1624 assert_eq!(args.max_parallel_chains, 2);
1625 } else {
1626 panic!("Expected Audit command");
1627 }
1628 }
1629
1630 #[test]
1631 fn test_deploy_safe_args() {
1632 let cli =
1633 Cli::try_parse_from(["forge-guard", "deploy-safe", "SecureVault", "--verify"]).unwrap();
1634 if let Commands::DeploySafe(args) = cli.command {
1635 assert_eq!(args.contract.as_deref(), Some("SecureVault"));
1636 assert!(args.verify);
1637 } else {
1638 panic!("Expected DeploySafe command");
1639 }
1640 }
1641
1642 #[test]
1643 fn test_report_args_json() {
1644 let cli = Cli::try_parse_from(["forge-guard", "report", "--format", "json"]).unwrap();
1645 if let Commands::Report(args) = cli.command {
1646 assert_eq!(args.format, "json");
1647 assert!(!args.summary);
1648 } else {
1649 panic!("Expected Report command");
1650 }
1651 }
1652
1653 #[test]
1654 fn test_verify_all_without_address() {
1655 let cli = Cli::try_parse_from(["forge-guard", "verify", "--all"]).unwrap();
1656 if let Commands::Verify(args) = cli.command {
1657 assert!(args.all);
1658 assert!(args.address.is_none());
1659 } else {
1660 panic!("Expected Verify command");
1661 }
1662 }
1663
1664 #[test]
1665 fn test_report_summary() {
1666 let cli = Cli::try_parse_from(["forge-guard", "report", "--summary"]).unwrap();
1667 if let Commands::Report(args) = cli.command {
1668 assert!(args.summary);
1669 } else {
1670 panic!("Expected Report command");
1671 }
1672 }
1673
1674 #[test]
1675 fn test_scan_vulnerable_only() {
1676 let cli = Cli::try_parse_from(["forge-guard", "scan", "--vulnerable-only"]).unwrap();
1677 if let Commands::Scan(args) = cli.command {
1678 assert!(args.vulnerable_only);
1679 } else {
1680 panic!("Expected Scan command");
1681 }
1682 }
1683
1684 #[test]
1685 fn test_doctor_check_category() {
1686 let cli =
1687 Cli::try_parse_from(["forge-guard", "doctor", "--check", "dependencies"]).unwrap();
1688 if let Commands::Doctor(args) = cli.command {
1689 assert_eq!(args.check.as_deref(), Some("dependencies"));
1690 } else {
1691 panic!("Expected Doctor command");
1692 }
1693 }
1694
1695 #[test]
1696 fn test_gas_contract_specific() {
1697 let cli =
1698 Cli::try_parse_from(["forge-guard", "gas", "Vault", "--diff", "prev.json"]).unwrap();
1699 if let Commands::Gas(args) = cli.command {
1700 assert_eq!(args.contract.as_deref(), Some("Vault"));
1701 assert_eq!(args.diff.as_deref(), Some("prev.json"));
1702 } else {
1703 panic!("Expected Gas command");
1704 }
1705 }
1706
1707 #[test]
1708 fn test_verify_with_constructor_args() {
1709 let cli = Cli::try_parse_from([
1710 "forge-guard",
1711 "verify",
1712 "0xabc",
1713 "Token",
1714 "--constructor-args",
1715 "0x0001",
1716 ])
1717 .unwrap();
1718 if let Commands::Verify(args) = cli.command {
1719 assert_eq!(args.constructor_args.as_deref(), Some("0x0001"));
1720 } else {
1721 panic!("Expected Verify command");
1722 }
1723 }
1724
1725 #[test]
1726 fn test_upgrade_check_proxy() {
1727 let cli =
1728 Cli::try_parse_from(["forge-guard", "upgrade-check", "0xproxy", "0ximpl"]).unwrap();
1729 if let Commands::UpgradeCheck(args) = cli.command {
1730 assert_eq!(args.proxy.as_deref(), Some("0xproxy"));
1731 assert_eq!(args.implementation.as_deref(), Some("0ximpl"));
1732 } else {
1733 panic!("Expected UpgradeCheck command");
1734 }
1735 }
1736
1737 #[test]
1738 fn test_benchmark_save_and_compare() {
1739 let cli = Cli::try_parse_from([
1740 "forge-guard",
1741 "benchmark",
1742 "--save",
1743 "results.json",
1744 "--compare",
1745 "baseline.json",
1746 ])
1747 .unwrap();
1748 if let Commands::Benchmark(args) = cli.command {
1749 assert_eq!(
1750 args.save.as_deref(),
1751 Some(std::path::Path::new("results.json"))
1752 );
1753 assert_eq!(
1754 args.compare.as_deref(),
1755 Some(std::path::Path::new("baseline.json"))
1756 );
1757 } else {
1758 panic!("Expected Benchmark command");
1759 }
1760 }
1761
1762 #[test]
1763 fn test_watch_exclude() {
1764 let cli = Cli::try_parse_from(["forge-guard", "watch", "--exclude", "*.test.sol"]).unwrap();
1765 if let Commands::Watch(args) = cli.command {
1766 assert_eq!(args.exclude.as_deref(), Some("*.test.sol"));
1767 } else {
1768 panic!("Expected Watch command");
1769 }
1770 }
1771
1772 #[test]
1773 fn test_simulate_deployer() {
1774 let cli =
1775 Cli::try_parse_from(["forge-guard", "simulate", "--deployer", "0xdeployer"]).unwrap();
1776 if let Commands::Simulate(args) = cli.command {
1777 assert_eq!(args.deployer.as_deref(), Some("0xdeployer"));
1778 } else {
1779 panic!("Expected Simulate command");
1780 }
1781 }
1782
1783 #[test]
1784 fn test_security_disable_check() {
1785 let cli = Cli::try_parse_from(["forge-guard", "security", "disable", "FA-H-001"]).unwrap();
1786 if let Commands::Security(args) = cli.command {
1787 assert!(matches!(args.action, Some(SecurityAction::Disable { .. })));
1788 } else {
1789 panic!("Expected Security command");
1790 }
1791 }
1792
1793 #[test]
1794 fn test_from_env_try_parse() {
1795 let cli = Cli::try_parse_from(["forge-guard", "audit", "--report", "--json"]).unwrap();
1798 assert!(matches!(cli.command, Commands::Audit(_)));
1799 if let Commands::Audit(args) = cli.command {
1800 assert!(args.shared.report);
1801 assert!(args.shared.json);
1802 }
1803 }
1804
1805 #[test]
1806 fn test_cli_parse_import() {
1807 let cli = Cli::try_parse_from(["forge-guard", "import", "results.json"]).unwrap();
1808 assert!(matches!(cli.command, Commands::Import(_)));
1809 }
1810
1811 #[test]
1812 fn test_import_args_from_slither() {
1813 let cli = Cli::try_parse_from([
1814 "forge-guard",
1815 "import",
1816 "--from",
1817 "mythril",
1818 "mythril_out.json",
1819 "--findings",
1820 "reports/audit.json",
1821 ])
1822 .unwrap();
1823 if let Commands::Import(args) = cli.command {
1824 assert_eq!(args.from, "mythril");
1825 assert_eq!(
1826 args.input.as_deref(),
1827 Some(std::path::Path::new("mythril_out.json"))
1828 );
1829 assert_eq!(
1830 args.findings.as_deref(),
1831 Some(std::path::Path::new("reports/audit.json"))
1832 );
1833 } else {
1834 panic!("Expected Import command");
1835 }
1836 }
1837
1838 #[test]
1839 fn test_import_args_defaults() {
1840 let cli = Cli::try_parse_from(["forge-guard", "import"]).unwrap();
1841 if let Commands::Import(args) = cli.command {
1842 assert_eq!(args.from, "slither");
1843 assert!(args.input.is_none());
1844 assert!(args.findings.is_none());
1845 assert!(args.output.is_none());
1846 } else {
1847 panic!("Expected Import command");
1848 }
1849 }
1850
1851 #[test]
1852 fn test_cli_parse_notify() {
1853 let cli = Cli::try_parse_from(["forge-guard", "notify", "--dry-run"]).unwrap();
1854 assert!(matches!(cli.command, Commands::Notify(_)));
1855 }
1856
1857 #[test]
1858 fn test_notify_args() {
1859 let cli = Cli::try_parse_from([
1860 "forge-guard",
1861 "notify",
1862 "--webhook",
1863 "https://hooks.slack.com/services/T/B/X",
1864 "--findings",
1865 "reports/audit.json",
1866 "--on-critical",
1867 "--title",
1868 "Nightly audit",
1869 ])
1870 .unwrap();
1871 if let Commands::Notify(args) = cli.command {
1872 assert_eq!(
1873 args.webhook.as_deref(),
1874 Some("https://hooks.slack.com/services/T/B/X")
1875 );
1876 assert_eq!(
1877 args.findings.as_deref(),
1878 Some(std::path::Path::new("reports/audit.json"))
1879 );
1880 assert!(args.on_critical);
1881 assert!(!args.on_high);
1882 assert!(!args.dry_run);
1883 assert_eq!(args.title, "Nightly audit");
1884 } else {
1885 panic!("Expected Notify command");
1886 }
1887 }
1888
1889 #[test]
1890 fn test_notify_defaults() {
1891 let cli = Cli::try_parse_from(["forge-guard", "notify"]).unwrap();
1892 if let Commands::Notify(args) = cli.command {
1893 assert_eq!(args.title, "Forge Guard Audit");
1894 assert!(args.webhook.is_none());
1895 assert!(args.kind.is_none());
1896 assert!(args.severity.is_none());
1897 assert!(!args.dry_run);
1898 } else {
1899 panic!("Expected Notify command");
1900 }
1901 }
1902
1903 #[test]
1904 fn test_audit_suppression_flags() {
1905 let cli = Cli::try_parse_from([
1906 "forge-guard",
1907 "audit",
1908 "--suppressions",
1909 ".forge-guard-suppressions",
1910 "--show-suppressed",
1911 ])
1912 .unwrap();
1913 if let Commands::Audit(args) = cli.command {
1914 assert_eq!(
1915 args.suppressions.as_deref(),
1916 Some(std::path::Path::new(".forge-guard-suppressions"))
1917 );
1918 assert!(args.show_suppressed);
1919 assert!(!args.generate_suppressions);
1920 assert!(!args.notify);
1921 } else {
1922 panic!("Expected Audit command");
1923 }
1924 }
1925
1926 #[test]
1927 fn test_audit_generate_suppressions_and_notify() {
1928 let cli = Cli::try_parse_from([
1929 "forge-guard",
1930 "audit",
1931 "--generate-suppressions",
1932 "--notify",
1933 ])
1934 .unwrap();
1935 if let Commands::Audit(args) = cli.command {
1936 assert!(args.generate_suppressions);
1937 assert!(args.notify);
1938 assert!(args.suppressions.is_none());
1939 } else {
1940 panic!("Expected Audit command");
1941 }
1942 }
1943
1944 #[test]
1945 fn test_deploy_notify_flag() {
1946 let cli = Cli::try_parse_from(["forge-guard", "deploy", "--notify"]).unwrap();
1947 if let Commands::Deploy(args) = cli.command {
1948 assert!(args.notify);
1949 } else {
1950 panic!("Expected Deploy command");
1951 }
1952 }
1953
1954 #[test]
1955 fn test_deploy_safe_notify_flag() {
1956 let cli = Cli::try_parse_from(["forge-guard", "deploy-safe", "--notify"]).unwrap();
1957 if let Commands::DeploySafe(args) = cli.command {
1958 assert!(args.notify);
1959 } else {
1960 panic!("Expected DeploySafe command");
1961 }
1962 }
1963}