use std::collections::BTreeMap;
use std::path::PathBuf;
use serde_json::{Value, json};
use super::{BackendState, portable_names, probe_mcp, reconcile_mcp, remove_mcp, render_mcp_server};
use crate::components::{McpKind, McpServer};
fn scratch() -> PathBuf {
let dir = crate::scratch::path("ez-kilo-unit");
std::fs::create_dir_all(&dir).unwrap();
dir.join("kilo.json")
}
fn server(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(),
}
}
fn read(path: &std::path::Path) -> Value {
serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap()
}
#[test]
fn portable_names_excludes_claude_plugin_root_servers() {
let servers = [server("ez-fixture", "host_fixture", &["mcp"]), server("rooted", "${CLAUDE_PLUGIN_ROOT}/bin/leaky", &[])];
assert_eq!(portable_names(&servers), vec!["ez-fixture"]);
}
#[test]
fn reconcile_writes_the_opencode_shape_and_second_run_is_a_noop() {
let path = scratch();
let servers = [server("ez-fixture", "host_fixture", &["mcp"])];
assert!(reconcile_mcp(&path, &servers, true).unwrap(), "first reconcile must write");
let root = read(&path);
assert_eq!(
root["mcp"]["ez-fixture"],
json!({ "type": "local", "command": ["host_fixture", "mcp"], "environment": {}, "enabled": true }),
"written server body drifted from the kilo/opencode shape:\n{root:#}",
);
assert!(!reconcile_mcp(&path, &servers, true).unwrap(), "an already-converged reconcile must not write");
}
#[test]
fn reconcile_skips_non_portable_and_leaves_the_user_config_untouched() {
let path = scratch();
std::fs::write(&path, r#"{ "theme": "dark", "mcp": { "theirs": { "type": "local", "command": ["their-server"], "enabled": true } } }"#)
.unwrap();
let servers = [server("ez-fixture", "host_fixture", &["mcp"]), server("rooted", "${CLAUDE_PLUGIN_ROOT}/bin/leaky", &[])];
assert!(reconcile_mcp(&path, &servers, true).unwrap(), "reconcile must add our portable server");
let root = read(&path);
assert!(root["mcp"].get("ez-fixture").is_some(), "portable server missing:\n{root:#}");
assert!(root["mcp"].get("rooted").is_none(), "non-portable server leaked into kilo.json:\n{root:#}");
assert_eq!(root["theme"], json!("dark"), "unrelated top-level key was clobbered:\n{root:#}");
assert!(root["mcp"].get("theirs").is_some(), "seeded user server was clobbered:\n{root:#}");
}
#[test]
fn remove_deletes_only_ours_and_never_a_same_named_user_entry() {
let path = scratch();
let servers = [server("ez-fixture", "host_fixture", &["mcp"]), server("rooted", "${CLAUDE_PLUGIN_ROOT}/bin/leaky", &[])];
std::fs::write(&path, r#"{ "mcp": { "rooted": { "type": "local", "command": ["users-own"], "enabled": true } } }"#).unwrap();
reconcile_mcp(&path, &servers, true).unwrap();
assert!(remove_mcp(&path, &portable_names(&servers)).unwrap(), "remove must delete our written server");
let root = read(&path);
assert!(root["mcp"].get("ez-fixture").is_none(), "our server survived remove:\n{root:#}");
assert_eq!(
root["mcp"]["rooted"],
json!({ "type": "local", "command": ["users-own"], "enabled": true }),
"a same-named user entry we never wrote was deleted:\n{root:#}",
);
}
#[test]
fn probe_classifies_absent_healthy_and_needs_repair() {
let path = scratch();
let servers = [server("ez-fixture", "host_fixture", &["mcp"])];
assert!(matches!(probe_mcp(&path, &servers).unwrap(), BackendState::Absent), "a missing config must probe Absent");
std::fs::write(&path, r#"{ "mcp": { "theirs": { "type": "local", "command": ["their-server"], "enabled": true } } }"#).unwrap();
assert!(matches!(probe_mcp(&path, &servers).unwrap(), BackendState::Absent), "our server absent must probe Absent");
reconcile_mcp(&path, &servers, true).unwrap();
assert!(matches!(probe_mcp(&path, &servers).unwrap(), BackendState::Healthy), "a converged install must probe Healthy");
let mut root = read(&path);
root["mcp"]["ez-fixture"] = json!({ "type": "local", "command": ["stale-binary"], "environment": {}, "enabled": true });
std::fs::write(&path, serde_json::to_vec(&root).unwrap()).unwrap();
assert!(matches!(probe_mcp(&path, &servers).unwrap(), BackendState::NeedsRepair), "a drifted install must probe NeedsRepair");
}
#[test]
fn probe_is_healthy_never_absent_for_a_plugin_with_no_portable_servers() {
let path = scratch();
let servers = [server("rooted", "${CLAUDE_PLUGIN_ROOT}/bin/leaky", &[])];
assert!(!path.exists());
assert!(matches!(probe_mcp(&path, &servers).unwrap(), BackendState::Healthy), "no portable servers must probe Healthy, not Absent");
}
#[test]
fn self_heal_never_reenables_a_user_disabled_server() {
let path = scratch();
let servers = [server("ez-fixture", "host_fixture", &["mcp"])];
reconcile_mcp(&path, &servers, true).unwrap();
let mut root = read(&path);
root["mcp"]["ez-fixture"] = render_mcp_server(&servers[0], false);
std::fs::write(&path, serde_json::to_vec(&root).unwrap()).unwrap();
assert!(matches!(probe_mcp(&path, &servers).unwrap(), BackendState::Disabled), "a user-disabled server must probe Disabled");
assert!(!reconcile_mcp(&path, &servers, false).unwrap(), "self_heal must not re-enable a user disable");
assert_eq!(read(&path)["mcp"]["ez-fixture"]["enabled"], json!(false), "self_heal flipped a user disable back on");
assert!(reconcile_mcp(&path, &servers, true).unwrap(), "explicit reconcile must re-enable");
assert_eq!(read(&path)["mcp"]["ez-fixture"]["enabled"], json!(true), "explicit reconcile did not re-enable");
}