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