use brontes::Config;
use clap::{Arg, Command};
#[test]
fn deep_nesting_no_stack_overflow() {
let mut leaf = Command::new("c10");
for i in (0..10).rev() {
leaf = Command::new(format!("c{i}")).subcommand(leaf);
}
let root = leaf;
let tools = brontes::generate_tools(&root, &Config::default())
.expect("deep tree must walk and render without error");
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
let deepest = "c0_c1_c2_c3_c4_c5_c6_c7_c8_c9_c10";
assert!(
names.contains(&deepest),
"expected deepest tool {deepest:?} in tool list, got: {names:?}"
);
}
#[test]
fn wide_flat_tree_renders_all_tools() {
let mut root = Command::new("wide").subcommand_required(true);
for i in 0..100 {
root = root.subcommand(Command::new(format!("leaf{i:03}")));
}
let tools = brontes::generate_tools(&root, &Config::default())
.expect("wide tree must render without error");
assert_eq!(
tools.len(),
100,
"expected 100 leaf tools (group-only root filtered), got {}",
tools.len()
);
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
for sample in ["wide_leaf000", "wide_leaf050", "wide_leaf099"] {
assert!(
names.contains(&sample),
"expected {sample:?} in tool list (first 5 names: {:?})",
&names[..names.len().min(5)]
);
}
}
#[test]
fn no_args_command_has_empty_flags_schema() {
let root = Command::new("noargs")
.subcommand_required(true)
.subcommand(Command::new("bare"));
let tools = brontes::generate_tools(&root, &Config::default())
.expect("no-args leaf must render without error");
let bare = tools
.iter()
.find(|t| t.name.as_ref() == "noargs_bare")
.expect("noargs_bare tool must be present");
let flags_props = bare
.input_schema
.get("properties")
.and_then(serde_json::Value::as_object)
.and_then(|p| p.get("flags"))
.and_then(serde_json::Value::as_object)
.and_then(|f| f.get("properties"))
.and_then(serde_json::Value::as_object)
.expect("flags.properties must be an object for a no-args leaf");
let keys: Vec<&str> = flags_props.keys().map(String::as_str).collect();
assert!(
keys.is_empty(),
"no-args leaf must expose an empty flags.properties map (clap's `help` filtered), got: {keys:?}"
);
let flags_obj = bare
.input_schema
.get("properties")
.and_then(serde_json::Value::as_object)
.and_then(|p| p.get("flags"))
.and_then(serde_json::Value::as_object)
.expect("flags must be an object");
assert_eq!(
flags_obj.get("additionalProperties"),
Some(&serde_json::Value::Bool(false)),
"no-args leaf flags must keep additionalProperties: false"
);
}
#[test]
fn positional_only_command_renders_args_schema() {
let root = Command::new("posonly")
.subcommand_required(true)
.subcommand(Command::new("touch").arg(Arg::new("path").required(true)));
let tools = brontes::generate_tools(&root, &Config::default())
.expect("positional-only leaf must render without error");
let touch = tools
.iter()
.find(|t| t.name.as_ref() == "posonly_touch")
.expect("posonly_touch tool must be present");
let args_obj = touch
.input_schema
.get("properties")
.and_then(serde_json::Value::as_object)
.and_then(|p| p.get("args"))
.and_then(serde_json::Value::as_object)
.expect("properties.args must be an object");
let args_desc = args_obj
.get("description")
.and_then(serde_json::Value::as_str)
.expect("args.description must be a string");
assert!(
args_desc.starts_with("Positional command line arguments"),
"args.description must start with the canonical phrase, got: {args_desc:?}"
);
assert!(
args_desc.contains("Usage pattern:") && args_desc.contains("<path>"),
"args.description must carry the `<path>` usage pattern, got: {args_desc:?}"
);
assert_eq!(
args_obj.get("type"),
Some(&serde_json::Value::String("array".into())),
"args must remain `type: array`"
);
let items = args_obj
.get("items")
.and_then(serde_json::Value::as_object)
.expect("args.items must be an object");
assert_eq!(
items.get("type"),
Some(&serde_json::Value::String("string".into())),
"args.items.type must be string"
);
let flags_obj = touch
.input_schema
.get("properties")
.and_then(serde_json::Value::as_object)
.and_then(|p| p.get("flags"))
.and_then(serde_json::Value::as_object)
.expect("properties.flags must be an object");
let flags_props = flags_obj
.get("properties")
.and_then(serde_json::Value::as_object)
.expect("flags.properties must be an object");
assert!(
!flags_props.contains_key("path"),
"positional `path` must NOT appear in flags.properties, got keys: {:?}",
flags_props.keys().collect::<Vec<_>>()
);
assert!(
flags_obj.get("required").is_none(),
"flags.required must be absent when no flags are required, got: {:?}",
flags_obj.get("required")
);
}
#[test]
fn empty_prefix_falls_back_to_root_name() {
let root = Command::new("mycli")
.subcommand_required(true)
.subcommand(Command::new("op"));
let cfg = Config::default().tool_name_prefix("");
let tools = brontes::generate_tools(&root, &cfg)
.expect("empty prefix must validate and fall back to the root name");
let names: Vec<&str> = tools.iter().map(|t| t.name.as_ref()).collect();
assert!(
names.contains(&"mycli_op"),
"expected leaf tool `mycli_op` after empty-prefix fallback, got: {names:?}"
);
assert!(
!names.iter().any(|n| n.starts_with('_')),
"no tool name may start with `_` (empty prefix must NOT be substituted verbatim), got: {names:?}"
);
}