use std::collections::BTreeMap;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use super::{config_disables_claude_plugins, doc_file, flat_stem, render_agent, resolve_omp_root};
use crate::agents::BackendState;
use crate::agents::mcpjson::{self, ServerShape};
use crate::components::{MarkdownDoc, McpKind, McpServer};
use crate::host::Outcome;
const KEY: &[&str] = &["mcpServers"];
fn scratch(name: &str) -> PathBuf {
let dir = crate::scratch::path("ez-omp-unit");
std::fs::create_dir_all(&dir).unwrap();
dir.join(name)
}
fn server(name: &str, command: &str, args: &[&str]) -> McpServer {
McpServer {
name: name.into(),
kind: McpKind::Stdio,
command: command.into(),
args: args.iter().map(|s| s.to_string()).collect(),
env: BTreeMap::new(),
}
}
fn agent_doc(name: &str, rel: &str, extra: &[(&str, &str)], body: &str) -> MarkdownDoc {
let mut frontmatter = BTreeMap::new();
frontmatter.insert("name".into(), Value::from(name));
for (k, v) in extra {
frontmatter.insert((*k).into(), Value::from(*v));
}
MarkdownDoc { name: name.into(), rel: rel.into(), frontmatter, body: body.into(), raw: body.as_bytes().to_vec() }
}
#[test]
fn resolve_omp_root_keeps_an_absolute_pi_config_dir_under_home() {
let home = Path::new("/home/user");
assert_eq!(resolve_omp_root(OsStr::new("/abs/dir"), Some(home)), Some(home.join("abs/dir")));
}
#[test]
fn resolve_omp_root_keeps_a_relative_pi_config_dir_and_the_default() {
let home = Path::new("/home/user");
assert_eq!(resolve_omp_root(OsStr::new("custom"), Some(home)), Some(home.join("custom")));
assert_eq!(resolve_omp_root(OsStr::new(".omp"), Some(home)), Some(home.join(".omp")));
assert_eq!(resolve_omp_root(OsStr::new(".omp"), None), None);
}
#[test]
fn doc_file_flattens_and_prefixes() {
assert_eq!(doc_file("ez-fixture-plugin", "commands/hello.md", "commands/"), "ez-fixture-plugin-hello.md");
assert_eq!(doc_file("ez-fixture-plugin", "commands/sub/nested.md", "commands/"), "ez-fixture-plugin-sub-nested.md");
assert_eq!(doc_file("ez-fixture-plugin", "agents/ez-helper.md", "agents/"), "ez-fixture-plugin-ez-helper.md");
assert_eq!(flat_stem("commands/hello", "commands/"), "hello");
}
#[test]
fn render_agent_re_emits_name_and_description_and_drops_model() {
let doc = agent_doc(
"ez-helper",
"agents/ez-helper.md",
&[("description", "fixture subagent so the plugin exercises an agent-def component"), ("model", "sonnet")],
"A fixture helper agent body that becomes systemPrompt.",
);
let out = render_agent("ez-fixture-plugin", &doc);
assert!(out.starts_with("---\n"), "frontmatter fence missing:\n{out}");
assert!(out.contains("name: ez-fixture-plugin-ez-helper\n"), "namespaced name missing:\n{out}");
assert!(out.contains("description: fixture subagent so the plugin exercises an agent-def component\n"), "description missing:\n{out}");
assert!(!out.contains("model") && !out.contains("sonnet"), "CC model alias leaked:\n{out}");
assert!(out.contains("A fixture helper agent body that becomes systemPrompt."), "body missing:\n{out}");
}
#[test]
fn render_agent_quotes_a_colon_bearing_description() {
let doc = agent_doc("x", "agents/x.md", &[("description", "does: things")], "body");
assert!(render_agent("p", &doc).contains("description: \"does: things\"\n"));
}
#[test]
fn config_disables_claude_plugins_reads_disabled_providers() {
let path = scratch("config.yml");
assert!(!config_disables_claude_plugins(&path));
std::fs::write(&path, "disabledProviders: []\n").unwrap();
assert!(!config_disables_claude_plugins(&path));
std::fs::write(&path, "disabledProviders:\n - some-other-provider\n").unwrap();
assert!(!config_disables_claude_plugins(&path));
std::fs::write(&path, "disabledProviders:\n - claude-plugins\n").unwrap();
assert!(config_disables_claude_plugins(&path));
std::fs::write(&path, "disabledProviders:\n - path: /some/dir\n providers: [claude-plugins]\n").unwrap();
assert!(config_disables_claude_plugins(&path));
std::fs::write(&path, "otherKey:\n - claude-plugins\ndisabledProviders: []\n").unwrap();
assert!(!config_disables_claude_plugins(&path));
std::fs::remove_dir_all(path.parent().unwrap()).ok();
}
#[test]
fn mcp_reconcile_writes_plain_shape_then_no_ops_and_preserves_a_user_entry() {
let path = scratch("mcp.json");
std::fs::write(&path, r#"{"$schema":"https://x","mcpServers":{"theirs":{"command":"their-server","args":[]}}}"#).unwrap();
let servers = [server("ez-fixture", "host_fixture", &["mcp"])];
assert_eq!(mcpjson::reconcile(&path, KEY, &servers, ServerShape::plain()).unwrap(), Outcome::Installed);
let root: Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
assert_eq!(root["mcpServers"]["ez-fixture"], json!({ "command": "host_fixture", "args": ["mcp"], "env": {} }));
assert_eq!(root["mcpServers"]["theirs"], json!({ "command": "their-server", "args": [] }));
assert_eq!(root["$schema"], json!("https://x"));
assert_eq!(mcpjson::reconcile(&path, KEY, &servers, ServerShape::plain()).unwrap(), Outcome::NoOp);
std::fs::remove_dir_all(path.parent().unwrap()).ok();
}
#[test]
fn mcp_probe_classifies_absent_healthy_and_needs_repair() {
let path = scratch("mcp.json");
let servers = [server("ez-fixture", "host_fixture", &["mcp"])];
std::fs::write(&path, r#"{"mcpServers":{}}"#).unwrap();
assert!(matches!(mcpjson::probe(&path, KEY, &servers, ServerShape::plain()).unwrap(), BackendState::Absent));
mcpjson::reconcile(&path, KEY, &servers, ServerShape::plain()).unwrap();
assert!(matches!(mcpjson::probe(&path, KEY, &servers, ServerShape::plain()).unwrap(), BackendState::Healthy));
std::fs::write(&path, r#"{"mcpServers":{"ez-fixture":{"command":"stale","args":[],"env":{}}}}"#).unwrap();
assert!(matches!(mcpjson::probe(&path, KEY, &servers, ServerShape::plain()).unwrap(), BackendState::NeedsRepair));
std::fs::remove_dir_all(path.parent().unwrap()).ok();
}
#[test]
fn mcp_remove_deletes_only_ours_and_a_non_portable_name_is_never_targeted() {
let path = scratch("mcp.json");
std::fs::write(&path, r#"{"mcpServers":{"theirs":{"command":"their-server","args":[]},"rooted":{"command":"user-owned"}}}"#).unwrap();
let servers = [server("ez-fixture", "host_fixture", &["mcp"]), server("rooted", "${CLAUDE_PLUGIN_ROOT}/bin/leaky", &[])];
mcpjson::reconcile(&path, KEY, &servers, ServerShape::plain()).unwrap();
let root: Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
assert!(root["mcpServers"].get("ez-fixture").is_some(), "portable server must be written");
assert_eq!(root["mcpServers"]["rooted"], json!({ "command": "user-owned" }), "non-portable name must not overwrite the user's entry");
assert_eq!(mcpjson::remove(&path, KEY, &servers, ServerShape::plain()).unwrap(), Outcome::Removed);
let root: Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
assert!(root["mcpServers"].get("ez-fixture").is_none(), "our server survived remove");
assert_eq!(root["mcpServers"]["theirs"], json!({ "command": "their-server", "args": [] }));
assert_eq!(root["mcpServers"]["rooted"], json!({ "command": "user-owned" }), "remove wrongly touched a same-named user entry");
assert!(matches!(mcpjson::probe(&path, KEY, &servers, ServerShape::plain()).unwrap(), BackendState::Absent));
std::fs::remove_dir_all(path.parent().unwrap()).ok();
}