use std::collections::BTreeMap;
use serde_json::{Value, json};
use super::{check_mcp_registered, probe_mcp, reconcile_mcp, remove_mcp, writable_names};
use crate::agents::BackendState;
use crate::components::{McpKind, McpServer};
use crate::host::Outcome;
fn scratch(name: &str) -> std::path::PathBuf {
let dir = crate::scratch::path("ez-zed-unit");
std::fs::create_dir_all(&dir).unwrap();
dir.join(name)
}
fn stdio(name: &str, command: &str, args: &[&str]) -> McpServer {
McpServer {
name: name.into(),
kind: McpKind::Stdio,
command: command.into(),
args: args.iter().map(|a| a.to_string()).collect(),
env: BTreeMap::new(),
}
}
#[cfg(not(any(windows, target_os = "macos")))]
#[test]
fn xdg_wins_only_when_absolute_else_home_config() {
use std::path::{Path, PathBuf};
use super::xdg_or_home_config;
let home = || Some(PathBuf::from("/home/u"));
assert_eq!(xdg_or_home_config(Some(PathBuf::from("/xdg")), home()), Some(PathBuf::from("/xdg/zed")));
assert_eq!(xdg_or_home_config(Some(PathBuf::from("relative")), home()), Some(Path::new("/home/u/.config/zed").to_path_buf()));
assert_eq!(xdg_or_home_config(None, home()), Some(Path::new("/home/u/.config/zed").to_path_buf()));
assert_eq!(xdg_or_home_config(None, None), None);
}
fn read(path: &std::path::Path) -> Value {
serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap()
}
#[test]
fn reconcile_writes_flat_context_servers_body_and_second_is_noop() {
let path = scratch("settings.json");
let server = stdio("ez-fixture", "host_fixture", &["mcp"]);
let out = reconcile_mcp(&path, std::slice::from_ref(&server)).unwrap();
assert_eq!(out, Outcome::Installed);
let root = read(&path);
assert_eq!(root["context_servers"]["ez-fixture"], json!({ "command": "host_fixture", "args": ["mcp"], "env": {} }));
let out = reconcile_mcp(&path, std::slice::from_ref(&server)).unwrap();
assert_eq!(out, Outcome::NoOp);
std::fs::remove_dir_all(path.parent().unwrap()).ok();
}
#[test]
fn probe_classifies_absent_healthy_and_needs_repair() {
let path = scratch("settings.json");
let server = stdio("ez-fixture", "host_fixture", &["mcp"]);
assert!(matches!(probe_mcp(&path, std::slice::from_ref(&server)).unwrap(), BackendState::Absent));
reconcile_mcp(&path, std::slice::from_ref(&server)).unwrap();
assert!(matches!(probe_mcp(&path, std::slice::from_ref(&server)).unwrap(), BackendState::Healthy));
let drifted = stdio("ez-fixture", "host_fixture", &["mcp", "--v2"]);
assert!(matches!(probe_mcp(&path, std::slice::from_ref(&drifted)).unwrap(), BackendState::NeedsRepair));
std::fs::remove_dir_all(path.parent().unwrap()).ok();
}
#[test]
fn remove_deletes_only_ours_and_preserves_a_user_entry() {
let path = scratch("settings.json");
std::fs::write(&path, r#"{"theme":"One Dark","context_servers":{"theirs":{"command":"their-server","args":[],"env":{}}}}"#).unwrap();
let server = stdio("ez-fixture", "host_fixture", &["mcp"]);
reconcile_mcp(&path, std::slice::from_ref(&server)).unwrap();
let root = read(&path);
assert!(root["context_servers"]["ez-fixture"].is_object(), "our server was not written");
assert!(root["context_servers"]["theirs"].is_object(), "the user's server was clobbered on reconcile");
let out = remove_mcp(&path, std::slice::from_ref(&server)).unwrap();
assert_eq!(out, Outcome::Removed);
let root = read(&path);
assert!(root["context_servers"].get("ez-fixture").is_none(), "our server survived remove");
assert_eq!(root["context_servers"]["theirs"], json!({ "command": "their-server", "args": [], "env": {} }), "user's server was touched");
assert_eq!(root["theme"], json!("One Dark"), "unrelated top-level key was touched");
assert_eq!(remove_mcp(&path, std::slice::from_ref(&server)).unwrap(), Outcome::NoOp);
std::fs::remove_dir_all(path.parent().unwrap()).ok();
}
#[test]
fn writable_names_includes_http_and_excludes_non_portable_and_sse() {
let http = McpServer {
name: "remote-http".into(),
kind: McpKind::Http { url: "https://example.test/mcp".into() },
command: String::new(),
args: Vec::new(),
env: BTreeMap::new(),
};
let sse = McpServer {
name: "remote-sse".into(),
kind: McpKind::Sse { url: "https://example.test/sse".into() },
command: String::new(),
args: Vec::new(),
env: BTreeMap::new(),
};
let servers = [stdio("ez-fixture", "host_fixture", &["mcp"]), stdio("rooted", "${CLAUDE_PLUGIN_ROOT}/bin/leaky", &[]), http, sse];
assert_eq!(writable_names(&servers), vec!["ez-fixture", "remote-http"]);
}
#[test]
fn reconcile_skips_non_portable_and_sse_servers_but_writes_http() {
let path = scratch("settings.json");
let http = McpServer {
name: "remote-http".into(),
kind: McpKind::Http { url: "https://example.test/mcp".into() },
command: String::new(),
args: Vec::new(),
env: BTreeMap::new(),
};
let sse = McpServer {
name: "remote-sse".into(),
kind: McpKind::Sse { url: "https://example.test/sse".into() },
command: String::new(),
args: Vec::new(),
env: BTreeMap::new(),
};
let servers = [stdio("ez-fixture", "host_fixture", &["mcp"]), stdio("rooted", "${CLAUDE_PLUGIN_ROOT}/bin/leaky", &[]), http, sse];
reconcile_mcp(&path, &servers).unwrap();
let root = read(&path);
assert!(root["context_servers"]["ez-fixture"].is_object(), "the portable stdio server must be written");
assert_eq!(
root["context_servers"]["remote-http"],
serde_json::json!({"url": "https://example.test/mcp", "headers": {}}),
"http must land in zed's {{url,headers}} form"
);
assert!(root["context_servers"].get("rooted").is_none(), "a CLAUDE_PLUGIN_ROOT-bearing server must be skipped");
assert!(root["context_servers"].get("remote-sse").is_none(), "an sse server must be skipped (no zed landing)");
std::fs::remove_dir_all(path.parent().unwrap()).ok();
}
#[test]
fn a_dropped_server_warns_in_the_report() {
let rooted = stdio("ez", "${CLAUDE_PLUGIN_ROOT}/bin/ez", &[]);
let sse = McpServer { name: "rm".into(), kind: McpKind::Sse { url: "https://x/sse".into() }, ..stdio("rm", "", &[]) };
let check = check_mcp_registered(&[rooted, sse], None);
let crate::doctor::CheckStatus::Warn(detail) = &check.status else {
panic!("a dropped server must warn, got {:?}", check.status);
};
assert!(detail.contains("skipped ez: ${CLAUDE_PLUGIN_ROOT}"), "{detail}");
assert!(detail.contains("skipped rm: this harness cannot host that transport"), "{detail}");
}