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