Skip to main content

forge_guard/cli/
mod.rs

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