Skip to main content

scope/cli/
mod.rs

1//! # CLI Module
2//!
3//! This module defines the command-line interface using `clap` with derive macros.
4//! It provides the main `Cli` struct and `Commands` enum that define all
5//! available commands and their arguments.
6//!
7//! ## UX Features
8//!
9//! - **Progress indicators** — Spinners and step counters via the `progress` module
10//! - **Help with examples** — `after_help` blocks with example invocations
11//! - **Command grouping** — Commands ordered by task (entity lookup, token
12//!   analysis, compliance, data/export, config)
13//! - **Shell completion** — `scope completions bash|zsh|fish` via `clap_complete`
14//! - **Typo suggestions** — Built-in clap fuzzy matching for misspelled commands
15//!
16//! ## Command Structure
17//!
18//! ```text
19//! scope [OPTIONS] <COMMAND>
20//!
21//! Entity lookup:
22//!   address      Analyze a blockchain address (alias: addr)
23//!   tx           Analyze a transaction (alias: transaction)
24//!   insights     Auto-detect target type and run analyses (alias: insight)
25//!
26//! Token analysis:
27//!   crawl        Crawl a token for DEX analytics (alias: token)
28//!   token-health DEX analytics + optional order book (alias: health)
29//!   discover     Browse trending/boosted tokens (alias: disc)
30//!   monitor      Live TUI dashboard (alias: mon)
31//!   market       Peg and order book health for stablecoins
32//!   venues       Manage exchange venue descriptors (alias: ven)
33//!
34//! Compliance:
35//!   compliance   Risk, trace, analyze, compliance-report
36//!
37//! Data & export:
38//!   address-book Add, remove, list, summary (alias: ab, portfolio)
39//!   export       Export to JSON/CSV
40//!   report       Batch and combined reports
41//!
42//! Config & interactive:
43//!   interactive  REPL with preserved context (alias: shell)
44//!   setup        Configure API keys and preferences (alias: config)
45//!   completions  Generate shell completions for bash/zsh/fish
46//!
47//! Options:
48//!   --config <PATH>   Path to configuration file
49//!   -v, --verbose...  Increase logging verbosity
50//!   --ai              Markdown output for agent parsing
51//!   --no-color        Disable colored output
52//!   -h, --help        Print help (with examples)
53//!   -V, --version     Print version
54//! ```
55
56pub mod address;
57pub mod address_book;
58pub mod address_report;
59pub mod compliance;
60pub mod crawl;
61pub mod discover;
62pub mod errors;
63pub mod export;
64pub mod insights;
65pub mod interactive;
66pub mod market;
67pub mod monitor;
68pub mod progress;
69pub mod report;
70pub mod setup;
71pub mod token_health;
72pub mod tx;
73pub mod venues;
74
75use clap::{Parser, Subcommand};
76use std::path::PathBuf;
77
78pub use address::AddressArgs;
79pub use address_book::AddressBookArgs;
80pub use crawl::CrawlArgs;
81pub use export::ExportArgs;
82pub use interactive::InteractiveArgs;
83pub use monitor::MonitorArgs;
84pub use setup::SetupArgs;
85pub use tx::TxArgs;
86
87/// Blockchain Analysis CLI - A tool for blockchain data analysis.
88///
89/// Scope provides comprehensive blockchain analysis capabilities including
90/// address investigation, transaction decoding, address book management, and
91/// data export functionality.
92#[derive(Debug, Parser)]
93#[command(
94    name = "scope",
95    version,
96    about = "Scope Blockchain Analysis - A tool for blockchain data analysis",
97    long_about = format!(
98        "Scope Blockchain Analysis v{}\n\n\
99         A production-grade tool for blockchain data analysis, address management,\n\
100         and transaction investigation.\n\n\
101         Use --help with any subcommand for detailed usage information.",
102        env!("CARGO_PKG_VERSION")
103    ),
104    after_help = "\x1b[1mExamples:\x1b[0m\n  \
105                  scope address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2\n  \
106                  scope crawl USDC --chain ethereum\n  \
107                  scope insights 0xabc123...\n  \
108                  scope monitor USDC\n  \
109                  scope compliance risk 0x742d...\n  \
110                  scope setup\n\n\
111                  \x1b[1mDocumentation:\x1b[0m\n  \
112                  https://github.com/robot-accomplice/scope-blockchain-analysis\n  \
113                  Quickstart guide: docs/QUICKSTART.md"
114)]
115pub struct Cli {
116    /// Subcommand to execute.
117    #[command(subcommand)]
118    pub command: Commands,
119
120    /// Path to configuration file.
121    ///
122    /// Overrides the default location (~/.config/scope/config.yaml).
123    #[arg(long, global = true, value_name = "PATH")]
124    pub config: Option<PathBuf>,
125
126    /// Increase logging verbosity.
127    ///
128    /// Can be specified multiple times:
129    /// -v    = INFO level
130    /// -vv   = DEBUG level
131    /// -vvv  = TRACE level
132    #[arg(short, long, global = true, action = clap::ArgAction::Count)]
133    pub verbose: u8,
134
135    /// Disable colored output.
136    #[arg(long, global = true)]
137    pub no_color: bool,
138
139    /// Output markdown to console for agent parsing.
140    ///
141    /// Forces all commands to emit markdown-formatted output to stdout
142    /// instead of tables or JSON. Useful for LLM/agent consumption.
143    #[arg(long, global = true)]
144    pub ai: bool,
145}
146
147/// Available CLI subcommands.
148#[derive(Debug, Subcommand)]
149pub enum Commands {
150    // -- Entity lookup --------------------------------------------------------
151    /// Analyze a blockchain address.
152    ///
153    /// Retrieves balance, transaction history, and token holdings
154    /// for the specified address.
155    #[command(visible_alias = "addr")]
156    Address(AddressArgs),
157
158    /// Analyze a transaction.
159    ///
160    /// Decodes transaction data, traces execution, and displays
161    /// detailed information about the transaction.
162    #[command(visible_alias = "transaction")]
163    Tx(TxArgs),
164
165    /// Unified insights: infer chain and type, run relevant analyses.
166    ///
167    /// Provide any target (address, tx hash, or token) and Scope will
168    /// detect it, run the appropriate analyses, and present observations.
169    #[command(visible_alias = "insight")]
170    Insights(insights::InsightsArgs),
171
172    // -- Token analysis -------------------------------------------------------
173    /// Crawl a token for analytics data.
174    ///
175    /// Retrieves comprehensive token information including top holders,
176    /// volume statistics, price data, and liquidity. Displays results
177    /// with ASCII charts and can generate markdown reports.
178    #[command(visible_alias = "token")]
179    Crawl(CrawlArgs),
180
181    /// Token health suite: DEX analytics + optional order book (crawl + market).
182    ///
183    /// Combines liquidity, volume, and holder data with optional market/peg
184    /// summary for stablecoins. Use --with-market for order book data.
185    #[command(visible_alias = "health")]
186    TokenHealth(token_health::TokenHealthArgs),
187
188    /// Discover trending and boosted tokens from DexScreener.
189    ///
190    /// Browse featured token profiles, recently boosted tokens,
191    /// or top boosted tokens by activity.
192    #[command(visible_alias = "disc")]
193    Discover(discover::DiscoverArgs),
194
195    /// Live token monitor with real-time TUI dashboard.
196    ///
197    /// Launches a terminal UI with price/volume charts, buy/sell gauges,
198    /// transaction feed, and more. Shortcut for `scope interactive` + `monitor <token>`.
199    #[command(visible_alias = "mon")]
200    Monitor(MonitorArgs),
201
202    /// Peg and order book health for stablecoin markets.
203    ///
204    /// Fetches level-2 depth from exchange APIs and reports
205    /// peg deviation, spread, depth, and configurable health checks.
206    #[command(subcommand)]
207    Market(market::MarketCommands),
208
209    /// Manage exchange venue descriptors.
210    ///
211    /// List available venues, view the YAML schema, initialise the
212    /// user venues directory, or validate custom descriptor files.
213    #[command(subcommand, visible_alias = "ven")]
214    Venues(venues::VenuesCommands),
215
216    // -- Compliance -----------------------------------------------------------
217    /// Compliance and risk analysis commands.
218    ///
219    /// Assess risk, trace taint, detect patterns, and generate
220    /// compliance reports for blockchain addresses.
221    #[command(subcommand)]
222    Compliance(compliance::ComplianceCommands),
223
224    // -- Data & export --------------------------------------------------------
225    /// Address book management commands.
226    ///
227    /// Add, remove, and list watched addresses (wallets, contracts, tokens).
228    /// View aggregated balances across multiple chains.
229    /// Use `@label` in any command to recall a saved address.
230    #[command(visible_alias = "ab", alias = "portfolio", alias = "port")]
231    AddressBook(AddressBookArgs),
232
233    /// Export analysis data.
234    ///
235    /// Export transaction history, balances, or analysis results
236    /// to various formats (JSON, CSV).
237    Export(ExportArgs),
238
239    /// Batch and combined report generation.
240    #[command(subcommand)]
241    Report(report::ReportCommands),
242
243    // -- Config & interactive -------------------------------------------------
244    /// Interactive mode with preserved context.
245    ///
246    /// Launch a REPL where chain, format, and other settings persist
247    /// between commands for faster workflow.
248    #[command(visible_alias = "shell")]
249    Interactive(InteractiveArgs),
250
251    /// Configure Scope settings and API keys.
252    ///
253    /// Run the setup wizard to configure API keys and preferences,
254    /// or use --status to view current configuration.
255    #[command(visible_alias = "config")]
256    Setup(SetupArgs),
257
258    /// Generate shell completions for bash, zsh, or fish.
259    ///
260    /// Output shell completion script to stdout for the specified shell.
261    /// Source the output in your shell profile for tab completion.
262    Completions(CompletionsArgs),
263
264    /// Start the web UI server (localhost).
265    ///
266    /// Serves the same Scope features as the CLI via a local web interface
267    /// at http://127.0.0.1:8080 (default). Use --daemon to run in the
268    /// background, --stop to halt a running daemon.
269    #[command(visible_alias = "serve")]
270    Web(WebArgs),
271}
272
273/// Arguments for the web server command.
274#[derive(Debug, Clone, clap::Args)]
275pub struct WebArgs {
276    /// Port to listen on.
277    #[arg(long, short, default_value = "8080")]
278    pub port: u16,
279
280    /// Address to bind to.
281    ///
282    /// Use 127.0.0.1 for local-only (default) or 0.0.0.0 for LAN access.
283    #[arg(long, default_value = "127.0.0.1")]
284    pub bind: String,
285
286    /// Run as a background daemon.
287    #[arg(long, short)]
288    pub daemon: bool,
289
290    /// Stop a running daemon.
291    #[arg(long)]
292    pub stop: bool,
293}
294
295/// Arguments for the completions command.
296#[derive(Debug, Clone, clap::Args)]
297pub struct CompletionsArgs {
298    /// The shell to generate completions for.
299    #[arg(value_enum)]
300    pub shell: clap_complete::Shell,
301}
302
303impl Cli {
304    /// Parses CLI arguments from the environment.
305    ///
306    /// This is a convenience wrapper around `clap::Parser::parse()`.
307    pub fn parse_args() -> Self {
308        Self::parse()
309    }
310
311    /// Returns the log level based on verbosity flag.
312    ///
313    /// Maps the `-v` count to tracing log levels:
314    /// - 0: WARN (default)
315    /// - 1: INFO
316    /// - 2: DEBUG
317    /// - 3+: TRACE
318    pub fn log_level(&self) -> tracing::Level {
319        match self.verbose {
320            0 => tracing::Level::WARN,
321            1 => tracing::Level::INFO,
322            2 => tracing::Level::DEBUG,
323            _ => tracing::Level::TRACE,
324        }
325    }
326}
327
328// ============================================================================
329// Unit Tests
330// ============================================================================
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use clap::Parser;
336
337    #[test]
338    fn test_cli_parse_address_command() {
339        let cli = Cli::try_parse_from([
340            "scope",
341            "address",
342            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
343        ])
344        .unwrap();
345
346        assert!(matches!(cli.command, Commands::Address(_)));
347        assert!(cli.config.is_none());
348        assert_eq!(cli.verbose, 0);
349    }
350
351    #[test]
352    fn test_cli_parse_address_alias() {
353        let cli = Cli::try_parse_from([
354            "scope",
355            "addr",
356            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
357        ])
358        .unwrap();
359
360        assert!(matches!(cli.command, Commands::Address(_)));
361    }
362
363    #[test]
364    fn test_cli_parse_tx_command() {
365        let cli = Cli::try_parse_from([
366            "scope",
367            "tx",
368            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
369        ])
370        .unwrap();
371
372        assert!(matches!(cli.command, Commands::Tx(_)));
373    }
374
375    #[test]
376    fn test_cli_parse_tx_alias() {
377        let cli = Cli::try_parse_from([
378            "scope",
379            "transaction",
380            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
381        ])
382        .unwrap();
383
384        assert!(matches!(cli.command, Commands::Tx(_)));
385    }
386
387    #[test]
388    fn test_cli_parse_address_book_command() {
389        let cli = Cli::try_parse_from(["scope", "address-book", "list"]).unwrap();
390
391        assert!(matches!(cli.command, Commands::AddressBook(_)));
392    }
393
394    #[test]
395    fn test_cli_parse_address_book_portfolio_alias() {
396        let cli = Cli::try_parse_from(["scope", "portfolio", "list"]).unwrap();
397
398        assert!(matches!(cli.command, Commands::AddressBook(_)));
399    }
400
401    #[test]
402    fn test_cli_parse_export_command() {
403        let cli = Cli::try_parse_from([
404            "scope",
405            "export",
406            "--address",
407            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
408            "--output",
409            "data.json",
410        ])
411        .unwrap();
412
413        assert!(matches!(cli.command, Commands::Export(_)));
414    }
415
416    #[test]
417    fn test_cli_parse_interactive_command() {
418        let cli = Cli::try_parse_from(["scope", "interactive"]).unwrap();
419
420        assert!(matches!(cli.command, Commands::Interactive(_)));
421    }
422
423    #[test]
424    fn test_cli_parse_interactive_alias() {
425        let cli = Cli::try_parse_from(["scope", "shell"]).unwrap();
426
427        assert!(matches!(cli.command, Commands::Interactive(_)));
428    }
429
430    #[test]
431    fn test_cli_parse_interactive_no_banner() {
432        let cli = Cli::try_parse_from(["scope", "interactive", "--no-banner"]).unwrap();
433
434        if let Commands::Interactive(args) = cli.command {
435            assert!(args.no_banner);
436        } else {
437            panic!("Expected Interactive command");
438        }
439    }
440
441    #[test]
442    fn test_cli_verbose_flag_counting() {
443        let cli = Cli::try_parse_from([
444            "scope",
445            "-vvv",
446            "address",
447            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
448        ])
449        .unwrap();
450
451        assert_eq!(cli.verbose, 3);
452    }
453
454    #[test]
455    fn test_cli_verbose_separate_flags() {
456        let cli = Cli::try_parse_from([
457            "scope",
458            "-v",
459            "-v",
460            "address",
461            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
462        ])
463        .unwrap();
464
465        assert_eq!(cli.verbose, 2);
466    }
467
468    #[test]
469    fn test_cli_global_config_option() {
470        let cli = Cli::try_parse_from([
471            "scope",
472            "--config",
473            "/custom/path.yaml",
474            "tx",
475            "0xabc123def456789012345678901234567890123456789012345678901234abcd",
476        ])
477        .unwrap();
478
479        assert_eq!(cli.config, Some(PathBuf::from("/custom/path.yaml")));
480    }
481
482    #[test]
483    fn test_cli_config_long_flag() {
484        let cli = Cli::try_parse_from([
485            "scope",
486            "--config",
487            "/custom/config.yaml",
488            "address",
489            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
490        ])
491        .unwrap();
492
493        assert_eq!(cli.config, Some(PathBuf::from("/custom/config.yaml")));
494    }
495
496    #[test]
497    fn test_cli_no_color_flag() {
498        let cli = Cli::try_parse_from([
499            "scope",
500            "--no-color",
501            "address",
502            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
503        ])
504        .unwrap();
505
506        assert!(cli.no_color);
507    }
508
509    #[test]
510    fn test_cli_missing_required_args_fails() {
511        let result = Cli::try_parse_from(["scope", "address"]);
512        assert!(result.is_err());
513    }
514
515    #[test]
516    fn test_cli_invalid_subcommand_fails() {
517        let result = Cli::try_parse_from(["scope", "invalid"]);
518        assert!(result.is_err());
519    }
520
521    #[test]
522    fn test_cli_log_level_default() {
523        let cli = Cli::try_parse_from([
524            "scope",
525            "address",
526            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
527        ])
528        .unwrap();
529
530        assert_eq!(cli.log_level(), tracing::Level::WARN);
531    }
532
533    #[test]
534    fn test_cli_log_level_info() {
535        let cli = Cli::try_parse_from([
536            "scope",
537            "-v",
538            "address",
539            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
540        ])
541        .unwrap();
542
543        assert_eq!(cli.log_level(), tracing::Level::INFO);
544    }
545
546    #[test]
547    fn test_cli_log_level_debug() {
548        let cli = Cli::try_parse_from([
549            "scope",
550            "-vv",
551            "address",
552            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
553        ])
554        .unwrap();
555
556        assert_eq!(cli.log_level(), tracing::Level::DEBUG);
557    }
558
559    #[test]
560    fn test_cli_log_level_trace() {
561        let cli = Cli::try_parse_from([
562            "scope",
563            "-vvvv",
564            "address",
565            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
566        ])
567        .unwrap();
568
569        assert_eq!(cli.log_level(), tracing::Level::TRACE);
570    }
571
572    #[test]
573    fn test_cli_debug_impl() {
574        let cli = Cli::try_parse_from([
575            "scope",
576            "address",
577            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
578        ])
579        .unwrap();
580
581        let debug_str = format!("{:?}", cli);
582        assert!(debug_str.contains("Cli"));
583        assert!(debug_str.contains("Address"));
584    }
585
586    // ========================================================================
587    // Monitor command tests
588    // ========================================================================
589
590    #[test]
591    fn test_cli_parse_monitor_command() {
592        let cli = Cli::try_parse_from([
593            "scope",
594            "monitor",
595            "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
596        ])
597        .unwrap();
598
599        assert!(matches!(cli.command, Commands::Monitor(_)));
600    }
601
602    #[test]
603    fn test_cli_parse_monitor_alias_mon() {
604        let cli = Cli::try_parse_from(["scope", "mon", "USDC"]).unwrap();
605
606        assert!(matches!(cli.command, Commands::Monitor(_)));
607        if let Commands::Monitor(args) = cli.command {
608            assert_eq!(args.token, "USDC");
609            assert_eq!(args.chain, "ethereum"); // default
610            assert!(args.layout.is_none());
611            assert!(args.refresh.is_none());
612            assert!(args.scale.is_none());
613            assert!(args.color_scheme.is_none());
614            assert!(args.export.is_none());
615        }
616    }
617
618    #[test]
619    fn test_cli_parse_monitor_with_chain() {
620        let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--chain", "solana"]).unwrap();
621
622        if let Commands::Monitor(args) = cli.command {
623            assert_eq!(args.token, "USDC");
624            assert_eq!(args.chain, "solana");
625        } else {
626            panic!("Expected Monitor command");
627        }
628    }
629
630    #[test]
631    fn test_cli_parse_monitor_chain_short_flag() {
632        let cli = Cli::try_parse_from(["scope", "monitor", "PEPE", "-c", "ethereum"]).unwrap();
633
634        if let Commands::Monitor(args) = cli.command {
635            assert_eq!(args.token, "PEPE");
636            assert_eq!(args.chain, "ethereum");
637        } else {
638            panic!("Expected Monitor command");
639        }
640    }
641
642    #[test]
643    fn test_cli_parse_monitor_with_layout() {
644        let cli =
645            Cli::try_parse_from(["scope", "monitor", "USDC", "--layout", "chart-focus"]).unwrap();
646
647        if let Commands::Monitor(args) = cli.command {
648            assert_eq!(args.layout, Some(monitor::LayoutPreset::ChartFocus));
649        } else {
650            panic!("Expected Monitor command");
651        }
652    }
653
654    #[test]
655    fn test_cli_parse_monitor_with_refresh() {
656        let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--refresh", "3"]).unwrap();
657
658        if let Commands::Monitor(args) = cli.command {
659            assert_eq!(args.refresh, Some(3));
660        } else {
661            panic!("Expected Monitor command");
662        }
663    }
664
665    #[test]
666    fn test_cli_parse_monitor_with_scale() {
667        let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--scale", "log"]).unwrap();
668
669        if let Commands::Monitor(args) = cli.command {
670            assert_eq!(args.scale, Some(monitor::ScaleMode::Log));
671        } else {
672            panic!("Expected Monitor command");
673        }
674    }
675
676    #[test]
677    fn test_cli_parse_monitor_with_color_scheme() {
678        let cli =
679            Cli::try_parse_from(["scope", "monitor", "USDC", "--color-scheme", "blue-orange"])
680                .unwrap();
681
682        if let Commands::Monitor(args) = cli.command {
683            assert_eq!(args.color_scheme, Some(monitor::ColorScheme::BlueOrange));
684        } else {
685            panic!("Expected Monitor command");
686        }
687    }
688
689    #[test]
690    fn test_cli_parse_monitor_with_export() {
691        let cli =
692            Cli::try_parse_from(["scope", "monitor", "USDC", "--export", "/tmp/out.csv"]).unwrap();
693
694        if let Commands::Monitor(args) = cli.command {
695            assert_eq!(args.export, Some(std::path::PathBuf::from("/tmp/out.csv")));
696        } else {
697            panic!("Expected Monitor command");
698        }
699    }
700
701    #[test]
702    fn test_cli_parse_monitor_short_flags() {
703        let cli = Cli::try_parse_from([
704            "scope", "mon", "USDC", "-c", "solana", "-l", "compact", "-r", "10", "-s", "log", "-e",
705            "data.csv",
706        ])
707        .unwrap();
708
709        if let Commands::Monitor(args) = cli.command {
710            assert_eq!(args.token, "USDC");
711            assert_eq!(args.chain, "solana");
712            assert_eq!(args.layout, Some(monitor::LayoutPreset::Compact));
713            assert_eq!(args.refresh, Some(10));
714            assert_eq!(args.scale, Some(monitor::ScaleMode::Log));
715            assert_eq!(args.export, Some(std::path::PathBuf::from("data.csv")));
716        } else {
717            panic!("Expected Monitor command");
718        }
719    }
720
721    #[test]
722    fn test_cli_parse_monitor_missing_token_fails() {
723        let result = Cli::try_parse_from(["scope", "monitor"]);
724        assert!(result.is_err());
725    }
726
727    #[test]
728    fn test_cli_parse_monitor_invalid_layout_fails() {
729        let result = Cli::try_parse_from(["scope", "monitor", "USDC", "--layout", "invalid"]);
730        assert!(result.is_err());
731    }
732
733    #[test]
734    fn test_cli_parse_monitor_invalid_scale_fails() {
735        let result = Cli::try_parse_from(["scope", "monitor", "USDC", "--scale", "quadratic"]);
736        assert!(result.is_err());
737    }
738
739    #[test]
740    fn test_cli_parse_market_summary() {
741        let cli = Cli::try_parse_from(["scope", "market", "summary"]).unwrap();
742        assert!(matches!(cli.command, Commands::Market(_)));
743    }
744
745    #[test]
746    fn test_cli_parse_market_summary_with_pair() {
747        let cli = Cli::try_parse_from(["scope", "market", "summary", "PUSD_USDT"]).unwrap();
748        if let Commands::Market(market::MarketCommands::Summary(args)) = cli.command {
749            assert_eq!(args.pair, "PUSD_USDT");
750        } else {
751            panic!("Expected Market Summary command");
752        }
753    }
754
755    #[test]
756    fn test_cli_parse_report_batch() {
757        let cli = Cli::try_parse_from([
758            "scope",
759            "report",
760            "batch",
761            "--addresses",
762            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
763            "--output",
764            "report.md",
765        ])
766        .unwrap();
767        assert!(matches!(cli.command, Commands::Report(_)));
768    }
769
770    #[test]
771    fn test_cli_parse_market_summary_with_thresholds() {
772        let cli = Cli::try_parse_from([
773            "scope",
774            "market",
775            "summary",
776            "--peg-range",
777            "0.002",
778            "--min-bid-ask-ratio",
779            "0.1",
780            "--max-bid-ask-ratio",
781            "10",
782        ])
783        .unwrap();
784        if let Commands::Market(market::MarketCommands::Summary(args)) = cli.command {
785            assert_eq!(args.peg_range, 0.002);
786            assert_eq!(args.min_bid_ask_ratio, 0.1);
787            assert_eq!(args.max_bid_ask_ratio, 10.0);
788        } else {
789            panic!("Expected Market Summary command");
790        }
791    }
792
793    #[test]
794    fn test_cli_parse_token_health() {
795        let cli = Cli::try_parse_from(["scope", "token-health", "USDC"]).unwrap();
796        if let Commands::TokenHealth(args) = cli.command {
797            assert_eq!(args.token, "USDC");
798            assert!(!args.with_market);
799        } else {
800            panic!("Expected TokenHealth command");
801        }
802    }
803
804    #[test]
805    fn test_cli_parse_token_health_alias() {
806        let cli = Cli::try_parse_from(["scope", "health", "USDC", "--with-market"]).unwrap();
807        if let Commands::TokenHealth(args) = cli.command {
808            assert_eq!(args.token, "USDC");
809            assert!(args.with_market);
810            assert_eq!(args.venue, "binance"); // default venue
811        } else {
812            panic!("Expected TokenHealth command");
813        }
814    }
815
816    #[test]
817    fn test_cli_parse_token_health_venue_biconomy() {
818        let cli = Cli::try_parse_from([
819            "scope",
820            "token-health",
821            "USDC",
822            "--with-market",
823            "--venue",
824            "biconomy",
825        ])
826        .unwrap();
827        if let Commands::TokenHealth(args) = cli.command {
828            assert_eq!(args.venue, "biconomy");
829        } else {
830            panic!("Expected TokenHealth command");
831        }
832    }
833
834    #[test]
835    fn test_cli_parse_token_health_venue_eth() {
836        let cli = Cli::try_parse_from([
837            "scope",
838            "token-health",
839            "USDC",
840            "--with-market",
841            "--venue",
842            "eth",
843        ])
844        .unwrap();
845        if let Commands::TokenHealth(args) = cli.command {
846            assert_eq!(args.venue, "eth");
847        } else {
848            panic!("Expected TokenHealth command");
849        }
850    }
851
852    #[test]
853    fn test_cli_parse_token_health_venue_solana() {
854        let cli = Cli::try_parse_from([
855            "scope",
856            "token-health",
857            "USDC",
858            "--with-market",
859            "--venue",
860            "solana",
861        ])
862        .unwrap();
863        if let Commands::TokenHealth(args) = cli.command {
864            assert_eq!(args.venue, "solana");
865        } else {
866            panic!("Expected TokenHealth command");
867        }
868    }
869
870    #[test]
871    fn test_cli_parse_ai_flag() {
872        let cli = Cli::try_parse_from([
873            "scope",
874            "--ai",
875            "address",
876            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
877        ])
878        .unwrap();
879        assert!(cli.ai);
880    }
881}