use std::path::PathBuf;
use clap::Command;
use serde_json::Value;
use tempfile::TempDir;
fn build_cli() -> Command {
Command::new("my-cli")
.version("0.1.0")
.subcommand(brontes::command(None))
}
fn dispatch(argv: &[&str]) -> brontes::Result<()> {
let cli = build_cli();
let mut full: Vec<&str> = vec!["my-cli"];
full.extend_from_slice(argv);
let matches = cli.clone().get_matches_from(full);
let Some(("mcp", sub)) = matches.subcommand() else {
panic!(
"expected `mcp` subcommand match, got {:?}",
matches.subcommand_name()
);
};
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
rt.block_on(brontes::handle(sub, &cli, None))
}
fn read_json(path: &PathBuf) -> Value {
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
serde_json::from_slice(&bytes).unwrap_or_else(|e| panic!("parse {}: {e}", path.display()))
}
#[test]
fn enable_writes_config_with_expected_shape() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().expect("utf8 path"),
"--server-name",
"test-cli",
])
.expect("enable succeeds");
let doc = read_json(&cfg_path);
let server = &doc["mcpServers"]["test-cli"];
assert!(server.is_object(), "test-cli must be present");
assert_eq!(
server["type"].as_str(),
Some("stdio"),
"type must be stdio on enable"
);
assert!(server["command"].is_string());
let args: Vec<&str> = server["args"]
.as_array()
.expect("args")
.iter()
.map(|v| v.as_str().expect("string"))
.collect();
assert_eq!(args, vec!["mcp", "start"]);
assert!(server.get("env").is_none(), "no env -> key omitted");
assert!(server.get("url").is_none(), "no url -> key omitted");
assert!(server.get("headers").is_none(), "no headers -> key omitted");
}
#[test]
fn enable_field_order_is_type_command_args_env() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"ordered",
"--env",
"K=V",
])
.expect("enable");
let raw = std::fs::read_to_string(&cfg_path).expect("read");
let server_start = raw.find(r#""ordered""#).expect("ordered key");
let after = &raw[server_start..];
let body_start = after.find('{').expect("body");
let body_end = after[body_start..].find('}').expect("close") + body_start;
let body = &after[body_start..=body_end];
let pos_type = body.find(r#""type""#).expect("type field");
let pos_command = body.find(r#""command""#).expect("command field");
let pos_args = body.find(r#""args""#).expect("args field");
let pos_env = body.find(r#""env""#).expect("env field");
assert!(
pos_type < pos_command,
"type must precede command, body={body}"
);
assert!(
pos_command < pos_args,
"command must precede args, body={body}"
);
assert!(pos_args < pos_env, "args must precede env, body={body}");
}
#[test]
fn round_trip_preserves_full_six_field_order_type_command_args_env_url_headers() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
let seed = serde_json::json!({
"mcpServers": {
"remote": {
"type": "sse",
"command": "ignored-for-sse",
"args": ["--unused"],
"env": { "K": "V" },
"url": "https://example.test/mcp",
"headers": { "Authorization": "Bearer abc" }
}
}
});
std::fs::write(&cfg_path, serde_json::to_vec_pretty(&seed).unwrap()).expect("seed");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"new-stdio",
])
.expect("enable second server");
let raw = std::fs::read_to_string(&cfg_path).expect("read");
let parsed: serde_json::Value = serde_json::from_str(&raw).expect("parse");
let remote = &parsed["mcpServers"]["remote"];
assert_eq!(remote["type"].as_str(), Some("sse"), "type preserved");
assert_eq!(
remote["url"].as_str(),
Some("https://example.test/mcp"),
"url preserved in {raw}"
);
assert_eq!(
remote["headers"]["Authorization"].as_str(),
Some("Bearer abc"),
"headers preserved in {raw}"
);
let server_start = raw.find(r#""remote""#).expect("remote key");
let after = &raw[server_start..];
let body_start = after.find('{').expect("body open");
let body_end_offset = balanced_object_end(&after[body_start..]).expect("balanced body close");
let body = &after[body_start..=body_start + body_end_offset];
let pos = |k: &str| {
body.find(k)
.unwrap_or_else(|| panic!("{k} missing in body={body}"))
};
let p_type = pos(r#""type""#);
let p_command = pos(r#""command""#);
let p_args = pos(r#""args""#);
let p_env = pos(r#""env""#);
let p_url = pos(r#""url""#);
let p_headers = pos(r#""headers""#);
assert!(p_type < p_command, "type < command failed in {body}");
assert!(p_command < p_args, "command < args failed in {body}");
assert!(p_args < p_env, "args < env failed in {body}");
assert!(p_env < p_url, "env < url failed in {body}");
assert!(p_url < p_headers, "url < headers failed in {body}");
}
fn balanced_object_end(slice: &str) -> Option<usize> {
let bytes = slice.as_bytes();
debug_assert_eq!(bytes[0], b'{');
let mut depth: u32 = 0;
let mut in_str = false;
let mut escaped = false;
for (i, &b) in bytes.iter().enumerate() {
if escaped {
escaped = false;
continue;
}
match b {
b'\\' if in_str => escaped = true,
b'"' => in_str = !in_str,
b'{' if !in_str => depth += 1,
b'}' if !in_str => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
#[test]
fn enable_includes_log_level_in_args() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"test-cli",
"--log-level",
"debug",
])
.expect("enable succeeds");
let doc = read_json(&cfg_path);
let args: Vec<&str> = doc["mcpServers"]["test-cli"]["args"]
.as_array()
.unwrap()
.iter()
.map(|v| v.as_str().unwrap())
.collect();
assert_eq!(args, vec!["mcp", "start", "--log-level", "debug"]);
}
#[test]
fn enable_with_env_writes_env_block() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"test-cli",
"--env",
"PATH=/usr/local/bin",
"--env",
"DEBUG=1",
])
.expect("enable succeeds");
let doc = read_json(&cfg_path);
let env = &doc["mcpServers"]["test-cli"]["env"];
assert_eq!(env["PATH"].as_str(), Some("/usr/local/bin"));
assert_eq!(env["DEBUG"].as_str(), Some("1"));
}
#[test]
fn enable_overwrites_existing_server() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"test-cli",
])
.expect("first enable");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"test-cli",
"--log-level",
"info",
])
.expect("second enable");
let doc = read_json(&cfg_path);
let has_log_level = doc["mcpServers"]["test-cli"]["args"]
.as_array()
.unwrap()
.iter()
.any(|v| v.as_str().unwrap() == "--log-level");
assert!(has_log_level, "second enable must have updated the args");
}
#[test]
fn disable_removes_existing_server() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"test-cli",
])
.expect("enable");
dispatch(&[
"mcp",
"cursor",
"disable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"test-cli",
])
.expect("disable");
let doc = read_json(&cfg_path);
assert!(
doc["mcpServers"].get("test-cli").is_none(),
"test-cli must be removed"
);
}
#[test]
fn disable_missing_server_is_ok() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
std::fs::write(&cfg_path, b"{\"mcpServers\":{}}").expect("seed");
dispatch(&[
"mcp",
"cursor",
"disable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"not-there",
])
.expect("disable on missing name returns Ok(())");
}
#[test]
fn list_on_missing_file_surfaces_path() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
dispatch(&[
"mcp",
"cursor",
"list",
"--config-path",
cfg_path.to_str().unwrap(),
])
.expect("list on missing file is Ok");
assert!(!cfg_path.exists(), "list must not create the config file");
}
#[test]
fn list_shows_configured_servers() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"alpha",
])
.expect("enable alpha");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"beta",
])
.expect("enable beta");
let doc = read_json(&cfg_path);
let names: Vec<&str> = doc["mcpServers"]
.as_object()
.unwrap()
.keys()
.map(String::as_str)
.collect();
assert!(names.contains(&"alpha"));
assert!(names.contains(&"beta"));
}
#[test]
fn save_creates_backup_when_file_exists() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"first",
])
.expect("first enable");
let backup_path = dir.path().join("mcp.backup.json");
assert!(
!backup_path.exists(),
"first write must not produce a backup"
);
let pre_second = std::fs::read(&cfg_path).expect("read pre-second");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"second",
])
.expect("second enable");
assert!(backup_path.exists(), "second write must produce a backup");
let backup_bytes = std::fs::read(&backup_path).expect("read backup");
assert_eq!(
backup_bytes, pre_second,
"backup must mirror the pre-second-write state"
);
}
#[test]
fn save_no_backup_on_missing_primary() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"fresh",
])
.expect("enable");
assert!(cfg_path.exists());
let backup_path = dir.path().join("mcp.backup.json");
assert!(
!backup_path.exists(),
"first write must NOT create a backup"
);
}
#[test]
fn round_trip_preserves_inputs_with_mixed_password_states() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
let seed = r#"{
"inputs": [
{
"type": "promptString",
"id": "api-key",
"description": "API key",
"password": true
},
{
"type": "promptString",
"id": "username",
"description": "Username",
"password": false
}
],
"mcpServers": {
"existing": {
"type": "stdio",
"command": "/bin/existing"
}
}
}
"#;
std::fs::write(&cfg_path, seed).expect("seed");
dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"added",
])
.expect("enable");
let doc = read_json(&cfg_path);
let inputs = doc["inputs"].as_array().expect("inputs array preserved");
assert_eq!(inputs.len(), 2, "must preserve both input entries");
let api_key = inputs
.iter()
.find(|i| i["id"].as_str() == Some("api-key"))
.expect("api-key input preserved");
assert_eq!(api_key["type"].as_str(), Some("promptString"));
assert_eq!(api_key["description"].as_str(), Some("API key"));
assert_eq!(
api_key["password"].as_bool(),
Some(true),
"password=true must survive"
);
let username = inputs
.iter()
.find(|i| i["id"].as_str() == Some("username"))
.expect("username input preserved");
assert_eq!(username["type"].as_str(), Some("promptString"));
assert_eq!(username["description"].as_str(), Some("Username"));
assert!(
username.get("password").is_none() || username["password"].as_bool() == Some(false),
"password=false should round-trip as either missing or false"
);
let servers = doc["mcpServers"].as_object().expect("mcpServers preserved");
assert!(
servers.contains_key("existing"),
"existing server preserved"
);
assert!(servers.contains_key("added"), "newly added server present");
assert_eq!(
servers["existing"]["type"].as_str(),
Some("stdio"),
"existing type field survives round-trip"
);
assert_eq!(
servers["existing"]["command"].as_str(),
Some("/bin/existing"),
"existing command field survives round-trip"
);
}
fn with_cwd<R>(workspace_dir: &std::path::Path, f: impl FnOnce() -> R) -> R {
use std::sync::{Mutex, OnceLock};
static CWD_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
let lock = CWD_LOCK.get_or_init(|| Mutex::new(()));
let _guard = lock.lock().expect("cwd lock");
let prev = std::env::current_dir().expect("save cwd");
std::env::set_current_dir(workspace_dir).expect("set cwd");
let result = f();
std::env::set_current_dir(&prev).expect("restore cwd");
result
}
#[test]
fn workspace_enable_writes_under_cwd_dot_cursor() {
let dir = TempDir::new().expect("tempdir");
with_cwd(dir.path(), || {
dispatch(&[
"mcp",
"cursor",
"enable",
"--workspace",
"--server-name",
"ws-srv",
])
.expect("workspace enable");
});
let expected = dir.path().join(".cursor").join("mcp.json");
assert!(
expected.exists(),
"workspace enable must write to {}",
expected.display()
);
let doc = read_json(&expected);
assert!(doc["mcpServers"]["ws-srv"].is_object());
}
#[test]
fn workspace_disable_targets_cwd_dot_cursor() {
let dir = TempDir::new().expect("tempdir");
let workspace_cfg = dir.path().join(".cursor").join("mcp.json");
std::fs::create_dir_all(workspace_cfg.parent().unwrap()).expect("mkdir");
std::fs::write(
&workspace_cfg,
br#"{"mcpServers":{"to-remove":{"type":"stdio","command":"/bin/x"}}}"#,
)
.expect("seed");
with_cwd(dir.path(), || {
dispatch(&[
"mcp",
"cursor",
"disable",
"--workspace",
"--server-name",
"to-remove",
])
.expect("workspace disable");
});
let doc = read_json(&workspace_cfg);
assert!(
doc["mcpServers"].get("to-remove").is_none(),
"workspace disable must target $CWD/.cursor/mcp.json"
);
}
#[test]
fn workspace_list_targets_cwd_dot_cursor() {
let dir = TempDir::new().expect("tempdir");
let workspace_cfg = dir.path().join(".cursor").join("mcp.json");
std::fs::create_dir_all(workspace_cfg.parent().unwrap()).expect("mkdir");
std::fs::write(
&workspace_cfg,
br#"{"mcpServers":{"present":{"type":"stdio","command":"/bin/x"}}}"#,
)
.expect("seed");
with_cwd(dir.path(), || {
dispatch(&["mcp", "cursor", "list", "--workspace"]).expect("workspace list");
});
assert!(
workspace_cfg.exists(),
"workspace list must not delete the file"
);
}
#[test]
fn list_on_invalid_json_returns_json_error() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
std::fs::write(&cfg_path, b"not valid json").expect("seed");
let result = dispatch(&[
"mcp",
"cursor",
"list",
"--config-path",
cfg_path.to_str().unwrap(),
]);
let err = result.expect_err("must fail on invalid JSON");
let msg = err.to_string();
assert!(
msg.contains("editor config: JSON error"),
"unexpected error: {msg}"
);
}
#[test]
fn enable_rejects_malformed_env_flag() {
let dir = TempDir::new().expect("tempdir");
let cfg_path = dir.path().join("mcp.json");
let result = dispatch(&[
"mcp",
"cursor",
"enable",
"--config-path",
cfg_path.to_str().unwrap(),
"--server-name",
"test",
"--env",
"NO_EQUALS_SIGN",
]);
let err = result.expect_err("malformed --env must error");
let msg = err.to_string();
assert!(
msg.contains("missing '='"),
"unexpected error message: {msg}"
);
assert!(
!cfg_path.exists(),
"config must NOT be written on env-flag rejection"
);
}