use std::fs;
use std::io::{self, BufRead, IsTerminal, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use anyhow::{bail, Context, Result};
use crate::config::manager::ConfigManager;
use crate::model::loader::Models;
use super::dispatch::OutputOpts;
pub const DEFAULT_DIR: &str = "./milvus-standalone";
pub const DEFAULT_SCRIPT_URL: &str =
"https://raw.githubusercontent.com/milvus-io/milvus/master/scripts/standalone_embed.sh";
pub const SCRIPT_FILENAME: &str = "standalone_embed.sh";
const ENDPOINT_MILVUS: &str = "localhost:19530";
const ENDPOINT_WEBUI: &str = "http://localhost:9091";
const ENDPOINT_ETCD: &str = "localhost:2379";
const ACTIONS: &[&str] = &["install", "start", "stop", "restart", "delete", "upgrade"];
pub async fn run_from_args(
_models: &Models,
_config_mgr: &ConfigManager,
raw_args: &[String],
_output_opts: &OutputOpts<'_>,
) -> Result<()> {
let sub = raw_args.first().map(|s| s.as_str()).unwrap_or("");
if sub.is_empty() || sub == "--help" || sub == "-h" {
print_milvus_help();
return Ok(());
}
if sub != "standalone" {
bail!("Unknown milvus operation '{}'. Available: standalone", sub);
}
let action_arg = raw_args.get(1).map(|s| s.as_str()).unwrap_or("");
if action_arg.is_empty() || action_arg == "--help" || action_arg == "-h" {
print_standalone_help();
return Ok(());
}
let action = normalize_action(action_arg)?;
let rest: Vec<String> = if raw_args.len() > 2 {
raw_args[2..].to_vec()
} else {
vec![]
};
match action {
Action::Install => run_install(&rest).await,
Action::Start => run_lifecycle(Action::Start, &rest),
Action::Stop => run_lifecycle(Action::Stop, &rest),
Action::Restart => run_lifecycle(Action::Restart, &rest),
Action::Delete => run_lifecycle(Action::Delete, &rest),
Action::Upgrade => run_lifecycle(Action::Upgrade, &rest),
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Action {
Install,
Start,
Stop,
Restart,
Delete,
Upgrade,
}
impl Action {
fn as_str(self) -> &'static str {
match self {
Action::Install => "install",
Action::Start => "start",
Action::Stop => "stop",
Action::Restart => "restart",
Action::Delete => "delete",
Action::Upgrade => "upgrade",
}
}
fn requires_docker(self) -> bool {
matches!(
self,
Action::Start | Action::Restart | Action::Upgrade | Action::Install
)
}
fn is_destructive(self) -> bool {
matches!(self, Action::Delete | Action::Upgrade)
}
}
fn normalize_action(s: &str) -> Result<Action> {
Ok(match s {
"install" => Action::Install,
"start" => Action::Start,
"stop" => Action::Stop,
"restart" => Action::Restart,
"delete" => Action::Delete,
"upgrade" | "update" => Action::Upgrade,
other => bail!(
"Unknown standalone action '{}'. Available: {}",
other,
ACTIONS.join(", ")
),
})
}
fn print_milvus_help() {
println!(
"Manage local Milvus deployments.\n\n\
Usage: zilliz milvus <SUBCOMMAND>\n\n\
Subcommands:\n\
\x20 standalone Manage a local Milvus standalone Docker deployment.\n\n\
Run `zilliz milvus standalone --help` for the standalone lifecycle."
);
}
fn print_standalone_help() {
println!(
"Manage a local Milvus standalone Docker deployment via the official\n\
standalone_embed.sh script.\n\n\
Usage: zilliz milvus standalone <ACTION> [OPTIONS]\n\n\
Actions:\n\
\x20 install Download standalone_embed.sh into an install directory.\n\
\x20 start Start the Milvus standalone container (`bash standalone_embed.sh start`).\n\
\x20 stop Stop the Milvus standalone container.\n\
\x20 restart Restart the Milvus standalone container.\n\
\x20 delete Remove container, data volumes, and config files (destructive).\n\
\x20 upgrade Upgrade to the latest standalone_embed.sh from upstream master (destructive).\n\
\x20 Alias: `update`.\n\n\
Common options:\n\
\x20 --dir <PATH> Install directory [default: {}]\n\
\x20 --dry-run Print what would happen without touching the filesystem or Docker.\n\
\x20 -y, --yes Skip confirmation prompt for destructive actions.\n\
\x20 -h, --help Print help.\n\n\
Requirements: bash and a working Docker daemon (macOS/Linux/WSL). The\n\
upstream script may invoke `sudo` for Docker calls.\n\n\
Default endpoints after start: Milvus {}, WebUI {}, embedded etcd {}.",
DEFAULT_DIR, ENDPOINT_MILVUS, ENDPOINT_WEBUI, ENDPOINT_ETCD,
);
}
fn print_install_help() {
println!(
"Download standalone_embed.sh into an install directory.\n\n\
Usage: zilliz milvus standalone install [OPTIONS]\n\n\
Options:\n\
\x20 --dir <PATH> Install directory [default: {default_dir}]\n\
\x20 --script-url <URL> Override the script download URL (must be https://) [default: {default_url}]\n\
\x20 --dry-run Print what would happen without downloading.\n\
\x20 --start After downloading, run `bash standalone_embed.sh start`.\n\
\x20 --force Overwrite an existing standalone_embed.sh in the install directory.\n\
\x20 -h, --help Print help.\n\n\
Default endpoints after start: Milvus {milvus}, WebUI {webui}, embedded etcd {etcd}.\n\
Data path: <install-dir>/volumes/milvus.",
default_dir = DEFAULT_DIR,
default_url = DEFAULT_SCRIPT_URL,
milvus = ENDPOINT_MILVUS,
webui = ENDPOINT_WEBUI,
etcd = ENDPOINT_ETCD,
);
}
fn print_lifecycle_help(action: Action) {
let extra = match action {
Action::Delete => "\nDestructive: removes the milvus-standalone container, the volumes/ directory,\nand the embedEtcd.yaml / user.yaml config files. Requires confirmation or --yes.\n",
Action::Upgrade => "\nDestructive: stops the container and replaces standalone_embed.sh with the\nlatest version from upstream master. Requires confirmation or --yes.\n",
Action::Start | Action::Restart => "\nRequires a working Docker daemon (`docker info`).\n",
_ => "\n",
};
println!(
"Run `bash standalone_embed.sh {action}` from the install directory.\n\n\
Usage: zilliz milvus standalone {action} [OPTIONS]\n\n\
Options:\n\
\x20 --dir <PATH> Install directory [default: {default_dir}]\n\
\x20 --dry-run Print the command without invoking it.\n\
\x20 -y, --yes Skip confirmation prompt (destructive actions only).\n\
\x20 -h, --help Print help.\n{extra}",
action = action.as_str(),
default_dir = DEFAULT_DIR,
extra = extra,
);
}
#[derive(Debug, Default)]
struct InstallOpts {
dir: Option<String>,
script_url: Option<String>,
dry_run: bool,
start: bool,
force: bool,
help: bool,
}
fn parse_install_args(raw: &[String]) -> Result<InstallOpts> {
let mut opts = InstallOpts::default();
let mut i = 0;
while i < raw.len() {
let arg = raw[i].as_str();
match arg {
"-h" | "--help" => opts.help = true,
"--dry-run" => opts.dry_run = true,
"--start" => opts.start = true,
"--force" => opts.force = true,
"--dir" => {
i += 1;
let v = raw
.get(i)
.with_context(|| "--dir requires a value".to_string())?;
opts.dir = Some(v.clone());
}
"--script-url" => {
i += 1;
let v = raw
.get(i)
.with_context(|| "--script-url requires a value".to_string())?;
opts.script_url = Some(v.clone());
}
other => bail!(
"Unknown flag '{}' for `milvus standalone install`. Run with --help for options.",
other
),
}
i += 1;
}
Ok(opts)
}
async fn run_install(raw_args: &[String]) -> Result<()> {
let opts = parse_install_args(raw_args)?;
if opts.help {
print_install_help();
return Ok(());
}
let dir_str = opts.dir.as_deref().unwrap_or(DEFAULT_DIR).to_string();
let dir = PathBuf::from(&dir_str);
let script_url = opts
.script_url
.as_deref()
.unwrap_or(DEFAULT_SCRIPT_URL)
.to_string();
validate_script_url(&script_url)?;
let script_path = dir.join(SCRIPT_FILENAME);
let abs_dir = absolute_display(&dir);
let data_path = format!("{}/volumes/milvus", abs_dir);
if opts.dry_run {
println!("DRY RUN: zilliz milvus standalone install");
println!(" Target directory : {}", abs_dir);
println!(" Script URL : {}", script_url);
println!(" Script path : {}", script_path.display());
println!(
" Start command : (cd {} && bash {} start)",
abs_dir, SCRIPT_FILENAME
);
println!(" Milvus endpoint : {}", ENDPOINT_MILVUS);
println!(" WebUI endpoint : {}", ENDPOINT_WEBUI);
println!(" Embedded etcd : {}", ENDPOINT_ETCD);
println!(" Data path : {}", data_path);
println!("No filesystem or network mutation performed.");
return Ok(());
}
if script_path.exists() && !opts.force {
bail!(
"{} already exists. Use --force to overwrite the install script.",
script_path.display()
);
}
fs::create_dir_all(&dir)
.with_context(|| format!("Failed to create install directory: {}", dir.display()))?;
download_script(&script_url, &script_path).await?;
make_executable(&script_path)?;
println!("Downloaded {} -> {}", script_url, script_path.display());
if opts.start {
check_docker_available()?;
run_script_in_dir(&dir, "start")?;
println!();
print_endpoints_block(&abs_dir, &data_path);
return Ok(());
}
println!();
print_install_summary(&abs_dir, &data_path);
Ok(())
}
fn print_install_summary(abs_dir: &str, data_path: &str) {
println!("Milvus standalone is ready to start.");
println!();
println!("Next steps:");
println!(" zilliz milvus standalone start --dir {}", abs_dir);
println!(" zilliz milvus standalone stop --dir {}", abs_dir);
println!(" zilliz milvus standalone delete --dir {}", abs_dir);
println!(" zilliz milvus standalone upgrade --dir {}", abs_dir);
println!();
println!("Default endpoints after start:");
println!(" Milvus : {}", ENDPOINT_MILVUS);
println!(" WebUI : {}", ENDPOINT_WEBUI);
println!(" Embedded etcd: {}", ENDPOINT_ETCD);
println!(" Data path : {}", data_path);
}
fn print_endpoints_block(abs_dir: &str, data_path: &str) {
println!("Milvus standalone is running.");
println!(" Install path : {}", abs_dir);
println!(" Milvus : {}", ENDPOINT_MILVUS);
println!(" WebUI : {}", ENDPOINT_WEBUI);
println!(" Embedded etcd: {}", ENDPOINT_ETCD);
println!(" Data path : {}", data_path);
}
#[derive(Debug, Default)]
struct LifecycleOpts {
dir: Option<String>,
dry_run: bool,
yes: bool,
help: bool,
}
fn parse_lifecycle_args(raw: &[String], action: Action) -> Result<LifecycleOpts> {
let mut opts = LifecycleOpts::default();
let mut i = 0;
while i < raw.len() {
let arg = raw[i].as_str();
match arg {
"-h" | "--help" => opts.help = true,
"--dry-run" => opts.dry_run = true,
"-y" | "--yes" => opts.yes = true,
"--dir" => {
i += 1;
let v = raw
.get(i)
.with_context(|| "--dir requires a value".to_string())?;
opts.dir = Some(v.clone());
}
other => bail!(
"Unknown flag '{}' for `milvus standalone {}`. Run with --help for options.",
other,
action.as_str()
),
}
i += 1;
}
Ok(opts)
}
fn run_lifecycle(action: Action, raw_args: &[String]) -> Result<()> {
let opts = parse_lifecycle_args(raw_args, action)?;
if opts.help {
print_lifecycle_help(action);
return Ok(());
}
let dir_str = opts.dir.as_deref().unwrap_or(DEFAULT_DIR).to_string();
let dir = PathBuf::from(&dir_str);
let abs_dir = absolute_display(&dir);
let script_path = dir.join(SCRIPT_FILENAME);
if opts.dry_run {
println!("DRY RUN: zilliz milvus standalone {}", action.as_str());
println!(" Target directory : {}", abs_dir);
println!(
" Command : (cd {} && bash {} {})",
abs_dir,
SCRIPT_FILENAME,
action.as_str()
);
println!("No Docker, filesystem, or script invocation performed.");
return Ok(());
}
if !script_path.exists() {
bail!(
"Install script not found at {}.\n\
Run `zilliz milvus standalone install --dir {}` first.",
script_path.display(),
abs_dir
);
}
if action.is_destructive() && !opts.yes {
let prompt = match action {
Action::Delete => format!(
"This will remove the milvus-standalone container, all data under \
{}/volumes, and generated config files. Continue?",
abs_dir
),
Action::Upgrade => format!(
"This will stop the milvus-standalone container, replace {} with the \
latest version from upstream master, and restart. Continue?",
script_path.display()
),
_ => unreachable!(),
};
if !confirm(&prompt)? {
println!("Aborted.");
return Ok(());
}
}
if action.requires_docker() {
check_docker_available()?;
}
run_script_in_dir(&dir, action.as_str())?;
if matches!(action, Action::Start | Action::Restart | Action::Upgrade) {
let data_path = format!("{}/volumes/milvus", abs_dir);
println!();
print_endpoints_block(&abs_dir, &data_path);
}
Ok(())
}
fn validate_script_url(url: &str) -> Result<()> {
if !url.starts_with("https://") {
bail!(
"--script-url must use the https:// scheme (got '{}'). Refusing to download \
a script that will be executed.",
url
);
}
Ok(())
}
fn absolute_display(p: &Path) -> String {
match fs::canonicalize(p) {
Ok(c) => c.display().to_string(),
Err(_) => {
if p.is_absolute() {
p.display().to_string()
} else {
match std::env::current_dir() {
Ok(cwd) => cwd.join(p).display().to_string(),
Err(_) => p.display().to_string(),
}
}
}
}
}
async fn download_script(url: &str, dest: &Path) -> Result<()> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(60))
.build()
.context("Failed to build HTTP client")?;
let resp = client
.get(url)
.send()
.await
.with_context(|| format!("Failed to download install script from {}", url))?;
let status = resp.status();
if !status.is_success() {
bail!("HTTP {} when downloading {}", status, url);
}
let bytes = resp
.bytes()
.await
.with_context(|| format!("Failed to read response body from {}", url))?;
fs::write(dest, &bytes)
.with_context(|| format!("Failed to write install script to {}", dest.display()))?;
Ok(())
}
#[cfg(unix)]
fn make_executable(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)
.with_context(|| format!("Failed to stat {}", path.display()))?
.permissions();
perms.set_mode(0o755);
fs::set_permissions(path, perms)
.with_context(|| format!("Failed to chmod {}", path.display()))?;
Ok(())
}
#[cfg(not(unix))]
fn make_executable(_path: &Path) -> Result<()> {
Ok(())
}
fn check_docker_available() -> Result<()> {
let output = Command::new("docker")
.arg("info")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
match output {
Ok(status) if status.success() => Ok(()),
Ok(status) => bail!(
"`docker info` exited with status {}. A working Docker daemon is required.",
status
),
Err(e) => bail!(
"Failed to invoke `docker info`: {}. Is the Docker CLI installed and the daemon running?",
e
),
}
}
fn run_script_in_dir(dir: &Path, action: &str) -> Result<()> {
let status = Command::new("bash")
.arg(SCRIPT_FILENAME)
.arg(action)
.current_dir(dir)
.status()
.with_context(|| format!("Failed to invoke `bash {} {}`", SCRIPT_FILENAME, action))?;
if !status.success() {
let code = status.code().unwrap_or(1);
std::process::exit(code);
}
Ok(())
}
fn confirm(prompt: &str) -> Result<bool> {
let stdin = io::stdin();
if !stdin.is_terminal() {
bail!("Refusing to run a destructive action non-interactively. Pass --yes to confirm.");
}
let stderr = io::stderr();
eprint!("{} [y/N]: ", prompt);
stderr.lock().flush()?;
let mut input = String::new();
stdin.lock().read_line(&mut input)?;
Ok(matches!(
input.trim().to_ascii_lowercase().as_str(),
"y" | "yes"
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_action_accepts_known_actions() {
for &a in ACTIONS {
assert!(normalize_action(a).is_ok(), "{} should parse", a);
}
}
#[test]
fn normalize_action_treats_update_as_upgrade() {
assert_eq!(normalize_action("update").unwrap(), Action::Upgrade);
assert_eq!(normalize_action("upgrade").unwrap(), Action::Upgrade);
}
#[test]
fn normalize_action_rejects_unknown() {
let err = normalize_action("nope").unwrap_err().to_string();
assert!(err.contains("Unknown standalone action"));
}
#[test]
fn install_args_default() {
let opts = parse_install_args(&[]).unwrap();
assert!(opts.dir.is_none());
assert!(opts.script_url.is_none());
assert!(!opts.dry_run);
assert!(!opts.start);
assert!(!opts.force);
}
#[test]
fn install_args_full() {
let args = vec![
"--dir".to_string(),
"/tmp/m".to_string(),
"--script-url".to_string(),
"https://example.com/x.sh".to_string(),
"--dry-run".to_string(),
"--start".to_string(),
"--force".to_string(),
];
let opts = parse_install_args(&args).unwrap();
assert_eq!(opts.dir.as_deref(), Some("/tmp/m"));
assert_eq!(opts.script_url.as_deref(), Some("https://example.com/x.sh"));
assert!(opts.dry_run);
assert!(opts.start);
assert!(opts.force);
}
#[test]
fn install_args_unknown_flag_rejected() {
let args = vec!["--what".to_string()];
let err = parse_install_args(&args).unwrap_err().to_string();
assert!(err.contains("Unknown flag"));
}
#[test]
fn install_args_dir_without_value() {
let args = vec!["--dir".to_string()];
let err = parse_install_args(&args).unwrap_err().to_string();
assert!(err.contains("--dir"));
}
#[test]
fn validate_script_url_requires_https() {
assert!(validate_script_url("https://example.com/x.sh").is_ok());
assert!(validate_script_url("http://example.com/x.sh").is_err());
assert!(validate_script_url("file:///etc/passwd").is_err());
assert!(validate_script_url("ftp://example.com").is_err());
}
#[test]
fn lifecycle_args_yes_short() {
let args = vec!["-y".to_string()];
let opts = parse_lifecycle_args(&args, Action::Delete).unwrap();
assert!(opts.yes);
}
#[test]
fn lifecycle_args_yes_long() {
let args = vec!["--yes".to_string()];
let opts = parse_lifecycle_args(&args, Action::Delete).unwrap();
assert!(opts.yes);
}
#[test]
fn lifecycle_args_dir_and_dry_run() {
let args = vec![
"--dir".to_string(),
"/tmp/m".to_string(),
"--dry-run".to_string(),
];
let opts = parse_lifecycle_args(&args, Action::Stop).unwrap();
assert_eq!(opts.dir.as_deref(), Some("/tmp/m"));
assert!(opts.dry_run);
}
#[test]
fn lifecycle_args_unknown_flag_rejected() {
let args = vec!["--what".to_string()];
let err = parse_lifecycle_args(&args, Action::Start)
.unwrap_err()
.to_string();
assert!(err.contains("Unknown flag"));
}
fn block_on<F: std::future::Future>(f: F) -> F::Output {
tokio::runtime::Runtime::new().unwrap().block_on(f)
}
#[test]
fn install_dry_run_does_not_create_dir() {
let tmp =
std::env::temp_dir().join(format!("zilliz-install-dryrun-{}", std::process::id()));
if tmp.exists() {
let _ = fs::remove_dir_all(&tmp);
}
let args = vec![
"--dir".to_string(),
tmp.display().to_string(),
"--dry-run".to_string(),
];
block_on(run_install(&args)).unwrap();
assert!(
!tmp.exists(),
"dry-run must not create install directory: {}",
tmp.display()
);
}
#[test]
fn install_dry_run_with_https_custom_url() {
let tmp = std::env::temp_dir().join(format!(
"zilliz-install-dryrun-custom-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&tmp);
let args = vec![
"--dir".to_string(),
tmp.display().to_string(),
"--script-url".to_string(),
"https://example.com/script.sh".to_string(),
"--dry-run".to_string(),
];
block_on(run_install(&args)).unwrap();
assert!(!tmp.exists());
}
#[test]
fn install_rejects_non_https_script_url() {
let tmp =
std::env::temp_dir().join(format!("zilliz-install-bad-url-{}", std::process::id()));
let args = vec![
"--dir".to_string(),
tmp.display().to_string(),
"--script-url".to_string(),
"http://example.com/script.sh".to_string(),
];
let err = block_on(run_install(&args)).unwrap_err().to_string();
assert!(err.contains("https://"));
assert!(!tmp.exists());
}
#[test]
fn install_refuses_to_overwrite_existing_script_without_force() {
let tmp =
std::env::temp_dir().join(format!("zilliz-install-existing-{}", std::process::id()));
fs::create_dir_all(&tmp).unwrap();
let script = tmp.join(SCRIPT_FILENAME);
fs::write(&script, b"#existing").unwrap();
let args = vec!["--dir".to_string(), tmp.display().to_string()];
let err = block_on(run_install(&args)).unwrap_err().to_string();
assert!(err.contains("--force"));
let after = fs::read(&script).unwrap();
assert_eq!(after, b"#existing");
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn lifecycle_dry_run_renders_command() {
let tmp =
std::env::temp_dir().join(format!("zilliz-lifecycle-dryrun-{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
let args = vec![
"--dir".to_string(),
tmp.display().to_string(),
"--dry-run".to_string(),
];
for action in [
Action::Start,
Action::Stop,
Action::Restart,
Action::Delete,
Action::Upgrade,
] {
run_lifecycle(action, &args).unwrap();
}
assert!(!tmp.exists());
}
#[test]
fn lifecycle_missing_script_errors_with_install_hint() {
let tmp =
std::env::temp_dir().join(format!("zilliz-lifecycle-missing-{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
let args = vec!["--dir".to_string(), tmp.display().to_string()];
let err = run_lifecycle(Action::Stop, &args).unwrap_err().to_string();
assert!(err.contains("Install script not found"));
assert!(err.contains("zilliz milvus standalone install"));
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn run_from_args_help_prints_without_error() {
let cfg = ConfigManager::new(None).unwrap();
let models = crate::model::loader::ModelLoader::load_builtin().unwrap();
let opts = OutputOpts::new("table");
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
run_from_args(&models, &cfg, &["--help".to_string()], &opts)
.await
.unwrap();
run_from_args(
&models,
&cfg,
&["standalone".to_string(), "--help".to_string()],
&opts,
)
.await
.unwrap();
run_from_args(
&models,
&cfg,
&[
"standalone".to_string(),
"install".to_string(),
"--help".to_string(),
],
&opts,
)
.await
.unwrap();
run_from_args(
&models,
&cfg,
&[
"standalone".to_string(),
"delete".to_string(),
"--help".to_string(),
],
&opts,
)
.await
.unwrap();
});
}
#[test]
fn run_from_args_update_alias_routes_to_upgrade() {
let cfg = ConfigManager::new(None).unwrap();
let models = crate::model::loader::ModelLoader::load_builtin().unwrap();
let opts = OutputOpts::new("table");
let tmp = std::env::temp_dir().join(format!("zilliz-update-alias-{}", std::process::id()));
let _ = fs::remove_dir_all(&tmp);
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
run_from_args(
&models,
&cfg,
&[
"standalone".to_string(),
"update".to_string(),
"--dir".to_string(),
tmp.display().to_string(),
"--dry-run".to_string(),
],
&opts,
)
.await
.unwrap();
});
}
#[test]
fn run_from_args_unknown_subcommand_errors() {
let cfg = ConfigManager::new(None).unwrap();
let models = crate::model::loader::ModelLoader::load_builtin().unwrap();
let opts = OutputOpts::new("table");
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let err = run_from_args(&models, &cfg, &["bogus".to_string()], &opts)
.await
.unwrap_err()
.to_string();
assert!(err.contains("Unknown milvus operation"));
});
}
}