Skip to main content

forge_guard/cli/
mod.rs

1//! CLI argument parsing and command dispatch.
2
3mod 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/// Forge Guard — pre-deployment smart contract auditing for Foundry.
30#[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    /// Create a CLI instance from environment arguments.
45    pub fn from_env() -> Self {
46        Self::parse()
47    }
48
49    /// Run the selected command.
50    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/// All available subcommands.
82#[derive(Subcommand, Debug)]
83pub enum Commands {
84    /// Run a comprehensive security audit
85    Audit(AuditArgs),
86    /// Deploy contracts with automatic security checks
87    Deploy(DeployArgs),
88    /// Deploy with mandatory security pass requirement
89    #[command(name = "deploy-safe")]
90    DeploySafe(DeploySafeArgs),
91    /// Run fuzzing campaigns
92    Fuzz(FuzzArgs),
93    /// Run invariant tests
94    Invariant(InvariantArgs),
95    /// Run deployment simulations
96    Simulate(SimulateArgs),
97    /// Analyze gas usage
98    Gas(GasArgs),
99    /// Generate audit reports from existing results
100    Report(ReportArgs),
101    /// Verify contract deployments
102    Verify(VerifyArgs),
103    /// Analyze project health and configuration
104    Doctor(DoctorArgs),
105    /// Watch files for changes and re-audit
106    Watch(WatchArgs),
107    /// Generate CI/CD pipeline configurations
108    Ci(CiArgs),
109    /// Run performance benchmarks
110    Benchmark(BenchmarkArgs),
111    /// Scan dependencies for vulnerabilities
112    Scan(ScanArgs),
113    /// Analyze upgrade paths and proxy safety
114    #[command(name = "upgrade-check")]
115    UpgradeCheck(UpgradeCheckArgs),
116    /// Manage audit plugins
117    Plugins(PluginArgs),
118    /// Configure chain settings
119    Chain(ChainArgs),
120    /// Generate a Software Bill of Materials (SBOM) for the project
121    Sbom(SbomArgs),
122    /// Install or uninstall git pre-commit hook for Solidity auditing
123    #[command(name = "install-hook")]
124    InstallHook(install_hook::InstallHookArgs),
125
126    /// Configure security settings
127    Security(SecurityArgs),
128    /// Import findings from external analyzers (Slither, Mythril, Semgrep)
129    Import(ImportArgs),
130    /// Send webhook notifications (Slack, Discord)
131    Notify(NotifyArgs),
132}
133
134// ── Shared CLI Flags ─────────────────────────────────────────────
135
136/// Common flags shared across many commands.
137#[derive(Debug, Default, clap::Args)]
138pub struct SharedFlags {
139    /// Target chain to audit for
140    #[arg(long, global = true, default_value = "ethereum")]
141    pub chain: String,
142
143    /// Path to Foundry project root
144    #[arg(long, global = true, default_value = ".")]
145    pub project: PathBuf,
146
147    /// Output as JSON
148    #[arg(long, global = true)]
149    pub json: bool,
150
151    /// Output as Markdown
152    #[arg(long, global = true)]
153    pub markdown: bool,
154
155    /// Output as HTML
156    #[arg(long, global = true)]
157    pub html: bool,
158
159    /// Strict mode: fail on any finding
160    #[arg(long, global = true)]
161    pub strict: bool,
162
163    /// Offline mode: skip RPC calls
164    #[arg(long, global = true)]
165    pub offline: bool,
166
167    /// Production mode: extra checks
168    #[arg(long, global = true)]
169    pub production: bool,
170
171    /// Generate report file
172    #[arg(long, global = true)]
173    pub report: bool,
174
175    /// Number of parallel workers
176    #[arg(long, global = true, default_value = "4")]
177    pub parallelism: usize,
178}
179
180// ── Per-command args ─────────────────────────────────────────────
181
182#[derive(Debug, clap::Args)]
183pub struct AuditArgs {
184    #[command(flatten)]
185    pub shared: SharedFlags,
186
187    /// Run all available checks
188    #[arg(long, short)]
189    pub full: bool,
190
191    /// Quick mode — skip parser-heavy and expensive checks for fast results
192    #[arg(long)]
193    pub quick: bool,
194
195    /// Show executive summary instead of full terminal report
196    #[arg(long)]
197    pub summary: bool,
198
199    /// Enable AI-powered auditing (uses configured LLM provider)
200    #[arg(long)]
201    pub ai: bool,
202
203    /// AI provider: openai, claude, or ollama
204    #[arg(long, default_value = "openai")]
205    pub ai_provider: String,
206
207    /// AI model identifier (e.g. gpt-5, claude-5-sonnet-20260701)
208    #[arg(long, default_value = "gpt-5")]
209    pub ai_model: String,
210
211    /// AI API key (reads OPENAI_API_KEY or ANTHROPIC_API_KEY env var when empty)
212    #[arg(long)]
213    pub ai_api_key: Option<String>,
214
215    /// Ollama endpoint URL (default: http://localhost:11434)
216    #[arg(long)]
217    pub ollama_endpoint: Option<String>,
218
219    /// Run a full AI audit (security + gas + logic auditors)
220    #[arg(long)]
221    pub ai_full: bool,
222
223    /// Include exploit path analysis
224    #[arg(long)]
225    pub exploit: bool,
226
227    /// Include gas analysis
228    #[arg(long)]
229    pub gas: bool,
230
231    /// Audit all supported chains
232    #[arg(long)]
233    pub all_chains: bool,
234
235    /// Source directories to audit (comma-separated)
236    #[arg(long, default_value = "src")]
237    pub sources: String,
238
239    /// Exclude patterns (comma-separated)
240    #[arg(long)]
241    pub exclude: Option<String>,
242
243    /// Audit template to use (erc20, erc721, defi, bridge, upgradeable)
244    #[arg(long)]
245    pub template: Option<String>,
246
247    /// List available audit templates
248    #[arg(long)]
249    pub list_templates: bool,
250
251    /// Path to a suppression file (default: .forge-guard-suppressions)
252    #[arg(long)]
253    pub suppressions: Option<PathBuf>,
254
255    /// Show suppressed findings (visually marked) instead of hiding them
256    #[arg(long)]
257    pub show_suppressed: bool,
258
259    /// Generate a suppression file from current findings and exit
260    #[arg(long)]
261    pub generate_suppressions: bool,
262
263    /// Send webhook notifications after the audit (see [notifications] config)
264    #[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    /// Contract name to deploy
274    pub contract: Option<String>,
275
276    /// Bypass deployment guard (warnings still shown)
277    #[arg(long)]
278    pub force: bool,
279
280    /// Constructor arguments (comma-separated)
281    #[arg(long)]
282    pub args: Option<String>,
283
284    /// Create2 salt
285    #[arg(long)]
286    pub salt: Option<String>,
287
288    /// Verify contract after deployment
289    #[arg(long)]
290    pub verify: bool,
291
292    /// Send webhook notifications after the deployment attempt
293    #[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    /// Contract name to deploy
303    pub contract: Option<String>,
304
305    /// Constructor arguments (comma-separated)
306    #[arg(long)]
307    pub args: Option<String>,
308
309    /// Verify contract after deployment
310    #[arg(long)]
311    pub verify: bool,
312
313    /// Send webhook notifications after the deployment attempt
314    #[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    /// Number of fuzz runs
324    #[arg(long, default_value = "10000")]
325    pub runs: u32,
326
327    /// Fuzz seed
328    #[arg(long)]
329    pub seed: Option<u64>,
330
331    /// Test function filter
332    #[arg(long)]
333    pub test: Option<String>,
334
335    /// Fuzz contract
336    pub contract: Option<String>,
337}
338
339#[derive(Debug, clap::Args)]
340pub struct InvariantArgs {
341    #[command(flatten)]
342    pub shared: SharedFlags,
343
344    /// Number of runs
345    #[arg(long, default_value = "1000")]
346    pub runs: u32,
347
348    /// Depth of calls per run
349    #[arg(long, default_value = "100")]
350    pub depth: u32,
351
352    /// Invariant contract
353    pub contract: Option<String>,
354
355    /// Fail on revert
356    #[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    /// Number of simulation blocks
366    #[arg(long, default_value = "100")]
367    pub blocks: u32,
368
369    /// Simulate with specific deployer address
370    #[arg(long)]
371    pub deployer: Option<String>,
372
373    /// Include MEV analysis
374    #[arg(long)]
375    pub mev: bool,
376
377    /// Contract to simulate deployment for
378    pub contract: Option<String>,
379}
380
381#[derive(Debug, clap::Args)]
382pub struct GasArgs {
383    #[command(flatten)]
384    pub shared: SharedFlags,
385
386    /// Check specific contract
387    pub contract: Option<String>,
388
389    /// Compare with previous report
390    #[arg(long)]
391    pub diff: Option<String>,
392
393    /// Report gas for all functions
394    #[arg(long)]
395    pub all: bool,
396
397    /// Minimum gas threshold for warnings
398    #[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    /// Path to audit result JSON
408    pub input: Option<PathBuf>,
409
410    /// Output report format
411    #[arg(long, default_value = "markdown")]
412    pub format: String,
413
414    /// Output file path
415    #[arg(long)]
416    pub output: Option<PathBuf>,
417
418    /// Include exploit paths
419    #[arg(long)]
420    pub exploit_paths: bool,
421
422    /// Summary only
423    #[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    /// Contract address to verify
433    pub address: Option<String>,
434
435    /// Contract name
436    pub name: Option<String>,
437
438    /// Explorer API key
439    #[arg(long)]
440    pub api_key: Option<String>,
441
442    /// Constructor arguments ABI-encoded
443    #[arg(long)]
444    pub constructor_args: Option<String>,
445
446    /// Check all deployments
447    #[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    /// Fix issues automatically where possible
457    #[arg(long)]
458    pub fix: bool,
459
460    /// Verbose output
461    #[arg(long, short)]
462    pub verbose: bool,
463
464    /// Check only specific category
465    #[arg(long)]
466    pub check: Option<String>,
467
468    /// Sync forge-guard.toml settings from foundry.toml
469    #[arg(long)]
470    pub sync: bool,
471
472    /// Preview config sync changes without writing (use with --sync)
473    #[arg(long)]
474    pub dry_run: bool,
475
476    /// Show diff of what --sync would change (use with --sync)
477    #[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    /// Watch specific directories
487    #[arg(long, default_value = "src")]
488    pub dirs: String,
489
490    /// Debounce interval in ms
491    #[arg(long, default_value = "500")]
492    pub debounce_ms: u64,
493
494    /// Exclude patterns
495    #[arg(long)]
496    pub exclude: Option<String>,
497
498    /// Run full audit on change
499    #[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    /// CI platform to generate for
509    #[arg(long, default_value = "github")]
510    pub platform: String,
511
512    /// Output directory for CI configs
513    #[arg(long, default_value = ".github/workflows")]
514    pub output: PathBuf,
515
516    /// Include deployment pipeline
517    #[arg(long)]
518    pub include_deploy: bool,
519
520    /// Overwrite existing files
521    #[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    /// Number of benchmark iterations
531    #[arg(long, default_value = "10")]
532    pub iterations: u32,
533
534    /// Compare with baseline
535    #[arg(long)]
536    pub compare: Option<PathBuf>,
537
538    /// Save benchmark results
539    #[arg(long)]
540    pub save: Option<PathBuf>,
541
542    /// Benchmark specific module
543    #[arg(long)]
544    pub module: Option<String>,
545
546    /// Warmup iterations
547    #[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    /// SBOM format: cyclonedx (default) or spdx
557    #[arg(long, default_value = "cyclonedx")]
558    pub format: String,
559
560    /// Output file path (default: stdout)
561    #[arg(long, short)]
562    pub output: Option<PathBuf>,
563
564    /// Also generate a GitHub Actions workflow for SBOM compliance (sbom.yml)
565    #[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    /// Scan depth (0=direct, 1=direct+indirect, etc.)
575    #[arg(long, default_value = "1")]
576    pub depth: u32,
577
578    /// Update vulnerability database
579    #[arg(long)]
580    pub update: bool,
581
582    /// Output only vulnerable packages
583    #[arg(long)]
584    pub vulnerable_only: bool,
585
586    /// Fail on any vulnerability
587    #[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    /// Proxy contract address
597    pub proxy: Option<String>,
598
599    /// Implementation contract address
600    pub implementation: Option<String>,
601
602    /// Check all proxies
603    #[arg(long)]
604    pub all: bool,
605
606    /// Check storage collision
607    #[arg(long)]
608    pub storage_collision: bool,
609
610    /// Check UUPS upgrade path
611    #[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    /// Plugin subcommand
621    #[command(subcommand)]
622    pub action: Option<PluginAction>,
623}
624
625#[derive(Debug, Subcommand)]
626pub enum PluginAction {
627    /// List installed plugins
628    List,
629    /// Install a plugin
630    Install {
631        name: String,
632        source: Option<String>,
633    },
634    /// Remove a plugin
635    Remove { name: String },
636    /// Enable a plugin
637    Enable { name: String },
638    /// Disable a plugin
639    Disable { name: String },
640    /// Create a new plugin scaffold
641    New { name: String },
642}
643
644#[derive(Debug, clap::Args)]
645pub struct ChainArgs {
646    #[command(flatten)]
647    pub shared: SharedFlags,
648
649    /// Chain subcommand
650    #[command(subcommand)]
651    pub action: Option<ChainAction>,
652}
653
654#[derive(Debug, Subcommand)]
655pub enum ChainAction {
656    /// List supported chains
657    List,
658    /// Show chain information
659    Info { chain: String },
660    /// Add a custom chain
661    Add {
662        name: String,
663        rpc_url: Option<String>,
664        chain_id: Option<u64>,
665    },
666    /// Remove a custom chain
667    Remove { name: String },
668    /// Test chain RPC connectivity
669    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    /// Security subcommand
681    #[command(subcommand)]
682    pub action: Option<SecurityAction>,
683}
684
685#[derive(Debug, Subcommand)]
686pub enum SecurityAction {
687    /// Show security configuration
688    Config,
689    /// Set security threshold
690    Threshold { score: u8 },
691    /// Enable a specific check
692    Enable { check: String },
693    /// Disable a specific check
694    Disable { check: String },
695    /// List all security checks
696    List,
697    /// Show security check details
698    Info { check: String },
699}
700
701// ── M15: import + notify args ────────────────────────────────────
702
703#[derive(Debug, clap::Args)]
704pub struct ImportArgs {
705    #[command(flatten)]
706    pub shared: SharedFlags,
707
708    /// Analyzer to import from: slither, mythril, semgrep
709    #[arg(long, default_value = "slither")]
710    pub from: String,
711
712    /// Path to the analyzer's JSON results file
713    pub input: Option<PathBuf>,
714
715    /// Path to a forge-guard audit JSON to deduplicate against (unified report)
716    #[arg(long)]
717    pub findings: Option<PathBuf>,
718
719    /// Write the unified report to this file (JSON)
720    #[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    /// Webhook URL (overrides forge-guard.toml)
730    #[arg(long)]
731    pub webhook: Option<String>,
732
733    /// Platform: slack or discord (auto-detected from URL when omitted)
734    #[arg(long)]
735    pub kind: Option<String>,
736
737    /// Path to a forge-guard audit result JSON to summarize
738    #[arg(long)]
739    pub findings: Option<PathBuf>,
740
741    /// Notification title
742    #[arg(long, default_value = "Forge Guard Audit")]
743    pub title: String,
744
745    /// Message text for simple (non-findings) notifications
746    #[arg(long)]
747    pub message: Option<String>,
748
749    /// Minimum severity gate: informational, low, medium, high, critical
750    #[arg(long)]
751    pub severity: Option<String>,
752
753    /// Only notify when critical findings are present
754    #[arg(long)]
755    pub on_critical: bool,
756
757    /// Only notify when high (or higher) findings are present
758    #[arg(long)]
759    pub on_high: bool,
760
761    /// Print the payload instead of sending it
762    #[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        // Test that try_parse_from works identically to parse()
1630        // by verifying the convenience constructor's underlying mechanism
1631        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}