use std::process::Command;
use basemind::cli::context::build_server;
use basemind::config::DocumentsCliOverrides;
use basemind::store::VIEW_WORKING;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_basemind")
}
fn tool_to_cli() -> Vec<(&'static str, Option<&'static str>, &'static str)> {
#[allow(unused_mut)]
let mut m: Vec<(&str, Option<&str>, &str)> = vec![
("code", Some("outline"), "code outline"),
("code", Some("symbols"), "code symbols"),
("code", Some("grep"), "code grep"),
("code", Some("files"), "code files"),
("code", Some("find"), "code find"),
("code", Some("definition"), "code definition"),
("code", Some("references"), "code references"),
("code", Some("callers"), "code callers"),
("code", Some("implementations"), "code implementations"),
("code", Some("dependents"), "code dependents"),
("code", Some("expand"), "code expand"),
("code", Some("semantic"), "code semantic"),
("code", Some("chunk"), "code chunk"),
("graph", Some("calls"), "graph calls"),
("graph", Some("neighbors"), "graph neighbors"),
("graph", Some("path"), "graph path"),
("graph", Some("subgraph"), "graph subgraph"),
("graph", Some("communities"), "graph communities"),
("graph", Some("map"), "graph map"),
("graph", Some("export"), "graph export"),
("graph", Some("display"), "graph display"),
("graph", Some("open"), "graph open"),
("admin", Some("status"), "admin status"),
("admin", Some("repo"), "admin repo"),
("admin", Some("rescan"), "admin rescan"),
("admin", Some("cache_stats"), "admin cache-stats"),
("admin", Some("gc"), "admin gc"),
("admin", Some("cache_clear"), "admin cache-clear"),
("admin", Some("telemetry"), "admin telemetry"),
("admin", Some("compress"), "admin compress"),
("admin", Some("delta"), "admin delta"),
("admin", Some("checkpoint"), "admin checkpoint"),
("admin", Some("waste"), "admin waste"),
("git", Some("status"), "git status"),
("git", Some("recent"), "git recent"),
("git", Some("touching"), "git touching"),
("git", Some("by_path"), "git by-path"),
("git", Some("churn"), "git churn"),
("git", Some("diff"), "git diff"),
("git", Some("diff_outline"), "git diff-outline"),
("git", Some("blame"), "git blame"),
("git", Some("blame_symbol"), "git blame-symbol"),
("git", Some("symbol_history"), "git symbol-history"),
("git", Some("search"), "git search"),
("memory", Some("put"), "memory put"),
("memory", Some("get"), "memory get"),
("memory", Some("list"), "memory list"),
("memory", Some("search"), "memory search"),
("memory", Some("delete"), "memory delete"),
("memory", Some("audit"), "memory audit"),
("memory", Some("documents"), "memory documents"),
("memory", Some("mine"), "memory mine"),
("memory", Some("proposals"), "memory proposals"),
("memory", Some("accept"), "memory accept"),
("memory", Some("reject"), "memory reject"),
];
#[cfg(feature = "crawl")]
m.extend([
("web", Some("scrape"), "web scrape"),
("web", Some("crawl"), "web crawl"),
("web", Some("map"), "web map"),
]);
#[cfg(all(feature = "comms", any(unix, windows)))]
m.extend([
("agents", Some("register"), "agents register"),
("agents", Some("list"), "agents list"),
("agents", Some("thread_start"), "agents thread-start"),
("agents", Some("thread_list"), "agents thread-list"),
("agents", Some("join"), "agents join"),
("agents", Some("leave"), "agents leave"),
("agents", Some("members"), "agents members"),
("agents", Some("add_member"), "agents add-member"),
("agents", Some("remove_member"), "agents remove-member"),
("agents", Some("archive"), "agents archive"),
("agents", Some("post"), "agents post"),
("agents", Some("history"), "agents history"),
("agents", Some("message"), "agents message"),
("agents", Some("inbox"), "agents inbox"),
("agents", Some("ack"), "agents ack"),
("agents", Some("wait"), "agents wait"),
("workspace", Some("workspaces"), "workspace workspaces"),
("workspace", Some("worktrees"), "workspace worktrees"),
("workspace", Some("branches"), "workspace branches"),
("workspace", Some("claim"), "workspace claim"),
("workspace", Some("release"), "workspace release"),
]);
#[cfg(all(feature = "shells", any(unix, windows)))]
m.extend([
("shell", Some("spawn"), "shell spawn"),
("shell", Some("send"), "shell send"),
("shell", Some("capture"), "shell capture"),
("shell", Some("kill"), "shell kill"),
("shell", Some("list"), "shell list"),
("shell", Some("broadcast"), "shell broadcast"),
]);
m
}
fn advertised_tools() -> Vec<String> {
basemind::store::init_isolated_cache();
let tmp = tempfile::tempdir().expect("tempdir");
let server =
build_server(tmp.path(), VIEW_WORKING, DocumentsCliOverrides::default()).expect("build one-shot server");
server.tool_names()
}
#[test]
fn every_mcp_tool_has_a_cli_command() {
let tools = advertised_tools();
let map = tool_to_cli();
let mapped: std::collections::HashSet<&str> = map.iter().map(|(tool, _, _)| *tool).collect();
let unmapped: Vec<&String> = tools.iter().filter(|t| !mapped.contains(t.as_str())).collect();
assert!(
unmapped.is_empty(),
"MCP tools with no CLI mapping (add the CLI command + a TOOL_TO_CLI row): {unmapped:?}"
);
let live: std::collections::HashSet<&str> = tools.iter().map(String::as_str).collect();
let stale: Vec<&str> = map
.iter()
.map(|(tool, _, _)| *tool)
.filter(|t| !live.contains(t))
.collect();
assert!(
stale.is_empty(),
"TOOL_TO_CLI rows for tools no longer advertised (remove or rename them): {stale:?}"
);
}
#[test]
fn every_advertised_mode_has_a_cli_command() {
let map = tool_to_cli();
for (domain, modes) in basemind::mcp::mode::domain_modes() {
let rows: Vec<&str> = map
.iter()
.filter(|(tool, _, _)| *tool == domain)
.filter_map(|(_, mode, _)| *mode)
.collect();
let unmapped: Vec<&&str> = modes.iter().filter(|m| !rows.contains(m)).collect();
assert!(
unmapped.is_empty(),
"`{domain}` modes with no CLI mapping (add the subcommand + a TOOL_TO_CLI row): {unmapped:?}"
);
let stale: Vec<&&str> = rows.iter().filter(|m| !modes.contains(m)).collect();
assert!(
stale.is_empty(),
"TOOL_TO_CLI rows for `{domain}` modes the tool no longer accepts: {stale:?}"
);
}
}
#[test]
fn every_mapped_cli_command_resolves() {
for (tool, mode, cli) in tool_to_cli() {
let mut args: Vec<&str> = cli.split(' ').collect();
args.push("--help");
let output = Command::new(bin())
.args(&args)
.output()
.unwrap_or_else(|e| panic!("spawn `basemind {cli} --help`: {e}"));
let operation = match mode {
Some(mode) => format!("{tool}:{mode}"),
None => tool.to_string(),
};
assert!(
output.status.success(),
"`basemind {cli} --help` (for `{operation}`) exited {:?}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr)
);
}
}