use crate::Ctx;
use crate::config::{Capabilities, Config, ModeWords};
use crate::dispatch::{run, run_config};
use crate::errors;
use crate::registry::Registry;
use crate::spec::command::Command;
use xyz_rust::XyzArgs;
#[derive(XyzArgs)]
struct TArgs {
#[xyz(desc = "s")]
s: String,
}
fn th(_: &Ctx, in_: &TArgs) -> errors::Result<String> {
Ok(in_.s.clone())
}
fn test_reg(names: &[&str]) -> Registry {
let reg = Registry::new();
for n in names {
Command::new(n, th).register(®).unwrap();
}
reg
}
fn test_words() -> crate::dispatch::Words {
crate::dispatch::Words {
serve: "serve".to_string(),
http: "http".to_string(),
mcp: "mcp".to_string(),
help: "help".to_string(),
}
}
fn args(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn run_overview_forms() {
let reg = test_reg(&["a.b"]);
assert_eq!(run(®, vec![]), 0);
assert_eq!(run(®, args(&["help"])), 0);
assert_eq!(run(®, args(&["--help"])), 0);
assert_eq!(run(®, args(&["-h"])), 0);
}
#[test]
fn run_empty_registry_is_noop() {
for argv in [
vec![],
args(&["help"]),
args(&["user", "add"]),
args(&["mcp", "stdio"]),
args(&["serve"]),
] {
assert_eq!(run(&Registry::new(), argv), 0);
}
}
#[test]
fn run_custom_mode_words() {
let cfg = Config {
modes: ModeWords {
serve: "httpd".into(),
http: String::new(),
mcp: "protocol".into(),
help: "assist".into(),
},
..Default::default()
};
let reg = test_reg(&["a.b"]);
assert_eq!(run_config(®, args(&["assist"]), cfg.clone()), 0);
let mut no_http = cfg;
no_http.capabilities.no_http = true;
assert_eq!(run_config(®, args(&["httpd"]), no_http), 1);
}
#[test]
fn run_invalid_mode_words() {
let reg = test_reg(&["a.b"]);
for cfg in [
Config {
modes: ModeWords {
serve: "serve".into(),
http: String::new(),
mcp: "serve".into(),
help: String::new(),
},
..Default::default()
},
Config {
modes: ModeWords {
serve: "-serve".into(),
http: String::new(),
mcp: String::new(),
help: String::new(),
},
..Default::default()
},
Config {
modes: ModeWords {
serve: "sv c".into(),
http: String::new(),
mcp: String::new(),
help: String::new(),
},
..Default::default()
},
] {
assert_eq!(run_config(®, vec![], cfg), 2);
}
}
#[test]
fn mode_words_are_no_longer_reserved() {
for name in ["serve.x", "mcp.up", "help.me", "http.get"] {
let reg = Registry::new();
Command::new(name, th).register(®).unwrap();
}
}
#[test]
fn run_version_flag_anywhere() {
let reg = test_reg(&["a.b"]);
for argv in [
args(&["-v"]),
args(&["--version"]),
args(&["echo", "hi", "-v"]),
] {
assert_eq!(run(®, argv), 0);
}
}
#[test]
fn run_disabled_capabilities() {
let reg = test_reg(&["echo.hi"]);
let mut no_cli = Config::default();
no_cli.capabilities.no_cli = true;
assert_eq!(
run_config(®, args(&["echo", "hi", "--s", "x"]), no_cli.clone()),
1
);
assert_eq!(run_config(®, args(&["help"]), no_cli.clone()), 0);
assert_eq!(run_config(®, args(&["-v"]), no_cli.clone()), 0);
let mut no_mcp = Config::default();
no_mcp.capabilities.no_mcp = true;
assert_eq!(run_config(®, args(&["mcp", "stdio"]), no_mcp), 1);
let no_http = Config {
capabilities: Capabilities {
no_http: true,
..Default::default()
},
..Default::default()
};
assert_eq!(run_config(®, args(&["serve"]), no_http), 1);
let both = Config {
capabilities: Capabilities {
no_cli: true,
no_http: true,
no_mcp: false,
},
..Default::default()
};
assert_eq!(run_config(®, args(&["serve"]), both), 1);
}
#[test]
fn strip_xyz_flags() {
let mut cfg = Config {
bearer_tokens: vec!["code-tok".into()],
..Default::default()
};
let rest = crate::builtins::strip_xyz_flags(
args(&[
"--xyz.bearer=a,b",
"mcp",
"stdio",
"--xyz.addr=:9090",
"--xyz.bearer=b",
]),
&mut cfg,
)
.unwrap();
assert_eq!(cfg.addr, ":9090");
assert_eq!(cfg.bearer_tokens, vec!["code-tok", "a", "b"]);
assert_eq!(rest, args(&["mcp", "stdio"]));
let mut cfg2 = Config::default();
let rest2 =
crate::builtins::strip_xyz_flags(args(&["--xyz.bearer", "x,,y", "echo"]), &mut cfg2)
.unwrap();
assert_eq!(cfg2.bearer_tokens, vec!["x", "y"]);
assert_eq!(rest2, args(&["echo"]));
let mut cfg3 = Config::default();
let rest3 = crate::builtins::strip_xyz_flags(
args(&[
"serve",
"--xyz.log-level=debug",
"--xyz.timeout",
"45s",
"--xyz.tls-cert=a.pem",
"--xyz.tls-key",
"k.pem",
"--xyz.cors=x,y,z",
]),
&mut cfg3,
)
.unwrap();
assert_eq!(cfg3.log_level, crate::logx::Level::Debug);
assert_eq!(cfg3.timeout, std::time::Duration::from_secs(45));
assert_eq!(cfg3.cert_file, "a.pem");
assert_eq!(cfg3.key_file, "k.pem");
assert_eq!(cfg3.cors_origins, vec!["x", "y", "z"]);
assert_eq!(rest3, args(&["serve"]));
assert!(
crate::builtins::strip_xyz_flags(
args(&["--xyz.log-level=verbose"]),
&mut Config::default()
)
.is_err()
);
assert!(
crate::builtins::strip_xyz_flags(args(&["--xyz.timeout=nope"]), &mut Config::default())
.is_err()
);
}
#[test]
fn lang_resolution_and_catalog() {
crate::lang::set(crate::lang::XyzLang::En, None);
let reg = test_reg(&["a.b"]);
let mut buf = Vec::new();
crate::overview::print_overview(
&mut buf,
®,
&test_words(),
&Default::default(),
Capabilities::default(),
"",
"",
)
.unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("Usage (the mode is detected"), "{out}");
crate::lang::set(crate::lang::XyzLang::ZhCn, None);
let mut buf2 = Vec::new();
crate::overview::print_overview(
&mut buf2,
®,
&test_words(),
&Default::default(),
Capabilities::default(),
"",
"",
)
.unwrap();
let out2 = String::from_utf8(buf2).unwrap();
assert!(out2.contains("用法(模式由程序自动判断"), "{out2}");
assert!(out.contains("no /mcp"), "{out}");
assert!(
out.contains("detailed help for a command or a mode"),
"{out}"
);
assert!(out2.contains("不挂 /mcp"), "{out2}");
assert!(out2.contains("某命令或某模式的详细帮助"), "{out2}");
let mut ov = std::collections::HashMap::new();
ov.insert("overview.commands".to_string(), "Commands!:".to_string());
crate::lang::set(crate::lang::XyzLang::En, Some(ov));
let mut buf3 = Vec::new();
crate::overview::print_overview(
&mut buf3,
®,
&test_words(),
&Default::default(),
Capabilities::default(),
"",
"",
)
.unwrap();
assert!(String::from_utf8(buf3).unwrap().contains("Commands!:"),);
crate::lang::set(crate::lang::XyzLang::En, None);
let mut cfg = Config::default();
assert!(crate::builtins::strip_xyz_flags(args(&["--xyz.lang=fr"]), &mut cfg).is_err());
let rest =
crate::builtins::strip_xyz_flags(args(&["--xyz.lang=zh-CN", "help"]), &mut cfg).unwrap();
assert_eq!(cfg.lang, "zh-CN");
assert_eq!(rest, args(&["help"]));
}
#[test]
fn overview_help_blocks() {
let reg = test_reg(&["a.b"]);
let mut buf = Vec::new();
let before = "myapp v1.2.3 — do the thing\nhttps://github.com/me/myapp";
let after = "Need help? https://github.com/me/myapp#faq";
crate::overview::print_overview(
&mut buf,
®,
&test_words(),
&Default::default(),
Capabilities::default(),
before,
after,
)
.unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.starts_with(&format!("{before}\n")), "{out}");
assert!(out.ends_with(&format!("{after}\n")), "{out}");
let mut a = Vec::new();
let mut b = Vec::new();
crate::overview::print_overview(
&mut a,
®,
&test_words(),
&Default::default(),
Capabilities::default(),
"",
"",
)
.unwrap();
crate::overview::print_overview(
&mut b,
®,
&test_words(),
&Default::default(),
Capabilities::default(),
"",
"",
)
.unwrap();
assert_eq!(a, b);
let mut c = Vec::new();
crate::overview::print_overview(
&mut c,
&Registry::new(),
&test_words(),
&Default::default(),
Capabilities::default(),
"",
"tail",
)
.unwrap();
assert!(String::from_utf8(c).unwrap().ends_with("tail\n"));
let mut d = Vec::new();
crate::overview::print_overview(
&mut d,
®,
&test_words(),
&Default::default(),
Capabilities::default(),
"a\nb\n\n\n",
"",
)
.unwrap();
let ds = String::from_utf8(d).unwrap();
assert!(
ds.starts_with(&format!("a\nb\n{}", crate::lang::t("overview.usage_line"))),
"{ds:?}"
);
}
#[test]
fn parse_serve_args_bare_flags() {
let cfg = crate::builtins::parse_serve_args(
&args(&[
"--addr",
":9000",
"--bearer=a,b",
"--timeout=30s",
"--cors",
"x",
]),
Config::default(),
);
assert_eq!(cfg.addr, ":9000");
assert_eq!(cfg.bearer_tokens, vec!["a", "b"]);
assert_eq!(cfg.timeout, std::time::Duration::from_secs(30));
assert_eq!(cfg.cors_origins, vec!["x"]);
let cfg2 = crate::builtins::parse_serve_args(&[], Config::default());
assert_eq!(cfg2.addr, ":8080");
}
#[test]
fn shadowing_modes_and_namespaced_reachability() {
let reg = test_reg(&["serve.x", "mcp.y"]);
#[cfg(feature = "cli")]
{
assert_eq!(
run_config(®, args(&["serve", "x"]), Config::default()),
0
);
assert_eq!(run_config(®, args(&["mcp", "y"]), Config::default()), 0);
}
assert_eq!(
run_config(®, args(&["xyz.serve", "-h"]), Config::default()),
0
);
assert_eq!(
run_config(®, args(&["xyz.mcp", "-h"]), Config::default()),
0
);
assert_eq!(
run_config(®, args(&["xyz.http", "-h"]), Config::default()),
0
);
}
#[test]
fn cli_skipped_commands_do_not_shadow() {
let reg = Registry::new();
Command::new("mcp.hidden", th)
.cli(crate::spec::command::CliHints {
skip: true,
..Default::default()
})
.register(®)
.unwrap();
assert_eq!(run_config(®, args(&["mcp", "-h"]), Config::default()), 0);
}
#[test]
fn help_subcommand_family() {
let reg = test_reg(&["user.add", "search.query"]);
for argv in [
vec!["help"],
vec!["help", "serve"],
vec!["help", "http"],
vec!["help", "mcp"],
vec!["help", "help"],
] {
assert_eq!(
run_config(®, args(&argv), Config::default()),
0,
"help family: {argv:?}"
);
}
#[cfg(feature = "cli")]
for argv in [vec!["help", "user.add"], vec!["help", "user", "add"]] {
assert_eq!(
run_config(®, args(&argv), Config::default()),
0,
"help family: {argv:?}"
);
}
#[cfg(feature = "cli")]
{
let reg2 = Registry::new();
Command::new("user.add", th)
.cli(crate::spec::command::CliHints {
aliases: vec!["ua".into()],
..Default::default()
})
.register(®2)
.unwrap();
assert_eq!(
run_config(®2, args(&["help", "ua"]), Config::default()),
0
);
}
}
#[test]
fn mode_help_does_not_start_servers() {
let reg = test_reg(&["user.add"]);
for argv in [
vec!["serve", "-h"],
vec!["serve", "--help"],
vec!["http", "-h"],
vec!["mcp", "-h"],
] {
assert_eq!(
run_config(®, args(&argv), Config::default()),
0,
"mode -h: {argv:?}"
);
}
}
#[test]
fn shadowing_edges() {
let reg = test_reg(&["serve"]);
#[cfg(feature = "cli")]
assert_eq!(run_config(®, args(&["serve"]), Config::default()), 0);
assert_eq!(
run_config(®, args(&["xyz.serve", "-h"]), Config::default()),
0
);
#[cfg(feature = "cli")]
assert_eq!(
run_config(®, args(&["help", "serve"]), Config::default()),
0
);
assert_eq!(
run_config(®, args(&["help", "xyz.serve"]), Config::default()),
0
);
let cfg = Config {
modes: ModeWords {
serve: "httpd".into(),
http: String::new(),
mcp: String::new(),
help: String::new(),
},
..Default::default()
};
let reg2 = test_reg(&["httpd.x"]);
#[cfg(feature = "cli")]
assert_eq!(run_config(®2, args(&["httpd", "x"]), cfg.clone()), 0);
assert_eq!(
run_config(®2, args(&["xyz.httpd", "-h"]), cfg.clone()),
0
);
}
#[test]
fn four_mode_words_pairwise_distinct() {
let cfg = Config {
modes: ModeWords {
serve: "d".into(),
http: "d".into(),
mcp: String::new(),
help: String::new(),
},
..Default::default()
};
assert_eq!(run_config(&test_reg(&["a.b"]), vec![], cfg), 2);
let cfg2 = Config {
modes: ModeWords {
serve: String::new(),
http: String::new(),
mcp: "h".into(),
help: "h".into(),
},
..Default::default()
};
assert_eq!(run_config(&test_reg(&["a.b"]), vec![], cfg2), 2);
}
#[test]
fn mode_help_texts_cover_modes() {
let w = test_words();
let serve = crate::dispatch::mode_help_text(crate::dispatch::ModeKind::Serve, &w);
assert!(serve.contains("listen address"), "{serve}");
assert!(serve.contains("at /mcp"), "{serve}");
let http = crate::dispatch::mode_help_text(crate::dispatch::ModeKind::Http, &w);
assert!(http.contains("no /mcp endpoint"), "{http}");
let mcp = crate::dispatch::mode_help_text(crate::dispatch::ModeKind::Mcp, &w);
assert!(mcp.contains("stdio|http"), "{mcp}"); let help = crate::dispatch::mode_help_text(crate::dispatch::ModeKind::Help, &w);
assert!(
help.contains("detailed help") || help.contains("详细帮助"),
"{help}"
);
let w2 = crate::dispatch::Words {
serve: "httpd".to_string(),
http: "rest".to_string(),
mcp: "protocol".to_string(),
help: "assist".to_string(),
};
assert!(
crate::dispatch::mode_help_text(crate::dispatch::ModeKind::Serve, &w2).contains("httpd")
);
}