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 vscode: bool,
577
578 #[arg(long)]
580 pub overwrite: bool,
581}
582
583#[derive(Debug, clap::Args)]
584pub struct BenchmarkArgs {
585 #[command(flatten)]
586 pub shared: SharedFlags,
587
588 #[arg(long, default_value = "10")]
590 pub iterations: u32,
591
592 #[arg(long)]
594 pub compare: Option<PathBuf>,
595
596 #[arg(long)]
598 pub save: Option<PathBuf>,
599
600 #[arg(long)]
602 pub module: Option<String>,
603
604 #[arg(long, default_value = "3")]
606 pub warmup: u32,
607}
608
609#[derive(Debug, clap::Args)]
610pub struct SbomArgs {
611 #[command(flatten)]
612 pub shared: SharedFlags,
613
614 #[arg(long, default_value = "cyclonedx")]
616 pub format: String,
617
618 #[arg(long, short)]
620 pub output: Option<PathBuf>,
621
622 #[arg(long)]
624 pub ci: bool,
625}
626
627#[derive(Debug, clap::Args)]
628pub struct ScanArgs {
629 #[command(flatten)]
630 pub shared: SharedFlags,
631
632 #[arg(long, default_value = "1")]
634 pub depth: u32,
635
636 #[arg(long)]
638 pub update: bool,
639
640 #[arg(long)]
642 pub vulnerable_only: bool,
643
644 #[arg(long)]
646 pub fail_fast: bool,
647}
648
649#[derive(Debug, clap::Args)]
650pub struct UpgradeCheckArgs {
651 #[command(flatten)]
652 pub shared: SharedFlags,
653
654 pub proxy: Option<String>,
656
657 pub implementation: Option<String>,
659
660 #[arg(long)]
662 pub all: bool,
663
664 #[arg(long)]
666 pub storage_collision: bool,
667
668 #[arg(long)]
670 pub uups: bool,
671}
672
673#[derive(Debug, clap::Args)]
674pub struct PluginArgs {
675 #[command(flatten)]
676 pub shared: SharedFlags,
677
678 #[command(subcommand)]
680 pub action: Option<PluginAction>,
681}
682
683#[derive(Debug, Subcommand)]
684pub enum PluginAction {
685 List,
687 Install {
689 name: String,
690 source: Option<String>,
691 },
692 Remove { name: String },
694 Enable { name: String },
696 Disable { name: String },
698 New { name: String },
700}
701
702#[derive(Debug, clap::Args)]
703pub struct ChainArgs {
704 #[command(flatten)]
705 pub shared: SharedFlags,
706
707 #[command(subcommand)]
709 pub action: Option<ChainAction>,
710}
711
712#[derive(Debug, Subcommand)]
713pub enum ChainAction {
714 List,
716 Info { chain: String },
718 Add {
720 name: String,
721 rpc_url: Option<String>,
722 chain_id: Option<u64>,
723 },
724 Remove { name: String },
726 Test {
728 chain: String,
729 rpc_url: Option<String>,
730 },
731}
732
733#[derive(Debug, clap::Args)]
734pub struct SecurityArgs {
735 #[command(flatten)]
736 pub shared: SharedFlags,
737
738 #[command(subcommand)]
740 pub action: Option<SecurityAction>,
741}
742
743#[derive(Debug, Subcommand)]
744pub enum SecurityAction {
745 Config,
747 Threshold { score: u8 },
749 Enable { check: String },
751 Disable { check: String },
753 List,
755 Info { check: String },
757}
758
759#[derive(Debug, clap::Args)]
762pub struct ImportArgs {
763 #[command(flatten)]
764 pub shared: SharedFlags,
765
766 #[arg(long, default_value = "slither")]
768 pub from: String,
769
770 pub input: Option<PathBuf>,
772
773 #[arg(long)]
775 pub findings: Option<PathBuf>,
776
777 #[arg(long, short)]
779 pub output: Option<PathBuf>,
780}
781
782#[derive(Debug, clap::Args)]
783pub struct NotifyArgs {
784 #[command(flatten)]
785 pub shared: SharedFlags,
786
787 #[arg(long)]
789 pub webhook: Option<String>,
790
791 #[arg(long)]
793 pub kind: Option<String>,
794
795 #[arg(long)]
797 pub findings: Option<PathBuf>,
798
799 #[arg(long, default_value = "Forge Guard Audit")]
801 pub title: String,
802
803 #[arg(long)]
805 pub message: Option<String>,
806
807 #[arg(long)]
809 pub severity: Option<String>,
810
811 #[arg(long)]
813 pub on_critical: bool,
814
815 #[arg(long)]
817 pub on_high: bool,
818
819 #[arg(long)]
821 pub dry_run: bool,
822}
823
824#[cfg(test)]
825mod tests {
826 use super::*;
827 use clap::Parser;
828
829 #[test]
830 fn test_cli_parse_audit() {
831 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
832 assert!(matches!(cli.command, Commands::Audit(_)));
833 }
834
835 #[test]
836 fn test_cli_parse_deploy() {
837 let cli = Cli::try_parse_from(["forge-guard", "deploy"]).unwrap();
838 assert!(matches!(cli.command, Commands::Deploy(_)));
839 }
840
841 #[test]
842 fn test_cli_parse_deploy_safe() {
843 let cli = Cli::try_parse_from(["forge-guard", "deploy-safe"]).unwrap();
844 assert!(matches!(cli.command, Commands::DeploySafe(_)));
845 }
846
847 #[test]
848 fn test_cli_parse_fuzz() {
849 let cli = Cli::try_parse_from(["forge-guard", "fuzz"]).unwrap();
850 assert!(matches!(cli.command, Commands::Fuzz(_)));
851 }
852
853 #[test]
854 fn test_cli_parse_invariant() {
855 let cli = Cli::try_parse_from(["forge-guard", "invariant"]).unwrap();
856 assert!(matches!(cli.command, Commands::Invariant(_)));
857 }
858
859 #[test]
860 fn test_cli_parse_simulate() {
861 let cli = Cli::try_parse_from(["forge-guard", "simulate"]).unwrap();
862 assert!(matches!(cli.command, Commands::Simulate(_)));
863 }
864
865 #[test]
866 fn test_cli_parse_gas() {
867 let cli = Cli::try_parse_from(["forge-guard", "gas"]).unwrap();
868 assert!(matches!(cli.command, Commands::Gas(_)));
869 }
870
871 #[test]
872 fn test_cli_parse_report() {
873 let cli = Cli::try_parse_from(["forge-guard", "report"]).unwrap();
874 assert!(matches!(cli.command, Commands::Report(_)));
875 }
876
877 #[test]
878 fn test_cli_parse_verify() {
879 let cli = Cli::try_parse_from(["forge-guard", "verify"]).unwrap();
880 assert!(matches!(cli.command, Commands::Verify(_)));
881 }
882
883 #[test]
884 fn test_cli_parse_doctor() {
885 let cli = Cli::try_parse_from(["forge-guard", "doctor"]).unwrap();
886 assert!(matches!(cli.command, Commands::Doctor(_)));
887 }
888
889 #[test]
890 fn test_cli_parse_watch() {
891 let cli = Cli::try_parse_from(["forge-guard", "watch"]).unwrap();
892 assert!(matches!(cli.command, Commands::Watch(_)));
893 }
894
895 #[test]
896 fn test_cli_parse_dashboard() {
897 let cli = Cli::try_parse_from(["forge-guard", "dashboard"]).unwrap();
898 assert!(matches!(cli.command, Commands::Dashboard(_)));
899 }
900
901 #[test]
902 fn test_cli_parse_dashboard_with_flags() {
903 let cli = Cli::try_parse_from([
904 "forge-guard",
905 "dashboard",
906 "--port",
907 "8080",
908 "--host",
909 "0.0.0.0",
910 "--watch",
911 "--dirs",
912 "src,contracts",
913 "--debounce-ms",
914 "250",
915 "--open",
916 ])
917 .unwrap();
918 if let Commands::Dashboard(args) = cli.command {
919 assert_eq!(args.port, 8080);
920 assert_eq!(args.host, "0.0.0.0");
921 assert!(args.watch);
922 assert_eq!(args.dirs, "src,contracts");
923 assert_eq!(args.debounce_ms, 250);
924 assert!(args.open);
925 } else {
926 panic!("expected Dashboard command");
927 }
928 }
929
930 #[test]
931 fn test_cli_parse_dashboard_defaults() {
932 let cli = Cli::try_parse_from(["forge-guard", "dashboard"]).unwrap();
933 if let Commands::Dashboard(args) = cli.command {
934 assert_eq!(args.port, 9090, "default port should be 9090");
935 assert_eq!(args.host, "127.0.0.1", "default host should be 127.0.0.1");
936 assert!(!args.watch);
937 assert_eq!(args.dirs, "src");
938 assert_eq!(args.debounce_ms, 500);
939 assert!(!args.open);
940 assert!(!args.full);
941 } else {
942 panic!("expected Dashboard command");
943 }
944 }
945
946 #[test]
947 fn test_cli_parse_ci() {
948 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
949 assert!(matches!(cli.command, Commands::Ci(_)));
950 }
951
952 #[test]
953 fn test_cli_parse_benchmark() {
954 let cli = Cli::try_parse_from(["forge-guard", "benchmark"]).unwrap();
955 assert!(matches!(cli.command, Commands::Benchmark(_)));
956 }
957
958 #[test]
959 fn test_cli_parse_scan() {
960 let cli = Cli::try_parse_from(["forge-guard", "scan"]).unwrap();
961 assert!(matches!(cli.command, Commands::Scan(_)));
962 }
963
964 #[test]
965 fn test_cli_parse_upgrade_check() {
966 let cli = Cli::try_parse_from(["forge-guard", "upgrade-check"]).unwrap();
967 assert!(matches!(cli.command, Commands::UpgradeCheck(_)));
968 }
969
970 #[test]
971 fn test_cli_parse_plugins() {
972 let cli = Cli::try_parse_from(["forge-guard", "plugins"]).unwrap();
973 assert!(matches!(cli.command, Commands::Plugins(_)));
974 }
975
976 #[test]
977 fn test_cli_parse_chain() {
978 let cli = Cli::try_parse_from(["forge-guard", "chain"]).unwrap();
979 assert!(matches!(cli.command, Commands::Chain(_)));
980 }
981
982 #[test]
983 fn test_cli_parse_security() {
984 let cli = Cli::try_parse_from(["forge-guard", "security"]).unwrap();
985 assert!(matches!(cli.command, Commands::Security(_)));
986 }
987
988 #[test]
989 fn test_cli_parse_install_hook() {
990 let cli = Cli::try_parse_from(["forge-guard", "install-hook"]).unwrap();
991 assert!(matches!(cli.command, Commands::InstallHook(_)));
992 }
993
994 #[test]
995 fn test_cli_parse_install_hook_uninstall() {
996 let cli = Cli::try_parse_from(["forge-guard", "install-hook", "--uninstall"]).unwrap();
997 assert!(matches!(cli.command, Commands::InstallHook(_)));
998 }
999
1000 #[test]
1001 fn test_cli_parse_doctor_sync() {
1002 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--sync"]).unwrap();
1003 if let Commands::Doctor(args) = cli.command {
1004 assert!(args.sync);
1005 } else {
1006 panic!("Expected Doctor command");
1007 }
1008 }
1009
1010 #[test]
1011 fn test_cli_parse_doctor_dry_run() {
1012 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--sync", "--dry-run"]).unwrap();
1013 if let Commands::Doctor(args) = cli.command {
1014 assert!(args.sync);
1015 assert!(args.dry_run);
1016 } else {
1017 panic!("Expected Doctor command");
1018 }
1019 }
1020
1021 #[test]
1022 fn test_cli_parse_audit_template() {
1023 let cli = Cli::try_parse_from(["forge-guard", "audit", "--template", "defi"]).unwrap();
1024 if let Commands::Audit(args) = cli.command {
1025 assert_eq!(args.template.as_deref(), Some("defi"));
1026 } else {
1027 panic!("Expected Audit command");
1028 }
1029 }
1030
1031 #[test]
1032 fn test_cli_parse_audit_list_templates() {
1033 let cli = Cli::try_parse_from(["forge-guard", "audit", "--list-templates"]).unwrap();
1034 if let Commands::Audit(args) = cli.command {
1035 assert!(args.list_templates);
1036 } else {
1037 panic!("Expected Audit command");
1038 }
1039 }
1040
1041 #[test]
1042 fn test_shared_flags_defaults() {
1043 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
1044 if let Commands::Audit(args) = cli.command {
1045 assert_eq!(args.shared.chain, "ethereum");
1046 assert!(!args.shared.json);
1047 assert!(!args.shared.markdown);
1048 assert!(!args.shared.strict);
1049 assert!(!args.shared.offline);
1050 assert!(!args.shared.production);
1051 assert!(!args.shared.report);
1052 assert_eq!(args.shared.parallelism, 4);
1053 } else {
1054 panic!("Expected Audit command");
1055 }
1056 }
1057
1058 #[test]
1059 fn test_shared_flags_custom() {
1060 let cli = Cli::try_parse_from([
1061 "forge-guard",
1062 "audit",
1063 "--chain",
1064 "base",
1065 "--json",
1066 "--strict",
1067 "--offline",
1068 "--production",
1069 "--parallelism",
1070 "8",
1071 "--project",
1072 "/tmp/project",
1073 ])
1074 .unwrap();
1075 if let Commands::Audit(args) = cli.command {
1076 assert_eq!(args.shared.chain, "base");
1077 assert!(args.shared.json);
1078 assert!(args.shared.strict);
1079 assert!(args.shared.offline);
1080 assert!(args.shared.production);
1081 assert_eq!(args.shared.parallelism, 8);
1082 assert_eq!(
1083 args.shared.project,
1084 std::path::PathBuf::from("/tmp/project")
1085 );
1086 } else {
1087 panic!("Expected Audit command");
1088 }
1089 }
1090
1091 #[test]
1092 fn test_audit_args_defaults() {
1093 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
1094 if let Commands::Audit(args) = cli.command {
1095 assert!(!args.full);
1096 assert!(!args.quick);
1097 assert!(!args.summary);
1098 assert!(!args.ai);
1099 assert_eq!(args.ai_provider, "openai");
1100 assert_eq!(args.ai_model, "gpt-5");
1101 assert!(args.ai_api_key.is_none());
1102 assert!(!args.exploit);
1103 assert!(!args.gas);
1104 assert!(!args.all_chains);
1105 assert_eq!(args.sources, "src");
1106 assert!(args.exclude.is_none());
1107 } else {
1108 panic!("Expected Audit command");
1109 }
1110 }
1111
1112 #[test]
1113 fn test_audit_args_full() {
1114 let cli = Cli::try_parse_from([
1115 "forge-guard",
1116 "audit",
1117 "--full",
1118 "--chain",
1119 "arbitrum",
1120 "--json",
1121 "--report",
1122 "--exploit",
1123 "--gas",
1124 ])
1125 .unwrap();
1126 if let Commands::Audit(args) = cli.command {
1127 assert!(args.full);
1128 assert!(args.exploit);
1129 assert!(args.gas);
1130 assert!(args.shared.json);
1131 assert!(args.shared.report);
1132 assert_eq!(args.shared.chain, "arbitrum");
1133 } else {
1134 panic!("Expected Audit command");
1135 }
1136 }
1137
1138 #[test]
1139 fn test_audit_ai_args() {
1140 let cli = Cli::try_parse_from([
1141 "forge-guard",
1142 "audit",
1143 "--ai",
1144 "--ai-provider",
1145 "claude",
1146 "--ai-model",
1147 "claude-5-sonnet-20260701",
1148 "--ai-full",
1149 ])
1150 .unwrap();
1151 if let Commands::Audit(args) = cli.command {
1152 assert!(args.ai);
1153 assert_eq!(args.ai_provider, "claude");
1154 assert_eq!(args.ai_model, "claude-5-sonnet-20260701");
1155 assert!(args.ai_full);
1156 } else {
1157 panic!("Expected Audit command");
1158 }
1159 }
1160
1161 #[test]
1162 fn test_deploy_args() {
1163 let cli = Cli::try_parse_from([
1164 "forge-guard",
1165 "deploy",
1166 "MyContract",
1167 "--force",
1168 "--salt",
1169 "0xabc",
1170 "--verify",
1171 "--args",
1172 "arg1,arg2",
1173 ])
1174 .unwrap();
1175 if let Commands::Deploy(args) = cli.command {
1176 assert_eq!(args.contract.as_deref(), Some("MyContract"));
1177 assert!(args.force);
1178 assert_eq!(args.salt.as_deref(), Some("0xabc"));
1179 assert!(args.verify);
1180 assert_eq!(args.args.as_deref(), Some("arg1,arg2"));
1181 } else {
1182 panic!("Expected Deploy command");
1183 }
1184 }
1185
1186 #[test]
1187 fn test_fuzz_args() {
1188 let cli = Cli::try_parse_from([
1189 "forge-guard",
1190 "fuzz",
1191 "--runs",
1192 "50000",
1193 "--seed",
1194 "42",
1195 "--test",
1196 "testFuzzDeposit",
1197 "Vault",
1198 ])
1199 .unwrap();
1200 if let Commands::Fuzz(args) = cli.command {
1201 assert_eq!(args.runs, 50_000);
1202 assert_eq!(args.seed, Some(42));
1203 assert_eq!(args.test.as_deref(), Some("testFuzzDeposit"));
1204 assert_eq!(args.contract.as_deref(), Some("Vault"));
1205 } else {
1206 panic!("Expected Fuzz command");
1207 }
1208 }
1209
1210 #[test]
1211 fn test_simulate_args() {
1212 let cli = Cli::try_parse_from([
1213 "forge-guard",
1214 "simulate",
1215 "MyContract",
1216 "--blocks",
1217 "200",
1218 "--mev",
1219 ])
1220 .unwrap();
1221 if let Commands::Simulate(args) = cli.command {
1222 assert_eq!(args.contract.as_deref(), Some("MyContract"));
1223 assert_eq!(args.blocks, 200);
1224 assert!(args.mev);
1225 } else {
1226 panic!("Expected Simulate command");
1227 }
1228 }
1229
1230 #[test]
1231 fn test_invariant_args() {
1232 let cli = Cli::try_parse_from([
1233 "forge-guard",
1234 "invariant",
1235 "--runs",
1236 "2000",
1237 "--depth",
1238 "150",
1239 "--fail-on-revert",
1240 ])
1241 .unwrap();
1242 if let Commands::Invariant(args) = cli.command {
1243 assert_eq!(args.runs, 2000);
1244 assert_eq!(args.depth, 150);
1245 assert!(args.fail_on_revert);
1246 } else {
1247 panic!("Expected Invariant command");
1248 }
1249 }
1250
1251 #[test]
1252 fn test_ci_args_defaults() {
1253 let cli = Cli::try_parse_from(["forge-guard", "ci"]).unwrap();
1254 if let Commands::Ci(args) = cli.command {
1255 assert_eq!(args.platform, "github");
1256 assert_eq!(args.output, std::path::PathBuf::from(".github/workflows"));
1257 assert!(!args.include_deploy);
1258 assert!(!args.vscode);
1259 assert!(!args.overwrite);
1260 } else {
1261 panic!("Expected Ci command");
1262 }
1263 }
1264
1265 #[test]
1266 fn test_ci_args_custom() {
1267 let cli = Cli::try_parse_from([
1268 "forge-guard",
1269 "ci",
1270 "--platform",
1271 "gitlab",
1272 "--include-deploy",
1273 "--overwrite",
1274 "--output",
1275 ".gitlab",
1276 ])
1277 .unwrap();
1278 if let Commands::Ci(args) = cli.command {
1279 assert_eq!(args.platform, "gitlab");
1280 assert!(args.include_deploy);
1281 assert!(args.overwrite);
1282 assert_eq!(args.output, std::path::PathBuf::from(".gitlab"));
1283 } else {
1284 panic!("Expected Ci command");
1285 }
1286 }
1287
1288 #[test]
1289 fn test_ci_args_vscode() {
1290 let cli = Cli::try_parse_from(["forge-guard", "ci", "--vscode"]).unwrap();
1291 if let Commands::Ci(args) = cli.command {
1292 assert!(args.vscode);
1293 } else {
1294 panic!("Expected Ci command");
1295 }
1296 }
1297
1298 #[test]
1299 fn test_benchmark_args() {
1300 let cli = Cli::try_parse_from([
1301 "forge-guard",
1302 "benchmark",
1303 "--iterations",
1304 "50",
1305 "--warmup",
1306 "5",
1307 "--module",
1308 "pattern_matching",
1309 ])
1310 .unwrap();
1311 if let Commands::Benchmark(args) = cli.command {
1312 assert_eq!(args.iterations, 50);
1313 assert_eq!(args.warmup, 5);
1314 assert_eq!(args.module.as_deref(), Some("pattern_matching"));
1315 } else {
1316 panic!("Expected Benchmark command");
1317 }
1318 }
1319
1320 #[test]
1321 fn test_gas_args() {
1322 let cli =
1323 Cli::try_parse_from(["forge-guard", "gas", "--all", "--warn-threshold", "100000"])
1324 .unwrap();
1325 if let Commands::Gas(args) = cli.command {
1326 assert!(args.all);
1327 assert_eq!(args.warn_threshold, 100000);
1328 assert!(args.contract.is_none());
1329 } else {
1330 panic!("Expected Gas command");
1331 }
1332 }
1333
1334 #[test]
1335 fn test_scan_args() {
1336 let cli = Cli::try_parse_from([
1337 "forge-guard",
1338 "scan",
1339 "--depth",
1340 "2",
1341 "--update",
1342 "--vulnerable-only",
1343 "--fail-fast",
1344 ])
1345 .unwrap();
1346 if let Commands::Scan(args) = cli.command {
1347 assert_eq!(args.depth, 2);
1348 assert!(args.update);
1349 assert!(args.vulnerable_only);
1350 assert!(args.fail_fast);
1351 } else {
1352 panic!("Expected Scan command");
1353 }
1354 }
1355
1356 #[test]
1357 fn test_upgrade_check_args() {
1358 let cli = Cli::try_parse_from([
1359 "forge-guard",
1360 "upgrade-check",
1361 "--storage-collision",
1362 "--uups",
1363 "--all",
1364 ])
1365 .unwrap();
1366 if let Commands::UpgradeCheck(args) = cli.command {
1367 assert!(args.storage_collision);
1368 assert!(args.uups);
1369 assert!(args.all);
1370 } else {
1371 panic!("Expected UpgradeCheck command");
1372 }
1373 }
1374
1375 #[test]
1376 fn test_doctor_args() {
1377 let cli = Cli::try_parse_from(["forge-guard", "doctor", "--fix", "--verbose"]).unwrap();
1378 if let Commands::Doctor(args) = cli.command {
1379 assert!(args.fix);
1380 assert!(args.verbose);
1381 assert!(args.check.is_none());
1382 } else {
1383 panic!("Expected Doctor command");
1384 }
1385 }
1386
1387 #[test]
1388 fn test_verify_args() {
1389 let cli = Cli::try_parse_from([
1390 "forge-guard",
1391 "verify",
1392 "0x1234",
1393 "MyContract",
1394 "--api-key",
1395 "test-key",
1396 "--all",
1397 ])
1398 .unwrap();
1399 if let Commands::Verify(args) = cli.command {
1400 assert_eq!(args.address.as_deref(), Some("0x1234"));
1401 assert_eq!(args.name.as_deref(), Some("MyContract"));
1402 assert_eq!(args.api_key.as_deref(), Some("test-key"));
1403 assert!(args.all);
1404 } else {
1405 panic!("Expected Verify command");
1406 }
1407 }
1408
1409 #[test]
1410 fn test_watch_args() {
1411 let cli = Cli::try_parse_from([
1412 "forge-guard",
1413 "watch",
1414 "--dirs",
1415 "src,test-contracts",
1416 "--debounce-ms",
1417 "1000",
1418 "--full",
1419 ])
1420 .unwrap();
1421 if let Commands::Watch(args) = cli.command {
1422 assert_eq!(args.dirs, "src,test-contracts");
1423 assert_eq!(args.debounce_ms, 1000);
1424 assert!(args.full);
1425 } else {
1426 panic!("Expected Watch command");
1427 }
1428 }
1429
1430 #[test]
1431 fn test_plugins_list() {
1432 let cli = Cli::try_parse_from(["forge-guard", "plugins", "list"]).unwrap();
1433 if let Commands::Plugins(args) = cli.command {
1434 assert!(matches!(args.action, Some(PluginAction::List)));
1435 } else {
1436 panic!("Expected Plugins command");
1437 }
1438 }
1439
1440 #[test]
1441 fn test_plugins_install() {
1442 let cli = Cli::try_parse_from(["forge-guard", "plugins", "install", "my-plugin"]).unwrap();
1443 if let Commands::Plugins(args) = cli.command {
1444 assert!(matches!(args.action, Some(PluginAction::Install { .. })));
1445 } else {
1446 panic!("Expected Plugins command");
1447 }
1448 }
1449
1450 #[test]
1451 fn test_plugins_remove() {
1452 let cli = Cli::try_parse_from(["forge-guard", "plugins", "remove", "bad-plugin"]).unwrap();
1453 if let Commands::Plugins(args) = cli.command {
1454 assert!(matches!(args.action, Some(PluginAction::Remove { .. })));
1455 } else {
1456 panic!("Expected Plugins command");
1457 }
1458 }
1459
1460 #[test]
1461 fn test_plugins_enable() {
1462 let cli = Cli::try_parse_from(["forge-guard", "plugins", "enable", "my-plugin"]).unwrap();
1463 if let Commands::Plugins(args) = cli.command {
1464 assert!(matches!(args.action, Some(PluginAction::Enable { .. })));
1465 } else {
1466 panic!("Expected Plugins command");
1467 }
1468 }
1469
1470 #[test]
1471 fn test_plugins_disable() {
1472 let cli = Cli::try_parse_from(["forge-guard", "plugins", "disable", "my-plugin"]).unwrap();
1473 if let Commands::Plugins(args) = cli.command {
1474 assert!(matches!(args.action, Some(PluginAction::Disable { .. })));
1475 } else {
1476 panic!("Expected Plugins command");
1477 }
1478 }
1479
1480 #[test]
1481 fn test_plugins_new() {
1482 let cli =
1483 Cli::try_parse_from(["forge-guard", "plugins", "new", "my-awesome-plugin"]).unwrap();
1484 if let Commands::Plugins(args) = cli.command {
1485 assert!(matches!(args.action, Some(PluginAction::New { .. })));
1486 } else {
1487 panic!("Expected Plugins command");
1488 }
1489 }
1490
1491 #[test]
1492 fn test_chain_list() {
1493 let cli = Cli::try_parse_from(["forge-guard", "chain", "list"]).unwrap();
1494 if let Commands::Chain(args) = cli.command {
1495 assert!(matches!(args.action, Some(ChainAction::List)));
1496 } else {
1497 panic!("Expected Chain command");
1498 }
1499 }
1500
1501 #[test]
1502 fn test_chain_info() {
1503 let cli = Cli::try_parse_from(["forge-guard", "chain", "info", "base"]).unwrap();
1504 if let Commands::Chain(args) = cli.command {
1505 assert!(matches!(args.action, Some(ChainAction::Info { .. })));
1506 } else {
1507 panic!("Expected Chain command");
1508 }
1509 }
1510
1511 #[test]
1512 fn test_chain_add() {
1513 let cli = Cli::try_parse_from([
1514 "forge-guard",
1515 "chain",
1516 "add",
1517 "my-chain",
1518 "https://rpc.my-chain.io",
1519 "99999",
1520 ])
1521 .unwrap();
1522 if let Commands::Chain(args) = cli.command {
1523 assert!(matches!(args.action, Some(ChainAction::Add { .. })));
1524 } else {
1525 panic!("Expected Chain command");
1526 }
1527 }
1528
1529 #[test]
1530 fn test_security_list() {
1531 let cli = Cli::try_parse_from(["forge-guard", "security", "list"]).unwrap();
1532 if let Commands::Security(args) = cli.command {
1533 assert!(matches!(args.action, Some(SecurityAction::List)));
1534 } else {
1535 panic!("Expected Security command");
1536 }
1537 }
1538
1539 #[test]
1540 fn test_security_threshold() {
1541 let cli = Cli::try_parse_from(["forge-guard", "security", "threshold", "85"]).unwrap();
1542 if let Commands::Security(args) = cli.command {
1543 assert!(matches!(
1544 args.action,
1545 Some(SecurityAction::Threshold { .. })
1546 ));
1547 } else {
1548 panic!("Expected Security command");
1549 }
1550 }
1551
1552 #[test]
1553 fn test_markdown_flag() {
1554 let cli = Cli::try_parse_from(["forge-guard", "audit", "--markdown", "--report"]).unwrap();
1555 if let Commands::Audit(args) = cli.command {
1556 assert!(args.shared.markdown);
1557 assert!(args.shared.report);
1558 } else {
1559 panic!("Expected Audit command");
1560 }
1561 }
1562
1563 #[test]
1564 fn test_quick_audit() {
1565 let cli = Cli::try_parse_from(["forge-guard", "audit", "--quick", "--summary"]).unwrap();
1566 if let Commands::Audit(args) = cli.command {
1567 assert!(args.quick);
1568 assert!(args.summary);
1569 } else {
1570 panic!("Expected Audit command");
1571 }
1572 }
1573
1574 #[test]
1575 fn test_enable_history_flag() {
1576 let cli = Cli::try_parse_from(["forge-guard", "audit", "--enable-history"]).unwrap();
1577 if let Commands::Audit(args) = cli.command {
1578 assert!(args.enable_history);
1579 } else {
1580 panic!("Expected Audit command");
1581 }
1582 }
1583
1584 #[test]
1585 fn test_report_history_flag() {
1586 let cli = Cli::try_parse_from(["forge-guard", "report", "--history"]).unwrap();
1587 if let Commands::Report(args) = cli.command {
1588 assert!(args.history);
1589 assert!(!args.regression);
1590 } else {
1591 panic!("Expected Report command");
1592 }
1593 }
1594
1595 #[test]
1596 fn test_report_regression_flag() {
1597 let cli = Cli::try_parse_from(["forge-guard", "report", "--regression"]).unwrap();
1598 if let Commands::Report(args) = cli.command {
1599 assert!(args.regression);
1600 assert!(!args.history);
1601 } else {
1602 panic!("Expected Report command");
1603 }
1604 }
1605
1606 #[test]
1607 fn test_all_chains_flag() {
1608 let cli = Cli::try_parse_from(["forge-guard", "audit", "--all-chains"]).unwrap();
1609 if let Commands::Audit(args) = cli.command {
1610 assert!(args.all_chains);
1611 assert_eq!(args.max_parallel_chains, 4);
1612 } else {
1613 panic!("Expected Audit command");
1614 }
1615 }
1616
1617 #[test]
1618 fn test_max_parallel_chains_default() {
1619 let cli = Cli::try_parse_from(["forge-guard", "audit"]).unwrap();
1620 if let Commands::Audit(args) = cli.command {
1621 assert_eq!(args.max_parallel_chains, 4);
1622 } else {
1623 panic!("Expected Audit command");
1624 }
1625 }
1626
1627 #[test]
1628 fn test_max_parallel_chains_custom() {
1629 let cli = Cli::try_parse_from([
1630 "forge-guard",
1631 "audit",
1632 "--all-chains",
1633 "--max-parallel-chains",
1634 "2",
1635 ])
1636 .unwrap();
1637 if let Commands::Audit(args) = cli.command {
1638 assert!(args.all_chains);
1639 assert_eq!(args.max_parallel_chains, 2);
1640 } else {
1641 panic!("Expected Audit command");
1642 }
1643 }
1644
1645 #[test]
1646 fn test_deploy_safe_args() {
1647 let cli =
1648 Cli::try_parse_from(["forge-guard", "deploy-safe", "SecureVault", "--verify"]).unwrap();
1649 if let Commands::DeploySafe(args) = cli.command {
1650 assert_eq!(args.contract.as_deref(), Some("SecureVault"));
1651 assert!(args.verify);
1652 } else {
1653 panic!("Expected DeploySafe command");
1654 }
1655 }
1656
1657 #[test]
1658 fn test_report_args_json() {
1659 let cli = Cli::try_parse_from(["forge-guard", "report", "--format", "json"]).unwrap();
1660 if let Commands::Report(args) = cli.command {
1661 assert_eq!(args.format, "json");
1662 assert!(!args.summary);
1663 } else {
1664 panic!("Expected Report command");
1665 }
1666 }
1667
1668 #[test]
1669 fn test_verify_all_without_address() {
1670 let cli = Cli::try_parse_from(["forge-guard", "verify", "--all"]).unwrap();
1671 if let Commands::Verify(args) = cli.command {
1672 assert!(args.all);
1673 assert!(args.address.is_none());
1674 } else {
1675 panic!("Expected Verify command");
1676 }
1677 }
1678
1679 #[test]
1680 fn test_report_summary() {
1681 let cli = Cli::try_parse_from(["forge-guard", "report", "--summary"]).unwrap();
1682 if let Commands::Report(args) = cli.command {
1683 assert!(args.summary);
1684 } else {
1685 panic!("Expected Report command");
1686 }
1687 }
1688
1689 #[test]
1690 fn test_scan_vulnerable_only() {
1691 let cli = Cli::try_parse_from(["forge-guard", "scan", "--vulnerable-only"]).unwrap();
1692 if let Commands::Scan(args) = cli.command {
1693 assert!(args.vulnerable_only);
1694 } else {
1695 panic!("Expected Scan command");
1696 }
1697 }
1698
1699 #[test]
1700 fn test_doctor_check_category() {
1701 let cli =
1702 Cli::try_parse_from(["forge-guard", "doctor", "--check", "dependencies"]).unwrap();
1703 if let Commands::Doctor(args) = cli.command {
1704 assert_eq!(args.check.as_deref(), Some("dependencies"));
1705 } else {
1706 panic!("Expected Doctor command");
1707 }
1708 }
1709
1710 #[test]
1711 fn test_gas_contract_specific() {
1712 let cli =
1713 Cli::try_parse_from(["forge-guard", "gas", "Vault", "--diff", "prev.json"]).unwrap();
1714 if let Commands::Gas(args) = cli.command {
1715 assert_eq!(args.contract.as_deref(), Some("Vault"));
1716 assert_eq!(args.diff.as_deref(), Some("prev.json"));
1717 } else {
1718 panic!("Expected Gas command");
1719 }
1720 }
1721
1722 #[test]
1723 fn test_verify_with_constructor_args() {
1724 let cli = Cli::try_parse_from([
1725 "forge-guard",
1726 "verify",
1727 "0xabc",
1728 "Token",
1729 "--constructor-args",
1730 "0x0001",
1731 ])
1732 .unwrap();
1733 if let Commands::Verify(args) = cli.command {
1734 assert_eq!(args.constructor_args.as_deref(), Some("0x0001"));
1735 } else {
1736 panic!("Expected Verify command");
1737 }
1738 }
1739
1740 #[test]
1741 fn test_upgrade_check_proxy() {
1742 let cli =
1743 Cli::try_parse_from(["forge-guard", "upgrade-check", "0xproxy", "0ximpl"]).unwrap();
1744 if let Commands::UpgradeCheck(args) = cli.command {
1745 assert_eq!(args.proxy.as_deref(), Some("0xproxy"));
1746 assert_eq!(args.implementation.as_deref(), Some("0ximpl"));
1747 } else {
1748 panic!("Expected UpgradeCheck command");
1749 }
1750 }
1751
1752 #[test]
1753 fn test_benchmark_save_and_compare() {
1754 let cli = Cli::try_parse_from([
1755 "forge-guard",
1756 "benchmark",
1757 "--save",
1758 "results.json",
1759 "--compare",
1760 "baseline.json",
1761 ])
1762 .unwrap();
1763 if let Commands::Benchmark(args) = cli.command {
1764 assert_eq!(
1765 args.save.as_deref(),
1766 Some(std::path::Path::new("results.json"))
1767 );
1768 assert_eq!(
1769 args.compare.as_deref(),
1770 Some(std::path::Path::new("baseline.json"))
1771 );
1772 } else {
1773 panic!("Expected Benchmark command");
1774 }
1775 }
1776
1777 #[test]
1778 fn test_watch_exclude() {
1779 let cli = Cli::try_parse_from(["forge-guard", "watch", "--exclude", "*.test.sol"]).unwrap();
1780 if let Commands::Watch(args) = cli.command {
1781 assert_eq!(args.exclude.as_deref(), Some("*.test.sol"));
1782 } else {
1783 panic!("Expected Watch command");
1784 }
1785 }
1786
1787 #[test]
1788 fn test_simulate_deployer() {
1789 let cli =
1790 Cli::try_parse_from(["forge-guard", "simulate", "--deployer", "0xdeployer"]).unwrap();
1791 if let Commands::Simulate(args) = cli.command {
1792 assert_eq!(args.deployer.as_deref(), Some("0xdeployer"));
1793 } else {
1794 panic!("Expected Simulate command");
1795 }
1796 }
1797
1798 #[test]
1799 fn test_security_disable_check() {
1800 let cli = Cli::try_parse_from(["forge-guard", "security", "disable", "FA-H-001"]).unwrap();
1801 if let Commands::Security(args) = cli.command {
1802 assert!(matches!(args.action, Some(SecurityAction::Disable { .. })));
1803 } else {
1804 panic!("Expected Security command");
1805 }
1806 }
1807
1808 #[test]
1809 fn test_from_env_try_parse() {
1810 let cli = Cli::try_parse_from(["forge-guard", "audit", "--report", "--json"]).unwrap();
1813 assert!(matches!(cli.command, Commands::Audit(_)));
1814 if let Commands::Audit(args) = cli.command {
1815 assert!(args.shared.report);
1816 assert!(args.shared.json);
1817 }
1818 }
1819
1820 #[test]
1821 fn test_cli_parse_import() {
1822 let cli = Cli::try_parse_from(["forge-guard", "import", "results.json"]).unwrap();
1823 assert!(matches!(cli.command, Commands::Import(_)));
1824 }
1825
1826 #[test]
1827 fn test_import_args_from_slither() {
1828 let cli = Cli::try_parse_from([
1829 "forge-guard",
1830 "import",
1831 "--from",
1832 "mythril",
1833 "mythril_out.json",
1834 "--findings",
1835 "reports/audit.json",
1836 ])
1837 .unwrap();
1838 if let Commands::Import(args) = cli.command {
1839 assert_eq!(args.from, "mythril");
1840 assert_eq!(
1841 args.input.as_deref(),
1842 Some(std::path::Path::new("mythril_out.json"))
1843 );
1844 assert_eq!(
1845 args.findings.as_deref(),
1846 Some(std::path::Path::new("reports/audit.json"))
1847 );
1848 } else {
1849 panic!("Expected Import command");
1850 }
1851 }
1852
1853 #[test]
1854 fn test_import_args_defaults() {
1855 let cli = Cli::try_parse_from(["forge-guard", "import"]).unwrap();
1856 if let Commands::Import(args) = cli.command {
1857 assert_eq!(args.from, "slither");
1858 assert!(args.input.is_none());
1859 assert!(args.findings.is_none());
1860 assert!(args.output.is_none());
1861 } else {
1862 panic!("Expected Import command");
1863 }
1864 }
1865
1866 #[test]
1867 fn test_cli_parse_notify() {
1868 let cli = Cli::try_parse_from(["forge-guard", "notify", "--dry-run"]).unwrap();
1869 assert!(matches!(cli.command, Commands::Notify(_)));
1870 }
1871
1872 #[test]
1873 fn test_notify_args() {
1874 let cli = Cli::try_parse_from([
1875 "forge-guard",
1876 "notify",
1877 "--webhook",
1878 "https://hooks.slack.com/services/T/B/X",
1879 "--findings",
1880 "reports/audit.json",
1881 "--on-critical",
1882 "--title",
1883 "Nightly audit",
1884 ])
1885 .unwrap();
1886 if let Commands::Notify(args) = cli.command {
1887 assert_eq!(
1888 args.webhook.as_deref(),
1889 Some("https://hooks.slack.com/services/T/B/X")
1890 );
1891 assert_eq!(
1892 args.findings.as_deref(),
1893 Some(std::path::Path::new("reports/audit.json"))
1894 );
1895 assert!(args.on_critical);
1896 assert!(!args.on_high);
1897 assert!(!args.dry_run);
1898 assert_eq!(args.title, "Nightly audit");
1899 } else {
1900 panic!("Expected Notify command");
1901 }
1902 }
1903
1904 #[test]
1905 fn test_notify_defaults() {
1906 let cli = Cli::try_parse_from(["forge-guard", "notify"]).unwrap();
1907 if let Commands::Notify(args) = cli.command {
1908 assert_eq!(args.title, "Forge Guard Audit");
1909 assert!(args.webhook.is_none());
1910 assert!(args.kind.is_none());
1911 assert!(args.severity.is_none());
1912 assert!(!args.dry_run);
1913 } else {
1914 panic!("Expected Notify command");
1915 }
1916 }
1917
1918 #[test]
1919 fn test_audit_suppression_flags() {
1920 let cli = Cli::try_parse_from([
1921 "forge-guard",
1922 "audit",
1923 "--suppressions",
1924 ".forge-guard-suppressions",
1925 "--show-suppressed",
1926 ])
1927 .unwrap();
1928 if let Commands::Audit(args) = cli.command {
1929 assert_eq!(
1930 args.suppressions.as_deref(),
1931 Some(std::path::Path::new(".forge-guard-suppressions"))
1932 );
1933 assert!(args.show_suppressed);
1934 assert!(!args.generate_suppressions);
1935 assert!(!args.notify);
1936 } else {
1937 panic!("Expected Audit command");
1938 }
1939 }
1940
1941 #[test]
1942 fn test_audit_generate_suppressions_and_notify() {
1943 let cli = Cli::try_parse_from([
1944 "forge-guard",
1945 "audit",
1946 "--generate-suppressions",
1947 "--notify",
1948 ])
1949 .unwrap();
1950 if let Commands::Audit(args) = cli.command {
1951 assert!(args.generate_suppressions);
1952 assert!(args.notify);
1953 assert!(args.suppressions.is_none());
1954 } else {
1955 panic!("Expected Audit command");
1956 }
1957 }
1958
1959 #[test]
1960 fn test_deploy_notify_flag() {
1961 let cli = Cli::try_parse_from(["forge-guard", "deploy", "--notify"]).unwrap();
1962 if let Commands::Deploy(args) = cli.command {
1963 assert!(args.notify);
1964 } else {
1965 panic!("Expected Deploy command");
1966 }
1967 }
1968
1969 #[test]
1970 fn test_deploy_safe_notify_flag() {
1971 let cli = Cli::try_parse_from(["forge-guard", "deploy-safe", "--notify"]).unwrap();
1972 if let Commands::DeploySafe(args) = cli.command {
1973 assert!(args.notify);
1974 } else {
1975 panic!("Expected DeploySafe command");
1976 }
1977 }
1978}