1mod cli;
2mod ic;
3mod icrc;
4mod nns;
5mod output;
6mod progress;
7mod sns;
8mod storage;
9mod system;
10
11#[cfg(test)]
12mod test_support;
13
14use crate::cli::clap::{
15 parse_matches_or_usage, passthrough_args, passthrough_subcommand, string_option,
16};
17use clap::{Arg, ArgAction, Command};
18use ic_query::subnet_catalog::MAINNET_NETWORK;
19use std::ffi::OsString;
20use thiserror::Error as ThisError;
21
22const TOP_LEVEL_HELP_TEMPLATE: &str = "{name} {version}\n{about-with-newline}\n{usage-heading} {usage}\n\nCommands:\n{subcommands}\n\nOptions:\n{options}{after-help}\n";
23const VERSION_TEXT: &str = concat!("icq ", env!("CARGO_PKG_VERSION"));
24const INTERNAL_NETWORK_OPTION: &str = "--__icq-network";
25
26const fn version_text() -> &'static str {
27 VERSION_TEXT
28}
29
30#[derive(Debug, ThisError)]
37pub enum IcqCliError {
38 #[error("{0}")]
39 Usage(String),
40
41 #[error("nns: {0}")]
42 Nns(#[from] nns::NnsCommandError),
43
44 #[error("icrc: {0}")]
45 Icrc(#[from] icrc::IcrcCommandError),
46
47 #[error("ic: {0}")]
48 Ic(#[from] ic::IcCommandError),
49
50 #[error("sns: {0}")]
51 Sns(#[from] sns::SnsCommandError),
52
53 #[error("system: {0}")]
54 System(#[from] system::SystemCommandError),
55}
56
57impl IcqCliError {
58 #[must_use]
60 pub fn is_broken_pipe(&self) -> bool {
61 match self {
62 Self::Ic(ic::IcCommandError::Io(err))
63 | Self::Nns(nns::NnsCommandError::Io(err))
64 | Self::Icrc(icrc::IcrcCommandError::Io(err))
65 | Self::Sns(sns::SnsCommandError::Io(err))
66 | Self::System(system::SystemCommandError::Io(err)) => {
67 err.kind() == std::io::ErrorKind::BrokenPipe
68 }
69 Self::Usage(_)
70 | Self::Nns(_)
71 | Self::Icrc(_)
72 | Self::Ic(_)
73 | Self::Sns(_)
74 | Self::System(_) => false,
75 }
76 }
77
78 #[must_use]
80 pub const fn exit_code(&self) -> i32 {
81 match self {
82 Self::Usage(_)
83 | Self::Ic(ic::IcCommandError::Usage(_))
84 | Self::Nns(nns::NnsCommandError::Usage(_))
85 | Self::Icrc(icrc::IcrcCommandError::Usage(_))
86 | Self::Sns(sns::SnsCommandError::Usage(_))
87 | Self::System(system::SystemCommandError::Usage(_)) => 2,
88 Self::Nns(_) | Self::Icrc(_) | Self::Ic(_) | Self::Sns(_) | Self::System(_) => 1,
89 }
90 }
91}
92
93pub fn run_from_env() -> Result<(), IcqCliError> {
95 run(std::env::args_os().skip(1))
96}
97
98pub fn run<I>(args: I) -> Result<(), IcqCliError>
100where
101 I: IntoIterator<Item = OsString>,
102{
103 let Some(args) = collect_args_or_print_help(args, usage) else {
104 return Ok(());
105 };
106 if let Some((command, option)) = command_local_global_option(&args) {
107 if matches!(command, "ic" | "icrc") {
108 return Err(unsupported_global_network_error(command));
109 }
110 return Err(IcqCliError::Usage(format!(
111 "{option} is a top-level option; put it before the command\n\n{}",
112 usage()
113 )));
114 }
115
116 let matches = parse_matches_or_usage(top_level_dispatch_command(), args, usage)
117 .map_err(IcqCliError::Usage)?;
118 if matches.get_flag("version") {
119 println!("{VERSION_TEXT}");
120 return Ok(());
121 }
122 let global_network = string_option(&matches, "network");
123
124 let Some((command, subcommand_matches)) = matches.subcommand() else {
125 return Err(IcqCliError::Usage(usage()));
126 };
127 let mut tail = passthrough_args(subcommand_matches);
128 apply_global_network(command, &mut tail, global_network)?;
129 let tail = tail.into_iter();
130
131 match command {
132 "ic" => Ok(ic::run(tail)?),
133 "icrc" => Ok(icrc::run(tail)?),
134 "nns" => Ok(nns::run(tail)?),
135 "sns" => Ok(sns::run(tail)?),
136 "system" => Ok(system::run(tail)?),
137 _ => unreachable!("top-level dispatch command only defines known commands"),
138 }
139}
140
141fn collect_args_or_print_help<I>(args: I, usage: impl FnOnce() -> String) -> Option<Vec<OsString>>
142where
143 I: IntoIterator<Item = OsString>,
144{
145 let args = args.into_iter().collect::<Vec<_>>();
146 if top_level_help_requested(&args) {
147 println!("{}", usage());
148 return None;
149 }
150 Some(args)
151}
152
153fn top_level_help_requested(args: &[OsString]) -> bool {
154 let mut index = 0;
155 while index < args.len() {
156 let Some(arg) = args[index].to_str() else {
157 return false;
158 };
159 if command_family(arg).is_some() {
160 return false;
161 }
162 if matches!(arg, "help" | "--help" | "-h") {
163 return true;
164 }
165 index += if arg == "--network" { 2 } else { 1 };
166 }
167 false
168}
169
170fn network_arg() -> Arg {
171 Arg::new("network")
172 .num_args(1)
173 .long("network")
174 .value_name("name")
175 .help("Network identity for NNS, SNS, and system commands; currently only ic")
176}
177
178fn top_level_command() -> Command {
179 Command::new("icq")
180 .version(env!("CARGO_PKG_VERSION"))
181 .about("Internet Computer metadata query CLI")
182 .disable_help_subcommand(true)
183 .disable_version_flag(true)
184 .arg(
185 Arg::new("version")
186 .short('V')
187 .long("version")
188 .action(ArgAction::SetTrue)
189 .help("Print version"),
190 )
191 .arg(network_arg().global(true))
192 .subcommand_help_heading("Commands")
193 .help_template(TOP_LEVEL_HELP_TEMPLATE)
194 .after_help("Run `icq <command> help` for command-specific help.")
195 .subcommands(
196 COMMAND_FAMILIES
197 .iter()
198 .map(|family| Command::new(family.name).about(family.about)),
199 )
200}
201
202fn top_level_dispatch_command() -> Command {
203 let command = Command::new("icq")
204 .disable_help_flag(true)
205 .disable_help_subcommand(true)
206 .disable_version_flag(true)
207 .arg(
208 Arg::new("version")
209 .short('V')
210 .long("version")
211 .action(ArgAction::SetTrue),
212 )
213 .arg(network_arg().global(true));
214
215 COMMAND_FAMILIES.iter().fold(command, |command, family| {
216 command.subcommand(passthrough_subcommand(
217 Command::new(family.name).about(family.about),
218 ))
219 })
220}
221
222fn usage() -> String {
223 let mut command = top_level_command();
224 command.render_help().to_string()
225}
226
227fn command_local_global_option(args: &[OsString]) -> Option<(&'static str, &'static str)> {
228 let mut index = 0;
229 while index < args.len() {
230 let arg = args[index].to_str()?;
231 if let Some(family) = command_family(arg) {
232 return args[index + 1..]
233 .iter()
234 .filter_map(|arg| arg.to_str())
235 .find_map(global_option_name)
236 .map(|option| (family.name, option));
237 }
238 index += if arg == "--network" { 2 } else { 1 };
239 }
240 None
241}
242
243fn global_option_name(arg: &str) -> Option<&'static str> {
244 match arg {
245 "--network" => Some("--network"),
246 _ if arg.starts_with("--network=") => Some("--network"),
247 _ => None,
248 }
249}
250
251fn apply_global_network(
252 command: &str,
253 tail: &mut Vec<OsString>,
254 global_network: Option<String>,
255) -> Result<(), IcqCliError> {
256 let Some(global_network) = global_network else {
257 return Ok(());
258 };
259 if tail_requests_help_or_version(tail) {
260 return Ok(());
261 }
262 if !command_accepts_global_network(command, tail) {
263 return Err(unsupported_global_network_error(command));
264 }
265 if global_network != MAINNET_NETWORK {
266 return Err(unsupported_mainnet_network_error(command, &global_network));
267 }
268 if tail_has_option(tail, INTERNAL_NETWORK_OPTION) {
269 return Ok(());
270 }
271
272 tail.push(OsString::from(INTERNAL_NETWORK_OPTION));
273 tail.push(OsString::from(global_network));
274 Ok(())
275}
276
277fn unsupported_global_network_error(command: &str) -> IcqCliError {
278 let guidance = if matches!(command, "ic" | "icrc") {
279 " use the command's --source-endpoint option to select its API endpoint"
280 } else {
281 ""
282 };
283 IcqCliError::Usage(format!(
284 "--network is not supported by `icq {command}`;{guidance}\n\n{}",
285 usage()
286 ))
287}
288
289fn unsupported_mainnet_network_error(command: &str, network: &str) -> IcqCliError {
290 IcqCliError::Usage(format!(
291 "`icq {command}` currently supports only the mainnet `{MAINNET_NETWORK}` network; received `{network}`\n\n{}",
292 usage()
293 ))
294}
295
296fn command_accepts_global_network(command: &str, tail: &[OsString]) -> bool {
297 command_family(command).is_some_and(|family| (family.accepts_global_network)(tail))
298}
299
300fn tail_has_option(tail: &[OsString], name: &str) -> bool {
301 tail.iter().any(|arg| arg.to_str() == Some(name))
302}
303
304fn tail_requests_help_or_version(tail: &[OsString]) -> bool {
305 tail.iter()
306 .filter_map(|arg| arg.to_str())
307 .any(|arg| matches!(arg, "help" | "--help" | "-h" | "--version" | "-V"))
308}
309
310#[derive(Clone, Copy, Debug)]
311struct CommandFamily {
312 name: &'static str,
313 about: &'static str,
314 accepts_global_network: fn(&[OsString]) -> bool,
315}
316
317const COMMAND_FAMILIES: &[CommandFamily] = &[
318 CommandFamily {
319 name: "ic",
320 about: "Inspect official IC Dashboard metadata",
321 accepts_global_network: ic_accepts_global_network,
322 },
323 CommandFamily {
324 name: "icrc",
325 about: "Inspect generic ICRC ledger and account metadata",
326 accepts_global_network: icrc_accepts_global_network,
327 },
328 CommandFamily {
329 name: "nns",
330 about: "Inspect NNS metadata",
331 accepts_global_network: nns_accepts_global_network,
332 },
333 CommandFamily {
334 name: "sns",
335 about: "Inspect SNS metadata",
336 accepts_global_network: sns_accepts_global_network,
337 },
338 CommandFamily {
339 name: "system",
340 about: "Inspect native IC system-canister metadata",
341 accepts_global_network: system_accepts_global_network,
342 },
343];
344
345fn command_family(name: &str) -> Option<&'static CommandFamily> {
346 COMMAND_FAMILIES.iter().find(|family| family.name == name)
347}
348
349const fn nns_accepts_global_network(_tail: &[OsString]) -> bool {
350 true
351}
352
353const fn ic_accepts_global_network(_tail: &[OsString]) -> bool {
354 false
355}
356
357const fn icrc_accepts_global_network(_tail: &[OsString]) -> bool {
358 false
359}
360
361const fn sns_accepts_global_network(_tail: &[OsString]) -> bool {
362 true
363}
364
365const fn system_accepts_global_network(_tail: &[OsString]) -> bool {
366 true
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn usage_lists_query_families() {
375 let text = usage();
376
377 assert!(text.contains("Usage: icq [OPTIONS] [COMMAND]"));
378 assert!(text.contains("ic"));
379 assert!(text.contains("Inspect official IC Dashboard metadata"));
380 assert!(text.contains("icrc"));
381 assert!(text.contains("Inspect generic ICRC ledger and account metadata"));
382 assert!(text.contains("nns"));
383 assert!(text.contains("Inspect NNS metadata"));
384 assert!(text.contains("sns"));
385 assert!(text.contains("Inspect SNS metadata"));
386 assert!(text.contains("system"));
387 assert!(text.contains("Inspect native IC system-canister metadata"));
388 assert!(text.contains("Run `icq <command> help`"));
389 }
390
391 #[test]
392 fn top_level_usage_snapshot() {
393 let expected = format!(
394 "\
395icq {}
396Internet Computer metadata query CLI
397
398Usage: icq [OPTIONS] [COMMAND]
399
400Commands:
401 ic Inspect official IC Dashboard metadata
402 icrc Inspect generic ICRC ledger and account metadata
403 nns Inspect NNS metadata
404 sns Inspect SNS metadata
405 system Inspect native IC system-canister metadata
406
407Options:
408 -V, --version Print version
409 --network <name> Network identity for NNS, SNS, and system commands; currently only ic
410 -h, --help Print help
411
412Run `icq <command> help` for command-specific help.
413",
414 env!("CARGO_PKG_VERSION")
415 );
416
417 assert_eq!(usage(), expected);
418 }
419
420 #[test]
421 fn command_family_help_returns_ok() {
422 for args in [
423 &["ic", "help"][..],
424 &["ic", "canister", "help"],
425 &["ic", "canister", "info", "help"],
426 &["icrc", "help"],
427 &["icrc", "ledger", "help"],
428 &["icrc", "ledger", "token", "help"],
429 &["icrc", "account", "help"],
430 &["icrc", "account", "balance", "help"],
431 &["icrc", "account", "allowance", "help"],
432 &["icrc", "account", "transaction", "help"],
433 &["icrc", "account", "transaction", "page", "help"],
434 &["icrc", "account", "transaction", "list", "help"],
435 &["icrc", "account", "transaction", "refresh", "help"],
436 &["icrc", "account", "transaction", "cache", "help"],
437 &["icrc", "account", "transaction", "cache", "status", "help"],
438 &["icrc", "ledger", "index", "help"],
439 &["nns", "help"][..],
440 &["nns", "data-center", "help"],
441 &["nns", "data-center", "list", "help"],
442 &["nns", "data-center", "info", "help"],
443 &["nns", "data-center", "refresh", "help"],
444 &["nns", "node", "help"],
445 &["nns", "node", "list", "help"],
446 &["nns", "node", "info", "help"],
447 &["nns", "node", "refresh", "help"],
448 &["nns", "node-provider", "help"],
449 &["nns", "node-provider", "list", "help"],
450 &["nns", "node-provider", "info", "help"],
451 &["nns", "node-provider", "refresh", "help"],
452 &["nns", "node-operator", "help"],
453 &["nns", "node-operator", "list", "help"],
454 &["nns", "node-operator", "info", "help"],
455 &["nns", "node-operator", "refresh", "help"],
456 &["nns", "proposal", "help"],
457 &["nns", "proposal", "list", "help"],
458 &["nns", "proposal", "info", "help"],
459 &["nns", "registry", "help"],
460 &["nns", "registry", "version", "help"],
461 &["nns", "subnet", "help"],
462 &["nns", "subnet", "list", "help"],
463 &["nns", "subnet", "info", "help"],
464 &["nns", "subnet", "refresh", "help"],
465 &["nns", "topology", "help"],
466 &["nns", "topology", "summary", "help"],
467 &["nns", "topology", "coverage", "help"],
468 &["nns", "topology", "versions", "help"],
469 &["nns", "topology", "health", "help"],
470 &["nns", "topology", "gaps", "help"],
471 &["nns", "topology", "capacity", "help"],
472 &["nns", "topology", "regions", "help"],
473 &["nns", "topology", "providers", "help"],
474 &["nns", "topology", "refresh", "help"],
475 &["sns", "help"],
476 &["sns", "list", "help"],
477 &["sns", "info", "help"],
478 &["sns", "token", "help"],
479 &["sns", "params", "help"],
480 &["sns", "proposal", "help"],
481 &["sns", "proposal", "list", "help"],
482 &["sns", "proposal", "info", "help"],
483 &["sns", "proposal", "cache", "help"],
484 &["sns", "proposal", "cache", "list", "help"],
485 &["sns", "proposal", "cache", "status", "help"],
486 &["sns", "proposal", "refresh", "help"],
487 &["sns", "neuron", "help"],
488 &["sns", "neuron", "list", "help"],
489 &["sns", "neuron", "cache", "help"],
490 &["sns", "neuron", "cache", "list", "help"],
491 &["sns", "neuron", "cache", "status", "help"],
492 &["sns", "neuron", "refresh", "help"],
493 &["system", "help"],
494 &["system", "xdr", "help"],
495 &["system", "cycles", "help"],
496 ] {
497 assert_run_ok(args);
498 }
499 }
500
501 #[test]
502 fn version_flags_return_ok() {
503 assert_eq!(VERSION_TEXT, concat!("icq ", env!("CARGO_PKG_VERSION")));
504 assert!(run([OsString::from("--version")]).is_ok());
505 assert!(run([OsString::from("ic"), OsString::from("--version")]).is_ok());
506 assert!(run([OsString::from("icrc"), OsString::from("--version")]).is_ok());
507 assert!(run([OsString::from("nns"), OsString::from("--version")]).is_ok());
508 assert!(run([OsString::from("sns"), OsString::from("--version")]).is_ok());
509 assert!(run([OsString::from("system"), OsString::from("--version")]).is_ok());
510 assert!(
511 run([
512 OsString::from("nns"),
513 OsString::from("subnet"),
514 OsString::from("list"),
515 OsString::from("--version")
516 ])
517 .is_ok()
518 );
519
520 let mut sns_info_tail = vec![OsString::from("info"), OsString::from("1")];
521
522 apply_global_network("sns", &mut sns_info_tail, Some("ic".to_string()))
523 .expect("SNS supports global network");
524
525 assert_eq!(
526 sns_info_tail,
527 vec![
528 OsString::from("info"),
529 OsString::from("1"),
530 OsString::from(INTERNAL_NETWORK_OPTION),
531 OsString::from("ic")
532 ]
533 );
534 }
535
536 #[test]
537 fn typed_cli_errors_preserve_exit_and_broken_pipe_semantics() {
538 for usage in [
539 IcqCliError::Ic(ic::IcCommandError::Usage("bad input".to_string())),
540 IcqCliError::Icrc(icrc::IcrcCommandError::Usage("bad input".to_string())),
541 IcqCliError::System(system::SystemCommandError::Usage("bad input".to_string())),
542 ] {
543 assert_eq!(usage.exit_code(), 2);
544 assert!(!usage.is_broken_pipe());
545 }
546
547 for broken_pipe in [
548 IcqCliError::Ic(ic::IcCommandError::Io(std::io::Error::from(
549 std::io::ErrorKind::BrokenPipe,
550 ))),
551 IcqCliError::Icrc(icrc::IcrcCommandError::Io(std::io::Error::from(
552 std::io::ErrorKind::BrokenPipe,
553 ))),
554 IcqCliError::System(system::SystemCommandError::Io(std::io::Error::from(
555 std::io::ErrorKind::BrokenPipe,
556 ))),
557 ] {
558 assert_eq!(broken_pipe.exit_code(), 1);
559 assert!(broken_pipe.is_broken_pipe());
560 }
561 }
562
563 #[test]
564 fn global_network_is_forwarded_to_networked_leaf_commands() {
565 for (command, leaf) in [
566 ("nns", "data-center"),
567 ("nns", "governance"),
568 ("nns", "neuron"),
569 ("nns", "node"),
570 ("nns", "node-operator"),
571 ("nns", "node-provider"),
572 ("nns", "proposal"),
573 ("nns", "registry"),
574 ("nns", "subnet"),
575 ("nns", "topology"),
576 ("sns", "canister"),
577 ("sns", "info"),
578 ("sns", "list"),
579 ("sns", "neuron"),
580 ("sns", "params"),
581 ("sns", "proposal"),
582 ("sns", "token"),
583 ("system", "xdr"),
584 ] {
585 let mut tail = vec![OsString::from(leaf), OsString::from("list")];
586
587 apply_global_network(command, &mut tail, Some("ic".to_string()))
588 .expect("NNS, SNS, and system families support the global network");
589
590 assert_eq!(
591 tail,
592 vec![
593 OsString::from(leaf),
594 OsString::from("list"),
595 OsString::from(INTERNAL_NETWORK_OPTION),
596 OsString::from("ic")
597 ]
598 );
599 }
600 }
601
602 #[test]
603 fn non_mainnet_network_is_rejected_before_mainnet_only_dispatch() {
604 for command in ["nns", "sns", "system"] {
605 let mut tail = if command == "nns" {
606 vec![OsString::from("proposal"), OsString::from("list")]
607 } else {
608 vec![OsString::from("list")]
609 };
610
611 let error = apply_global_network(command, &mut tail, Some("local".to_string()))
612 .expect_err("current mainnet canister adapters reject other networks");
613
614 assert_eq!(error.exit_code(), 2);
615 assert!(error.to_string().contains("supports only the mainnet `ic`"));
616 assert!(error.to_string().contains("received `local`"));
617 assert!(!tail_has_option(&tail, INTERNAL_NETWORK_OPTION));
618 }
619
620 let mut preforwarded_tail = vec![
621 OsString::from("proposal"),
622 OsString::from("list"),
623 OsString::from(INTERNAL_NETWORK_OPTION),
624 OsString::from(MAINNET_NETWORK),
625 ];
626 let error = apply_global_network("nns", &mut preforwarded_tail, Some("local".to_string()))
627 .expect_err("an internal forwarded value must not bypass global validation");
628 assert!(error.to_string().contains("received `local`"));
629
630 for args in [
631 vec![
632 OsString::from("--network"),
633 OsString::from("local"),
634 OsString::from("nns"),
635 OsString::from("proposal"),
636 OsString::from("list"),
637 ],
638 vec![
639 OsString::from("--network"),
640 OsString::from("local"),
641 OsString::from("nns"),
642 OsString::from("governance"),
643 OsString::from("economics"),
644 ],
645 vec![
646 OsString::from("--network"),
647 OsString::from("local"),
648 OsString::from("nns"),
649 OsString::from("neuron"),
650 OsString::from("list"),
651 ],
652 vec![
653 OsString::from("--network"),
654 OsString::from("local"),
655 OsString::from("sns"),
656 OsString::from("list"),
657 ],
658 vec![
659 OsString::from("--network"),
660 OsString::from("local"),
661 OsString::from("sns"),
662 OsString::from("canister"),
663 OsString::from("list"),
664 OsString::from("1"),
665 ],
666 vec![
667 OsString::from("--network"),
668 OsString::from("local"),
669 OsString::from("system"),
670 OsString::from("xdr"),
671 ],
672 ] {
673 let command = args[2].to_string_lossy().into_owned();
674 let error = run(args).expect_err("non-mainnet network must fail before dispatch");
675
676 assert_eq!(error.exit_code(), 2);
677 assert!(
678 error
679 .to_string()
680 .starts_with(&format!("`icq {command}` currently"))
681 );
682 }
683 }
684
685 #[test]
686 fn global_network_is_rejected_when_the_family_uses_endpoint_identity() {
687 for (command, mut tail) in [
688 (
689 "ic",
690 vec![OsString::from("canister"), OsString::from("info")],
691 ),
692 (
693 "icrc",
694 vec![OsString::from("ledger"), OsString::from("token")],
695 ),
696 ] {
697 let original = tail.clone();
698 let error = apply_global_network(command, &mut tail, Some("ic".to_string()))
699 .expect_err("endpoint-identified family must reject global network");
700
701 assert_eq!(error.exit_code(), 2);
702 assert!(error.to_string().contains("--network is not supported"));
703 assert!(error.to_string().contains(&format!("icq {command}")));
704 assert!(error.to_string().contains("--source-endpoint"));
705 assert_eq!(tail, original);
706 }
707
708 let error = run([
709 OsString::from("--network"),
710 OsString::from("ic"),
711 OsString::from("icrc"),
712 OsString::from("ledger"),
713 OsString::from("token"),
714 OsString::from("ryjl3-tyaaa-aaaaa-aaaba-cai"),
715 ])
716 .expect_err("ICRC global network must fail before dispatch");
717
718 assert_eq!(error.exit_code(), 2);
719 assert!(error.to_string().contains("--source-endpoint"));
720
721 let error = run([
722 OsString::from("icrc"),
723 OsString::from("ledger"),
724 OsString::from("token"),
725 OsString::from("ryjl3-tyaaa-aaaaa-aaaba-cai"),
726 OsString::from("--network"),
727 OsString::from("ic"),
728 ])
729 .expect_err("command-local ICRC network must use the same rejection");
730
731 assert_eq!(error.exit_code(), 2);
732 assert!(error.to_string().contains("--network is not supported"));
733 assert!(!error.to_string().contains("put it before the command"));
734
735 assert!(
736 run([
737 OsString::from("--network"),
738 OsString::from("ic"),
739 OsString::from("icrc"),
740 OsString::from("ledger"),
741 OsString::from("token"),
742 OsString::from("help"),
743 ])
744 .is_ok(),
745 "help must remain available without dispatching a query"
746 );
747
748 let error = run([
749 OsString::from("--network"),
750 OsString::from("ic"),
751 OsString::from("ic"),
752 OsString::from("canister"),
753 OsString::from("info"),
754 OsString::from("ryjl3-tyaaa-aaaaa-aaaba-cai"),
755 ])
756 .expect_err("Dashboard family global network must fail before dispatch");
757
758 assert_eq!(error.exit_code(), 2);
759 assert!(error.to_string().contains("icq ic"));
760 assert!(error.to_string().contains("--source-endpoint"));
761
762 let error = run([
763 OsString::from("ic"),
764 OsString::from("canister"),
765 OsString::from("info"),
766 OsString::from("ryjl3-tyaaa-aaaaa-aaaba-cai"),
767 OsString::from("--network"),
768 OsString::from("ic"),
769 ])
770 .expect_err("command-local Dashboard network must use the same rejection");
771
772 assert_eq!(error.exit_code(), 2);
773 assert!(error.to_string().contains("--network is not supported"));
774 assert!(!error.to_string().contains("put it before the command"));
775 }
776
777 #[test]
778 fn malformed_source_endpoint_returns_typed_error_without_network_io() {
779 let error = run([
780 OsString::from("icrc"),
781 OsString::from("ledger"),
782 OsString::from("token"),
783 OsString::from("ryjl3-tyaaa-aaaaa-aaaba-cai"),
784 OsString::from("--source-endpoint"),
785 OsString::from(":::"),
786 ])
787 .expect_err("malformed endpoint must return an error");
788
789 assert_eq!(error.exit_code(), 1);
790 assert!(error.to_string().contains("failed to build IC agent"));
791 assert!(error.to_string().contains(":::"));
792 }
793
794 #[test]
795 fn sns_nested_commands_dispatch_through_clap_subcommands() {
796 assert!(
797 run([
798 OsString::from("sns"),
799 OsString::from("neuron"),
800 OsString::from("refresh"),
801 OsString::from("--help")
802 ])
803 .is_ok()
804 );
805 assert!(
806 run([
807 OsString::from("sns"),
808 OsString::from("proposal"),
809 OsString::from("cache"),
810 OsString::from("status"),
811 OsString::from("--help")
812 ])
813 .is_ok()
814 );
815 }
816
817 fn assert_run_ok(args: &[&str]) {
818 let args = args.iter().copied().map(OsString::from).collect::<Vec<_>>();
819 if let Err(err) = run(args.clone()) {
820 panic!("expected {args:?} to succeed, got {err}");
821 }
822 }
823}