1use std::path::PathBuf;
6
7use clap::{Parser, Subcommand, ValueEnum};
8use clap_complete::Shell;
9
10fn parse_shell(value: &str) -> Result<Shell, String> {
11 match value {
12 "bash" => Ok(Shell::Bash),
13 "zsh" => Ok(Shell::Zsh),
14 "fish" => Ok(Shell::Fish),
15 "pwsh" | "powershell" => Ok(Shell::PowerShell),
16 _ => Err(format!(
17 "Unsupported shell: {}. Use bash, zsh, fish, or pwsh.",
18 value
19 )),
20 }
21}
22
23#[derive(Parser, Debug)]
25#[command(name = "alopex")]
26#[command(version, about, long_about = None)]
27pub struct Cli {
28 #[arg(long)]
30 pub data_dir: Option<String>,
31
32 #[arg(long)]
34 pub profile: Option<String>,
35
36 #[arg(long, conflicts_with = "data_dir")]
38 pub in_memory: bool,
39
40 #[arg(long, value_enum)]
42 pub output: Option<OutputFormat>,
43
44 #[arg(long)]
46 pub limit: Option<usize>,
47
48 #[arg(long)]
50 pub quiet: bool,
51
52 #[arg(long)]
54 pub verbose: bool,
55
56 #[arg(long)]
58 pub insecure: bool,
59
60 #[arg(long, value_enum, default_value = "multi")]
62 pub thread_mode: ThreadMode,
63
64 #[arg(long, short = 'b')]
66 pub batch: bool,
67
68 #[arg(long)]
70 pub yes: bool,
71
72 #[command(subcommand)]
74 pub command: Option<Command>,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
79pub enum OutputFormat {
80 Table,
82 Json,
84 Jsonl,
86 Csv,
88 Tsv,
90}
91
92impl OutputFormat {
93 #[allow(dead_code)]
95 pub fn supports_streaming(&self) -> bool {
96 matches!(self, Self::Json | Self::Jsonl | Self::Csv | Self::Tsv)
97 }
98}
99
100impl Cli {
101 pub fn output_format(&self) -> OutputFormat {
102 self.output.unwrap_or(OutputFormat::Table)
103 }
104
105 pub fn output_is_explicit(&self) -> bool {
106 self.output.is_some()
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
112pub enum ThreadMode {
113 Multi,
115 Single,
117}
118
119#[derive(Subcommand, Debug)]
121pub enum Command {
122 Profile {
124 #[command(subcommand)]
125 command: Option<ProfileCommand>,
126 },
127 Kv {
129 #[command(subcommand)]
130 command: Option<KvCommand>,
131 },
132 Sql(SqlCommand),
134 Vector {
136 #[command(subcommand)]
137 command: Option<VectorCommand>,
138 },
139 Hnsw {
141 #[command(subcommand)]
142 command: Option<HnswCommand>,
143 },
144 Columnar {
146 #[command(subcommand)]
147 command: Option<ColumnarCommand>,
148 },
149 Server {
151 #[command(subcommand)]
152 command: Option<ServerCommand>,
153 },
154 Lifecycle {
156 #[command(subcommand)]
157 command: Option<LifecycleCommand>,
158 },
159 Version,
161 Completions {
163 #[arg(value_parser = parse_shell, value_name = "SHELL")]
165 shell: Shell,
166 },
167}
168
169#[derive(Subcommand, Debug, Clone)]
171pub enum ProfileCommand {
172 Create {
174 name: String,
176 #[arg(long)]
178 data_dir: String,
179 },
180 List,
182 Show {
184 name: String,
186 },
187 Delete {
189 name: String,
191 },
192 SetDefault {
194 name: String,
196 },
197}
198
199#[derive(Subcommand, Debug)]
201pub enum KvCommand {
202 Get {
204 key: String,
206 },
207 Put {
209 key: String,
211 value: String,
213 },
214 Delete {
216 key: String,
218 },
219 List {
221 #[arg(long)]
223 prefix: Option<String>,
224 },
225 #[command(subcommand)]
227 Txn(KvTxnCommand),
228}
229
230#[derive(Subcommand, Debug)]
232pub enum KvTxnCommand {
233 Begin {
235 #[arg(long)]
237 timeout_secs: Option<u64>,
238 },
239 Get {
241 key: String,
243 #[arg(long)]
245 txn_id: String,
246 },
247 Put {
249 key: String,
251 value: String,
253 #[arg(long)]
255 txn_id: String,
256 },
257 Delete {
259 key: String,
261 #[arg(long)]
263 txn_id: String,
264 },
265 Commit {
267 #[arg(long)]
269 txn_id: String,
270 },
271 Rollback {
273 #[arg(long)]
275 txn_id: String,
276 },
277}
278
279#[derive(Parser, Debug)]
281pub struct SqlCommand {
282 #[arg(conflicts_with = "file")]
284 pub query: Option<String>,
285
286 #[arg(long, short = 'f')]
288 pub file: Option<String>,
289
290 #[arg(long)]
292 pub fetch_size: Option<usize>,
293
294 #[arg(long)]
296 pub max_rows: Option<usize>,
297
298 #[arg(long)]
300 pub deadline: Option<String>,
301
302 #[arg(long)]
304 pub tui: bool,
305}
306
307#[derive(Subcommand, Debug)]
309pub enum VectorCommand {
310 Search {
312 #[arg(long)]
314 index: String,
315 #[arg(long)]
317 query: String,
318 #[arg(long, short = 'k', default_value = "10")]
320 k: usize,
321 #[arg(long)]
323 progress: bool,
324 },
325 Upsert {
327 #[arg(long)]
329 index: String,
330 #[arg(long)]
332 key: String,
333 #[arg(long)]
335 vector: String,
336 },
337 Delete {
339 #[arg(long)]
341 index: String,
342 #[arg(long)]
344 key: String,
345 },
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
350pub enum DistanceMetric {
351 #[default]
353 Cosine,
354 L2,
356 Ip,
358}
359
360#[derive(Subcommand, Debug)]
362pub enum HnswCommand {
363 Create {
365 name: String,
367 #[arg(long)]
369 dim: usize,
370 #[arg(long, value_enum, default_value = "cosine")]
372 metric: DistanceMetric,
373 },
374 Stats {
376 name: String,
378 },
379 Drop {
381 name: String,
383 },
384}
385
386#[derive(Subcommand, Debug)]
388pub enum ColumnarCommand {
389 Scan {
391 #[arg(long)]
393 segment: String,
394 #[arg(long)]
396 progress: bool,
397 },
398 Stats {
400 #[arg(long)]
402 segment: String,
403 },
404 List,
406 Ingest {
408 #[arg(long)]
410 file: PathBuf,
411 #[arg(long)]
413 table: String,
414 #[arg(long, default_value = ",", value_parser = clap::value_parser!(char))]
416 delimiter: char,
417 #[arg(
419 long,
420 default_value = "true",
421 value_parser = clap::value_parser!(bool),
422 action = clap::ArgAction::Set
423 )]
424 header: bool,
425 #[arg(long, default_value = "zstd")]
427 compression: String,
428 #[arg(long)]
430 row_group_size: Option<usize>,
431 },
432 #[command(subcommand)]
434 Index(IndexCommand),
435}
436
437#[derive(Subcommand, Debug)]
439pub enum IndexCommand {
440 Create {
442 #[arg(long)]
444 segment: String,
445 #[arg(long)]
447 column: String,
448 #[arg(long = "type")]
450 index_type: String,
451 },
452 List {
454 #[arg(long)]
456 segment: String,
457 },
458 Drop {
460 #[arg(long)]
462 segment: String,
463 #[arg(long)]
465 column: String,
466 },
467}
468
469#[derive(Subcommand, Debug)]
471pub enum ServerCommand {
472 Status,
474 Metrics,
476 Health,
478 Compaction {
480 #[command(subcommand)]
481 command: CompactionCommand,
482 },
483}
484
485#[derive(Subcommand, Debug)]
487pub enum LifecycleCommand {
488 Archive,
490 Restore,
492 Backup,
494 Export,
496}
497
498#[derive(Subcommand, Debug)]
500pub enum CompactionCommand {
501 Trigger,
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 #[test]
510 fn test_parse_in_memory_kv_get() {
511 let args = vec!["alopex", "--in-memory", "kv", "get", "mykey"];
512 let cli = Cli::try_parse_from(args).unwrap();
513
514 assert!(cli.in_memory);
515 assert!(cli.data_dir.is_none());
516 assert_eq!(cli.output_format(), OutputFormat::Table);
517 assert!(matches!(
518 cli.command,
519 Some(Command::Kv {
520 command: Some(KvCommand::Get { key })
521 }) if key == "mykey"
522 ));
523 }
524
525 #[test]
526 fn test_parse_data_dir_sql() {
527 let args = vec![
528 "alopex",
529 "--data-dir",
530 "/path/to/db",
531 "sql",
532 "SELECT * FROM users",
533 ];
534 let cli = Cli::try_parse_from(args).unwrap();
535
536 assert!(!cli.in_memory);
537 assert_eq!(cli.data_dir, Some("/path/to/db".to_string()));
538 assert!(matches!(
539 cli.command,
540 Some(Command::Sql(SqlCommand { query: Some(q), file: None, .. })) if q == "SELECT * FROM users"
541 ));
542 }
543
544 #[test]
545 fn test_parse_output_format() {
546 let args = vec!["alopex", "--in-memory", "--output", "jsonl", "kv", "list"];
547 let cli = Cli::try_parse_from(args).unwrap();
548
549 assert_eq!(cli.output_format(), OutputFormat::Jsonl);
550 assert!(cli.output_is_explicit());
551 }
552
553 #[test]
554 fn test_parse_limit() {
555 let args = vec!["alopex", "--in-memory", "--limit", "100", "kv", "list"];
556 let cli = Cli::try_parse_from(args).unwrap();
557
558 assert_eq!(cli.limit, Some(100));
559 }
560
561 #[test]
562 fn test_parse_sql_streaming_options() {
563 let args = vec![
564 "alopex",
565 "sql",
566 "--fetch-size",
567 "500",
568 "--max-rows",
569 "250",
570 "--deadline",
571 "30s",
572 "SELECT 1",
573 ];
574 let cli = Cli::try_parse_from(args).unwrap();
575
576 match cli.command {
577 Some(Command::Sql(cmd)) => {
578 assert_eq!(cmd.fetch_size, Some(500));
579 assert_eq!(cmd.max_rows, Some(250));
580 assert_eq!(cmd.deadline.as_deref(), Some("30s"));
581 assert!(!cmd.tui);
582 }
583 _ => panic!("expected sql command"),
584 }
585 }
586
587 #[test]
588 fn test_parse_sql_tui_flag() {
589 let args = vec!["alopex", "sql", "--tui", "SELECT 1"];
590 let cli = Cli::try_parse_from(args).unwrap();
591
592 match cli.command {
593 Some(Command::Sql(cmd)) => {
594 assert!(cmd.tui);
595 assert_eq!(cmd.query.as_deref(), Some("SELECT 1"));
596 }
597 _ => panic!("expected sql command"),
598 }
599 }
600
601 #[test]
602 fn test_parse_server_status() {
603 let args = vec!["alopex", "server", "status"];
604 let cli = Cli::try_parse_from(args).unwrap();
605
606 assert!(matches!(
607 cli.command,
608 Some(Command::Server {
609 command: Some(ServerCommand::Status)
610 })
611 ));
612 }
613
614 #[test]
615 fn test_parse_server_compaction_trigger() {
616 let args = vec!["alopex", "server", "compaction", "trigger"];
617 let cli = Cli::try_parse_from(args).unwrap();
618
619 assert!(matches!(
620 cli.command,
621 Some(Command::Server {
622 command: Some(ServerCommand::Compaction {
623 command: CompactionCommand::Trigger
624 })
625 })
626 ));
627 }
628
629 #[test]
630 fn test_parse_verbose_quiet() {
631 let args = vec!["alopex", "--in-memory", "--verbose", "kv", "list"];
632 let cli = Cli::try_parse_from(args).unwrap();
633
634 assert!(cli.verbose);
635 assert!(!cli.quiet);
636 }
637
638 #[test]
639 fn test_parse_thread_mode() {
640 let args = vec![
641 "alopex",
642 "--in-memory",
643 "--thread-mode",
644 "single",
645 "kv",
646 "list",
647 ];
648 let cli = Cli::try_parse_from(args).unwrap();
649
650 assert_eq!(cli.thread_mode, ThreadMode::Single);
651 }
652
653 #[test]
654 fn test_parse_profile_option_batch_yes() {
655 let args = vec![
656 "alopex",
657 "--profile",
658 "dev",
659 "--batch",
660 "--yes",
661 "--in-memory",
662 "kv",
663 "list",
664 ];
665 let cli = Cli::try_parse_from(args).unwrap();
666
667 assert_eq!(cli.profile.as_deref(), Some("dev"));
668 assert!(cli.batch);
669 assert!(cli.yes);
670 }
671
672 #[test]
673 fn test_parse_batch_short_flag() {
674 let args = vec!["alopex", "-b", "--in-memory", "kv", "list"];
675 let cli = Cli::try_parse_from(args).unwrap();
676
677 assert!(cli.batch);
678 }
679
680 #[test]
681 fn test_parse_profile_create_subcommand() {
682 let args = vec![
683 "alopex",
684 "profile",
685 "create",
686 "dev",
687 "--data-dir",
688 "/path/to/db",
689 ];
690 let cli = Cli::try_parse_from(args).unwrap();
691
692 assert!(matches!(
693 cli.command,
694 Some(Command::Profile {
695 command: Some(ProfileCommand::Create { name, data_dir })
696 })
697 if name == "dev" && data_dir == "/path/to/db"
698 ));
699 }
700
701 #[test]
702 fn test_parse_completions_bash() {
703 let args = vec!["alopex", "completions", "bash"];
704 let cli = Cli::try_parse_from(args).unwrap();
705
706 assert!(matches!(
707 cli.command,
708 Some(Command::Completions { shell }) if shell == Shell::Bash
709 ));
710 }
711
712 #[test]
713 fn test_parse_completions_pwsh() {
714 let args = vec!["alopex", "completions", "pwsh"];
715 let cli = Cli::try_parse_from(args).unwrap();
716
717 assert!(matches!(
718 cli.command,
719 Some(Command::Completions { shell }) if shell == Shell::PowerShell
720 ));
721 }
722
723 #[test]
724 fn test_parse_kv_put() {
725 let args = vec!["alopex", "--in-memory", "kv", "put", "mykey", "myvalue"];
726 let cli = Cli::try_parse_from(args).unwrap();
727
728 assert!(matches!(
729 cli.command,
730 Some(Command::Kv {
731 command: Some(KvCommand::Put { key, value })
732 }) if key == "mykey" && value == "myvalue"
733 ));
734 }
735
736 #[test]
737 fn test_parse_kv_delete() {
738 let args = vec!["alopex", "--in-memory", "kv", "delete", "mykey"];
739 let cli = Cli::try_parse_from(args).unwrap();
740
741 assert!(matches!(
742 cli.command,
743 Some(Command::Kv {
744 command: Some(KvCommand::Delete { key })
745 }) if key == "mykey"
746 ));
747 }
748
749 #[test]
750 fn test_parse_kv_txn_begin() {
751 let args = vec!["alopex", "kv", "txn", "begin", "--timeout-secs", "30"];
752 let cli = Cli::try_parse_from(args).unwrap();
753
754 assert!(matches!(
755 cli.command,
756 Some(Command::Kv {
757 command: Some(KvCommand::Txn(KvTxnCommand::Begin {
758 timeout_secs: Some(30)
759 }))
760 })
761 ));
762 }
763
764 #[test]
765 fn test_parse_kv_txn_get_requires_txn_id() {
766 let args = vec!["alopex", "kv", "txn", "get", "mykey"];
767
768 assert!(Cli::try_parse_from(args).is_err());
769 }
770
771 #[test]
772 fn test_parse_kv_txn_get() {
773 let args = vec!["alopex", "kv", "txn", "get", "mykey", "--txn-id", "txn123"];
774 let cli = Cli::try_parse_from(args).unwrap();
775
776 assert!(matches!(
777 cli.command,
778 Some(Command::Kv {
779 command: Some(KvCommand::Txn(KvTxnCommand::Get { key, txn_id }))
780 }) if key == "mykey" && txn_id == "txn123"
781 ));
782 }
783
784 #[test]
785 fn test_parse_kv_list_with_prefix() {
786 let args = vec!["alopex", "--in-memory", "kv", "list", "--prefix", "user:"];
787 let cli = Cli::try_parse_from(args).unwrap();
788
789 assert!(matches!(
790 cli.command,
791 Some(Command::Kv {
792 command: Some(KvCommand::List { prefix: Some(p) })
793 }) if p == "user:"
794 ));
795 }
796
797 #[test]
798 fn test_parse_sql_from_file() {
799 let args = vec!["alopex", "--in-memory", "sql", "-f", "query.sql"];
800 let cli = Cli::try_parse_from(args).unwrap();
801
802 assert!(matches!(
803 cli.command,
804 Some(Command::Sql(SqlCommand { query: None, file: Some(f), .. })) if f == "query.sql"
805 ));
806 }
807
808 #[test]
809 fn test_parse_vector_search() {
810 let args = vec![
811 "alopex",
812 "--in-memory",
813 "vector",
814 "search",
815 "--index",
816 "my_index",
817 "--query",
818 "[1.0,2.0,3.0]",
819 "-k",
820 "5",
821 ];
822 let cli = Cli::try_parse_from(args).unwrap();
823
824 assert!(matches!(
825 cli.command,
826 Some(Command::Vector {
827 command: Some(VectorCommand::Search { index, query, k, progress })
828 }) if index == "my_index" && query == "[1.0,2.0,3.0]" && k == 5 && !progress
829 ));
830 }
831
832 #[test]
833 fn test_parse_vector_upsert() {
834 let args = vec![
835 "alopex",
836 "--in-memory",
837 "vector",
838 "upsert",
839 "--index",
840 "my_index",
841 "--key",
842 "vec1",
843 "--vector",
844 "[1.0,2.0,3.0]",
845 ];
846 let cli = Cli::try_parse_from(args).unwrap();
847
848 assert!(matches!(
849 cli.command,
850 Some(Command::Vector {
851 command: Some(VectorCommand::Upsert { index, key, vector })
852 }) if index == "my_index" && key == "vec1" && vector == "[1.0,2.0,3.0]"
853 ));
854 }
855
856 #[test]
857 fn test_parse_vector_delete() {
858 let args = vec![
859 "alopex",
860 "--in-memory",
861 "vector",
862 "delete",
863 "--index",
864 "my_index",
865 "--key",
866 "vec1",
867 ];
868 let cli = Cli::try_parse_from(args).unwrap();
869
870 assert!(matches!(
871 cli.command,
872 Some(Command::Vector {
873 command: Some(VectorCommand::Delete { index, key })
874 }) if index == "my_index" && key == "vec1"
875 ));
876 }
877
878 #[test]
879 fn test_parse_hnsw_create() {
880 let args = vec![
881 "alopex",
882 "--in-memory",
883 "hnsw",
884 "create",
885 "my_index",
886 "--dim",
887 "128",
888 "--metric",
889 "l2",
890 ];
891 let cli = Cli::try_parse_from(args).unwrap();
892
893 assert!(matches!(
894 cli.command,
895 Some(Command::Hnsw {
896 command: Some(HnswCommand::Create { name, dim, metric })
897 }) if name == "my_index" && dim == 128 && metric == DistanceMetric::L2
898 ));
899 }
900
901 #[test]
902 fn test_parse_hnsw_create_default_metric() {
903 let args = vec![
904 "alopex",
905 "--in-memory",
906 "hnsw",
907 "create",
908 "my_index",
909 "--dim",
910 "128",
911 ];
912 let cli = Cli::try_parse_from(args).unwrap();
913
914 assert!(matches!(
915 cli.command,
916 Some(Command::Hnsw {
917 command: Some(HnswCommand::Create { name, dim, metric })
918 }) if name == "my_index" && dim == 128 && metric == DistanceMetric::Cosine
919 ));
920 }
921
922 #[test]
923 fn test_parse_columnar_scan() {
924 let args = vec![
925 "alopex",
926 "--in-memory",
927 "columnar",
928 "scan",
929 "--segment",
930 "seg_001",
931 ];
932 let cli = Cli::try_parse_from(args).unwrap();
933
934 assert!(matches!(
935 cli.command,
936 Some(Command::Columnar {
937 command: Some(ColumnarCommand::Scan { segment, progress })
938 }) if segment == "seg_001" && !progress
939 ));
940 }
941
942 #[test]
943 fn test_parse_columnar_stats() {
944 let args = vec![
945 "alopex",
946 "--in-memory",
947 "columnar",
948 "stats",
949 "--segment",
950 "seg_001",
951 ];
952 let cli = Cli::try_parse_from(args).unwrap();
953
954 assert!(matches!(
955 cli.command,
956 Some(Command::Columnar {
957 command: Some(ColumnarCommand::Stats { segment })
958 }) if segment == "seg_001"
959 ));
960 }
961
962 #[test]
963 fn test_parse_columnar_list() {
964 let args = vec!["alopex", "--in-memory", "columnar", "list"];
965 let cli = Cli::try_parse_from(args).unwrap();
966
967 assert!(matches!(
968 cli.command,
969 Some(Command::Columnar {
970 command: Some(ColumnarCommand::List)
971 })
972 ));
973 }
974
975 #[test]
976 fn test_parse_columnar_ingest_defaults() {
977 let args = vec![
978 "alopex",
979 "--in-memory",
980 "columnar",
981 "ingest",
982 "--file",
983 "data.csv",
984 "--table",
985 "events",
986 ];
987 let cli = Cli::try_parse_from(args).unwrap();
988
989 assert!(matches!(
990 cli.command,
991 Some(Command::Columnar {
992 command: Some(ColumnarCommand::Ingest {
993 file,
994 table,
995 delimiter,
996 header,
997 compression,
998 row_group_size,
999 })
1000 }) if file == std::path::Path::new("data.csv")
1001 && table == "events"
1002 && delimiter == ','
1003 && header
1004 && compression == "zstd"
1005 && row_group_size.is_none()
1006 ));
1007 }
1008
1009 #[test]
1010 fn test_parse_columnar_ingest_custom_options() {
1011 let args = vec![
1012 "alopex",
1013 "--in-memory",
1014 "columnar",
1015 "ingest",
1016 "--file",
1017 "data.csv",
1018 "--table",
1019 "events",
1020 "--delimiter",
1021 ";",
1022 "--header",
1023 "false",
1024 "--compression",
1025 "zstd",
1026 "--row-group-size",
1027 "500",
1028 ];
1029 let cli = Cli::try_parse_from(args).unwrap();
1030
1031 assert!(matches!(
1032 cli.command,
1033 Some(Command::Columnar {
1034 command: Some(ColumnarCommand::Ingest {
1035 file,
1036 table,
1037 delimiter,
1038 header,
1039 compression,
1040 row_group_size,
1041 })
1042 }) if file == std::path::Path::new("data.csv")
1043 && table == "events"
1044 && delimiter == ';'
1045 && !header
1046 && compression == "zstd"
1047 && row_group_size == Some(500)
1048 ));
1049 }
1050
1051 #[test]
1052 fn test_parse_columnar_index_create() {
1053 let args = vec![
1054 "alopex",
1055 "--in-memory",
1056 "columnar",
1057 "index",
1058 "create",
1059 "--segment",
1060 "123:1",
1061 "--column",
1062 "col1",
1063 "--type",
1064 "bloom",
1065 ];
1066 let cli = Cli::try_parse_from(args).unwrap();
1067
1068 assert!(matches!(
1069 cli.command,
1070 Some(Command::Columnar {
1071 command: Some(ColumnarCommand::Index(IndexCommand::Create {
1072 segment,
1073 column,
1074 index_type,
1075 }))
1076 }) if segment == "123:1"
1077 && column == "col1"
1078 && index_type == "bloom"
1079 ));
1080 }
1081
1082 #[test]
1083 fn test_output_format_supports_streaming() {
1084 assert!(!OutputFormat::Table.supports_streaming());
1085 assert!(OutputFormat::Json.supports_streaming());
1086 assert!(OutputFormat::Jsonl.supports_streaming());
1087 assert!(OutputFormat::Csv.supports_streaming());
1088 assert!(OutputFormat::Tsv.supports_streaming());
1089 }
1090
1091 #[test]
1092 fn test_default_values() {
1093 let args = vec!["alopex", "--in-memory", "kv", "list"];
1094 let cli = Cli::try_parse_from(args).unwrap();
1095
1096 assert_eq!(cli.output_format(), OutputFormat::Table);
1097 assert!(!cli.output_is_explicit());
1098 assert_eq!(cli.thread_mode, ThreadMode::Multi);
1099 assert!(cli.limit.is_none());
1100 assert!(!cli.quiet);
1101 assert!(!cli.verbose);
1102 }
1103
1104 #[test]
1105 fn test_s3_data_dir() {
1106 let args = vec![
1107 "alopex",
1108 "--data-dir",
1109 "s3://my-bucket/prefix",
1110 "kv",
1111 "list",
1112 ];
1113 let cli = Cli::try_parse_from(args).unwrap();
1114
1115 assert_eq!(cli.data_dir, Some("s3://my-bucket/prefix".to_string()));
1116 }
1117}