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