use crate::cli::{CliArgInfo, CliArgType, CliBuilder, CliCommandRegistration};
use std::any::Any;
use std::sync::Arc;
inventory::submit!(CliCommandRegistration::new(
"builder_test_command",
"v1",
"Test command for builder_tests",
"builder_test_handler"
)
.with_args(&[
CliArgInfo::new("id", "Resource ID", CliArgType::Path, true, None),
CliArgInfo::new("limit", "Max results", CliArgType::Body, false, Some("10"),),
CliArgInfo::new("state", "App state", CliArgType::State, true, None),
]));
#[test]
fn test_builder_new_is_empty() {
let _from_new = CliBuilder::new();
let _from_default = CliBuilder::default();
let cmd_from_new = CliBuilder::new().build();
let cmd_from_default = CliBuilder::default().build();
assert_eq!(cmd_from_new.get_name(), cmd_from_default.get_name());
}
#[test]
fn test_builder_collects_commands() {
let cmd = CliBuilder::new().build();
let sub = cmd
.find_subcommand("builder_test_command")
.expect("builder_test_command must be a subcommand");
let about_str = sub.get_about().map(|s| s.to_string()).unwrap_or_default();
assert_eq!(about_str, "Test command for builder_tests");
let positionals: Vec<_> = sub.get_positionals().collect();
assert_eq!(
positionals.len(),
1,
"expected exactly 1 positional (id); State must be dropped, Body is an option"
);
assert_eq!(positionals[0].get_id().as_str(), "id");
let limit_arg = sub
.get_arguments()
.find(|a| a.get_id().as_str() == "limit")
.expect("limit option must be present");
assert!(
limit_arg.get_long().is_some(),
"Body arg `limit` must have a --long flag"
);
let defaults: Vec<String> = limit_arg
.get_default_values()
.iter()
.map(|v| v.to_string_lossy().into_owned())
.collect();
assert_eq!(
defaults,
vec!["10".to_string()],
"limit default must match CliArgInfo.default"
);
let state_present = sub.get_arguments().any(|a| a.get_id().as_str() == "state");
assert!(!state_present, "State arg must not be surfaced on the CLI");
}
struct TestAppState {
counter: u32,
label: &'static str,
}
#[test]
fn test_builder_with_dependencies_injects_state() {
let empty = CliBuilder::new();
assert!(
empty.state().is_none(),
"CliBuilder::new() must start with no state"
);
let state: Arc<dyn Any + Send + Sync> = Arc::new(TestAppState {
counter: 42,
label: "test",
});
let builder = CliBuilder::with_dependencies(state);
let state_ref = builder
.state()
.expect("state must be present after with_dependencies");
let typed = state_ref
.downcast_ref::<TestAppState>()
.expect("downcast to TestAppState must succeed");
assert_eq!(typed.counter, 42);
assert_eq!(typed.label, "test");
let _cmd = builder.build();
}
#[cfg(feature = "docs")]
#[test]
fn test_builder_includes_docs_subcommand() {
let cmd = CliBuilder::new().build();
let docs_sub = cmd
.find_subcommand("docs")
.expect("docs feature 启用时 build() 必须包含 docs 子命令");
let about_str = docs_sub
.get_about()
.map(|s| s.to_string())
.unwrap_or_default();
assert!(
about_str.contains("documentation") || about_str.contains("文档"),
"docs 子命令 about 必须包含文档描述,实际: {}",
about_str
);
assert!(
docs_sub
.get_arguments()
.any(|a| a.get_id().as_str() == "format"),
"注入的 docs 子命令必须含 --format 参数"
);
assert!(
docs_sub
.get_arguments()
.any(|a| a.get_id().as_str() == "output"),
"注入的 docs 子命令必须含 --output 参数"
);
}
#[cfg(not(feature = "docs"))]
#[test]
fn test_builder_excludes_docs_subcommand_when_docs_disabled() {
let cmd = CliBuilder::new().build();
assert!(
cmd.find_subcommand("docs").is_none(),
"docs feature 未启用时不应注入 docs 子命令"
);
}