//! Smoke tests for clap-derive command parsing. use std::time::Duration; use bsk::cli::daemon::{DaemonCmd, parse_duration}; use bsk::cli::navigate::NavigateCmd; use bsk::cli::record::{RecordCmd, RecordSub}; use bsk::{Cli, Command}; use clap::Parser; fn parse(args: &[&str]) -> Cli { Cli::try_parse_from(args).expect("clap parse should succeed") } #[test] fn parses_daemon_start_with_defaults() { let cli = parse(&["bsk", "daemon", "start"]); let Command::Daemon(DaemonCmd::Start(args)) = cli.command else { panic!("expected daemon start subcommand"); }; assert!(args.port.is_none()); assert!(!args.foreground); assert_eq!(args.resolved_port(), 52800); assert_eq!(args.resolved_daemon_idle(), Duration::from_secs(600)); } #[test] fn parses_daemon_start_with_flags() { let cli = parse(&[ "bsk", "daemon", "start", "--foreground", "--port", "52900", "--daemon-idle", "2s", "--session-idle", "30s", ]); let Command::Daemon(DaemonCmd::Start(args)) = cli.command else { panic!("expected daemon start subcommand"); }; assert!(args.foreground); assert_eq!(args.resolved_port(), 52900); assert_eq!(args.resolved_daemon_idle(), Duration::from_secs(2)); assert_eq!(args.resolved_session_idle(), Duration::from_secs(30)); } #[test] fn parses_daemon_stop_and_restart() { let cli = parse(&["bsk", "daemon", "stop"]); assert!(matches!(cli.command, Command::Daemon(DaemonCmd::Stop))); let cli = parse(&["bsk", "daemon", "restart", "--foreground"]); let Command::Daemon(DaemonCmd::Restart(args)) = cli.command else { panic!("expected daemon restart subcommand"); }; assert!(args.foreground); } #[test] fn parses_top_level_status_and_doctor() { let cli = parse(&["bsk", "status"]); assert!(matches!(cli.command, Command::Status)); let cli = parse(&["bsk", "doctor"]); assert!(matches!(cli.command, Command::Doctor)); } #[test] fn parses_console_command_with_context_safety_flags() { let cli = parse(&[ "bsk", "console", "--session", "s1", "--tab-id", "9", "--since", "12", "--limit", "75", "--max-text-chars", "2048", "--include-stack", ]); let Command::Console(args) = cli.command else { panic!("expected console command"); }; assert_eq!(args.session, "s1"); assert_eq!(args.tab_id, Some(9)); assert_eq!(args.since, Some(12)); assert_eq!(args.limit, Some(75)); assert_eq!(args.max_text_chars, Some(2048)); assert!(args.include_stack); } #[test] fn rejects_zero_console_bounds() { assert!(Cli::try_parse_from(["bsk", "console", "--session", "s1", "--limit", "0"]).is_err()); assert!( Cli::try_parse_from(["bsk", "console", "--session", "s1", "--max-text-chars", "0"]) .is_err() ); } #[test] fn parses_network_command_with_context_safety_flags() { let cli = parse(&[ "bsk", "network", "--session", "s1", "--tab-id", "9", "--since", "12", "--limit", "75", "--max-text-chars", "2048", ]); let Command::Network(args) = cli.command else { panic!("expected network command"); }; assert_eq!(args.session, "s1"); assert_eq!(args.tab_id, Some(9)); assert_eq!(args.since, Some(12)); assert_eq!(args.limit, Some(75)); assert_eq!(args.max_text_chars, Some(2048)); } #[test] fn rejects_zero_network_bounds() { assert!(Cli::try_parse_from(["bsk", "network", "--session", "s1", "--limit", "0"]).is_err()); assert!( Cli::try_parse_from(["bsk", "network", "--session", "s1", "--max-text-chars", "0"]) .is_err() ); } #[test] fn parses_install_skill_subcommand() { let cli = parse(&["bsk", "install-skill", "--list"]); assert!(matches!(cli.command, Command::InstallSkill(_))); } #[test] fn parses_update_subcommand_with_flags() { let cli = parse(&["bsk", "update", "--check", "--yes", "--no-restart-daemon"]); let Command::Update(args) = cli.command else { panic!("expected update subcommand"); }; assert!(args.check); assert!(args.yes); assert!(!args.restart_daemon); } #[test] fn duration_parser_accepts_units() { assert_eq!(parse_duration("750ms").unwrap(), Duration::from_millis(750)); assert_eq!(parse_duration("2m").unwrap(), Duration::from_secs(120)); } #[test] fn parses_nested_navigate_back_and_forward() { let cli = parse(&["bsk", "navigate", "back", "--session", "s1"]); let Command::Navigate(cmd) = cli.command else { panic!("expected navigate command"); }; assert!(matches!(cmd.command, Some(NavigateCmd::Back(_)))); let cli = parse(&["bsk", "navigate", "forward", "--session", "s1"]); let Command::Navigate(cmd) = cli.command else { panic!("expected navigate command"); }; assert!(matches!(cmd.command, Some(NavigateCmd::Forward(_)))); } #[test] fn parses_click_count_alias() { let cli = parse(&["bsk", "click", "@e1", "--session", "s1", "--count", "2"]); let Command::Click(args) = cli.command else { panic!("expected click command"); }; assert_eq!(args.click_count, 2); } #[test] fn rejects_zero_click_count() { assert!( Cli::try_parse_from(["bsk", "click", "@e1", "--session", "s1", "--count", "0"]).is_err() ); } #[test] fn parses_record_start_with_browser_and_url() { let cli = parse(&[ "bsk", "record", "start", "--browser", "022ca8ac", "--url", "https://x", ]); let Command::Record(RecordCmd { sub: RecordSub::Start(args), }) = cli.command else { panic!("expected record start subcommand"); }; assert_eq!(args.browser.as_deref(), Some("022ca8ac")); assert_eq!(args.url.as_deref(), Some("https://x")); } #[test] fn parses_record_start_without_url() { let cli = parse(&["bsk", "record", "start", "--browser", "022ca8ac"]); let Command::Record(RecordCmd { sub: RecordSub::Start(args), }) = cli.command else { panic!("expected record start subcommand"); }; assert_eq!(args.browser.as_deref(), Some("022ca8ac")); assert!(args.url.is_none()); } #[test] fn parses_session_start_with_window_size() { use bsk::cli::session::{SessionCmd, SessionSub}; let cli = parse(&[ "bsk", "session", "start", "--width", "1280", "--height", "800", ]); let Command::Session(SessionCmd { sub: SessionSub::Start(args), }) = cli.command else { panic!("expected session start subcommand"); }; assert_eq!(args.width, Some(1280)); assert_eq!(args.height, Some(800)); } #[test] fn session_start_window_size_defaults_to_none() { use bsk::cli::session::{SessionCmd, SessionSub}; let cli = parse(&["bsk", "session", "start"]); let Command::Session(SessionCmd { sub: SessionSub::Start(args), }) = cli.command else { panic!("expected session start subcommand"); }; assert!(args.width.is_none()); assert!(args.height.is_none()); } #[test] fn rejects_out_of_range_session_start_window_size() { assert!(Cli::try_parse_from(["bsk", "session", "start", "--width", "99"]).is_err()); assert!(Cli::try_parse_from(["bsk", "session", "start", "--height", "7681"]).is_err()); assert!(Cli::try_parse_from(["bsk", "session", "start", "--width", "abc"]).is_err()); } #[test] fn parses_window_resize() { use bsk::cli::window::{WindowCmd, WindowSub}; let cli = parse(&[ "bsk", "window", "resize", "--session", "s1", "--width", "1280", "--height", "800", ]); let Command::Window(WindowCmd { sub: WindowSub::Resize(args), }) = cli.command else { panic!("expected window resize subcommand"); }; assert_eq!(args.session, "s1"); assert_eq!(args.width, 1280); assert_eq!(args.height, 800); } #[test] fn rejects_invalid_window_resize_dimensions() { assert!( Cli::try_parse_from([ "bsk", "window", "resize", "--session", "s1", "--width", "99", "--height", "800" ]) .is_err() ); assert!( Cli::try_parse_from([ "bsk", "window", "resize", "--session", "s1", "--width", "1280", "--height", "7681" ]) .is_err() ); // width/height are required for resize. assert!(Cli::try_parse_from(["bsk", "window", "resize", "--session", "s1"]).is_err()); } #[test] fn parses_emulate_with_device_preset() { use bsk::cli::emulate::EmulateArgs; let cli = parse(&["bsk", "emulate", "--session", "s1", "--device", "iphone-14"]); let Command::Emulate(EmulateArgs { session, device, off, .. }) = cli.command else { panic!("expected emulate subcommand"); }; assert_eq!(session, "s1"); assert_eq!(device.as_deref(), Some("iphone-14")); assert!(!off); } #[test] fn parses_emulate_manual_overrides() { let cli = parse(&[ "bsk", "emulate", "--session", "s1", "--width", "390", "--height", "844", "--dpr", "3", "--mobile", "--ua", "Mozilla/5.0 (iPhone)", "--accept-language", "zh-CN", "--touch", "--max-touch-points", "5", "--tab-id", "7", ]); let Command::Emulate(args) = cli.command else { panic!("expected emulate subcommand"); }; assert_eq!(args.width, Some(390)); assert_eq!(args.height, Some(844)); assert_eq!(args.dpr, Some(3.0)); assert!(args.mobile); assert_eq!(args.ua.as_deref(), Some("Mozilla/5.0 (iPhone)")); assert_eq!(args.accept_language.as_deref(), Some("zh-CN")); assert!(args.touch); assert_eq!(args.max_touch_points, Some(5)); assert_eq!(args.tab_id, Some(7)); } #[test] fn parses_emulate_off() { let cli = parse(&["bsk", "emulate", "--session", "s1", "--off"]); let Command::Emulate(args) = cli.command else { panic!("expected emulate subcommand"); }; assert!(args.off); } #[test] fn parses_emulate_no_mobile_no_touch() { let cli = parse(&[ "bsk", "emulate", "--session", "s1", "--device", "iphone-14", "--no-mobile", "--no-touch", ]); let Command::Emulate(args) = cli.command else { panic!("expected emulate subcommand"); }; assert!(args.no_mobile); assert!(args.no_touch); assert!(!args.mobile); assert!(!args.touch); } #[test] fn rejects_conflicting_emulate_flags() { for extra in [ &["--mobile", "--no-mobile"][..], &["--touch", "--no-touch"][..], ] { let mut argv = vec!["bsk", "emulate", "--session", "s1", "--device", "iphone-14"]; argv.extend_from_slice(extra); assert!(Cli::try_parse_from(argv).is_err()); } } #[test] fn rejects_invalid_emulate_values() { // Zero / out-of-range dimensions. assert!( Cli::try_parse_from([ "bsk", "emulate", "--session", "s1", "--width", "0", "--height", "844" ]) .is_err() ); // Non-positive dpr. assert!( Cli::try_parse_from([ "bsk", "emulate", "--session", "s1", "--width", "390", "--height", "844", "--dpr", "0" ]) .is_err() ); assert!( Cli::try_parse_from([ "bsk", "emulate", "--session", "s1", "--width", "390", "--height", "844", "--dpr", "abc" ]) .is_err() ); // Zero touch points. assert!( Cli::try_parse_from([ "bsk", "emulate", "--session", "s1", "--max-touch-points", "0" ]) .is_err() ); }