test: Implemented tests for the MCP server lifecycle
This commit is contained in:
@@ -137,3 +137,203 @@ impl McpFactory {
|
||||
Ok(handle)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mcp::{JsonField, McpServer, McpTransportType};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn stdio_spec(
|
||||
command: &str,
|
||||
args: Option<Vec<String>>,
|
||||
env: Option<HashMap<String, JsonField>>,
|
||||
) -> McpServer {
|
||||
McpServer {
|
||||
transport_type: McpTransportType::Stdio,
|
||||
command: Some(command.to_string()),
|
||||
args,
|
||||
env,
|
||||
cwd: None,
|
||||
url: None,
|
||||
headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_spec(
|
||||
transport: McpTransportType,
|
||||
url: &str,
|
||||
headers: Option<HashMap<String, String>>,
|
||||
) -> McpServer {
|
||||
McpServer {
|
||||
transport_type: transport,
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: Some(url.to_string()),
|
||||
headers,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_stdio_spec_captures_command_args_env() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("TOKEN".into(), JsonField::Str("abc".into()));
|
||||
let spec = stdio_spec("npx", Some(vec!["-y".into(), "server".into()]), Some(env));
|
||||
let key = McpServerKey::from_spec("my-server", &spec);
|
||||
|
||||
assert_eq!(key.name, "my-server");
|
||||
match &key.transport {
|
||||
McpTransportKey::Stdio { command, args, env } => {
|
||||
assert_eq!(command, "npx");
|
||||
assert_eq!(args, &["-y", "server"]);
|
||||
assert_eq!(env, &[("TOKEN".to_string(), "abc".to_string())]);
|
||||
}
|
||||
_ => panic!("expected Stdio transport key"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_stdio_spec_sorts_args_and_env() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("Z_VAR".into(), JsonField::Str("z".into()));
|
||||
env.insert("A_VAR".into(), JsonField::Int(42));
|
||||
let spec = stdio_spec(
|
||||
"cmd",
|
||||
Some(vec!["charlie".into(), "alpha".into(), "bravo".into()]),
|
||||
Some(env),
|
||||
);
|
||||
let key = McpServerKey::from_spec("s", &spec);
|
||||
|
||||
match &key.transport {
|
||||
McpTransportKey::Stdio { args, env, .. } => {
|
||||
assert_eq!(args, &["alpha", "bravo", "charlie"]);
|
||||
assert_eq!(env[0].0, "A_VAR");
|
||||
assert_eq!(env[0].1, "42");
|
||||
assert_eq!(env[1].0, "Z_VAR");
|
||||
assert_eq!(env[1].1, "z");
|
||||
}
|
||||
_ => panic!("expected Stdio"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_stdio_spec_defaults_empty_when_none() {
|
||||
let spec = stdio_spec("echo", None, None);
|
||||
let key = McpServerKey::from_spec("bare", &spec);
|
||||
|
||||
match &key.transport {
|
||||
McpTransportKey::Stdio { command, args, env } => {
|
||||
assert_eq!(command, "echo");
|
||||
assert!(args.is_empty());
|
||||
assert!(env.is_empty());
|
||||
}
|
||||
_ => panic!("expected Stdio"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_remote_http_spec() {
|
||||
let spec = remote_spec(McpTransportType::Http, "http://localhost:8080", None);
|
||||
let key = McpServerKey::from_spec("http-srv", &spec);
|
||||
|
||||
assert_eq!(key.name, "http-srv");
|
||||
match &key.transport {
|
||||
McpTransportKey::Remote {
|
||||
transport_type,
|
||||
url,
|
||||
headers,
|
||||
} => {
|
||||
assert_eq!(*transport_type, McpTransportType::Http);
|
||||
assert_eq!(url, "http://localhost:8080");
|
||||
assert!(headers.is_empty());
|
||||
}
|
||||
_ => panic!("expected Remote"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_from_remote_sse_spec_with_sorted_headers() {
|
||||
let mut hdrs = HashMap::new();
|
||||
hdrs.insert("Z-Key".into(), "z-val".into());
|
||||
hdrs.insert("A-Key".into(), "a-val".into());
|
||||
let spec = remote_spec(McpTransportType::Sse, "http://sse.example.com", Some(hdrs));
|
||||
let key = McpServerKey::from_spec("sse-srv", &spec);
|
||||
|
||||
match &key.transport {
|
||||
McpTransportKey::Remote { headers, .. } => {
|
||||
assert_eq!(headers[0], ("A-Key".to_string(), "a-val".to_string()));
|
||||
assert_eq!(headers[1], ("Z-Key".to_string(), "z-val".to_string()));
|
||||
}
|
||||
_ => panic!("expected Remote"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_equality_same_spec_produces_equal_keys() {
|
||||
let spec = stdio_spec("npx", Some(vec!["a".into()]), None);
|
||||
let k1 = McpServerKey::from_spec("s", &spec);
|
||||
let k2 = McpServerKey::from_spec("s", &spec);
|
||||
assert_eq!(k1, k2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_inequality_different_names() {
|
||||
let spec = stdio_spec("npx", None, None);
|
||||
let k1 = McpServerKey::from_spec("a", &spec);
|
||||
let k2 = McpServerKey::from_spec("b", &spec);
|
||||
assert_ne!(k1, k2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_inequality_different_commands() {
|
||||
let s1 = stdio_spec("npx", None, None);
|
||||
let s2 = stdio_spec("node", None, None);
|
||||
let k1 = McpServerKey::from_spec("s", &s1);
|
||||
let k2 = McpServerKey::from_spec("s", &s2);
|
||||
assert_ne!(k1, k2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_env_bool_and_int_coerce_to_string() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("FLAG".into(), JsonField::Bool(true));
|
||||
env.insert("PORT".into(), JsonField::Int(3000));
|
||||
let spec = stdio_spec("cmd", None, Some(env));
|
||||
let key = McpServerKey::from_spec("s", &spec);
|
||||
|
||||
match &key.transport {
|
||||
McpTransportKey::Stdio { env, .. } => {
|
||||
let map: HashMap<&str, &str> =
|
||||
env.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
assert_eq!(map["FLAG"], "true");
|
||||
assert_eq!(map["PORT"], "3000");
|
||||
}
|
||||
_ => panic!("expected Stdio"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_try_get_active_returns_none_when_empty() {
|
||||
let factory = McpFactory::default();
|
||||
let spec = stdio_spec("cmd", None, None);
|
||||
let key = McpServerKey::from_spec("s", &spec);
|
||||
assert!(factory.try_get_active(&key).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_try_get_active_returns_none_for_unknown_key() {
|
||||
let factory = McpFactory::default();
|
||||
let spec = stdio_spec("cmd", None, None);
|
||||
let key = McpServerKey::from_spec("s", &spec);
|
||||
assert!(factory.try_get_active(&key).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn factory_default_has_empty_active_map() {
|
||||
let factory = McpFactory::default();
|
||||
let map = factory.active.lock();
|
||||
assert!(map.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2373,6 +2373,11 @@ mod tests {
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use crate::function::ToolCall;
|
||||
use crate::mcp::{McpServer, McpServersConfig, McpTransportType};
|
||||
use crate::utils;
|
||||
use crate::vault::Vault;
|
||||
use super::super::mcp_factory::McpFactory;
|
||||
|
||||
struct TestConfigDirGuard {
|
||||
key: String,
|
||||
@@ -2419,8 +2424,8 @@ mod tests {
|
||||
fn default_app_state() -> Arc<AppState> {
|
||||
Arc::new(AppState {
|
||||
config: Arc::new(AppConfig::default()),
|
||||
vault: Arc::new(crate::vault::Vault::default()),
|
||||
mcp_factory: Arc::new(super::super::mcp_factory::McpFactory::default()),
|
||||
vault: Arc::new(Vault::default()),
|
||||
mcp_factory: Arc::new(McpFactory::default()),
|
||||
rag_cache: Arc::new(RagCache::default()),
|
||||
mcp_config: None,
|
||||
mcp_log_path: None,
|
||||
@@ -2563,7 +2568,7 @@ mod tests {
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
ctx.use_agent(&app, &agent_name, None, crate::utils::create_abort_signal())
|
||||
ctx.use_agent(&app, &agent_name, None, utils::create_abort_signal())
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
@@ -2606,4 +2611,157 @@ mod tests {
|
||||
let ctx = create_test_ctx();
|
||||
assert!(ctx.root_escalation_queue().is_none());
|
||||
}
|
||||
|
||||
fn app_state_with_mcp_config(mcp_server_support: bool, server_names: &[&str]) -> Arc<AppState> {
|
||||
let mut app_config = AppConfig::default();
|
||||
app_config.mcp_server_support = mcp_server_support;
|
||||
|
||||
let mcp_config = if server_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut servers = HashMap::new();
|
||||
for name in server_names {
|
||||
servers.insert(
|
||||
name.to_string(),
|
||||
McpServer {
|
||||
transport_type: McpTransportType::Stdio,
|
||||
command: Some("echo".to_string()),
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: None,
|
||||
headers: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(McpServersConfig {
|
||||
mcp_servers: servers,
|
||||
})
|
||||
};
|
||||
|
||||
Arc::new(AppState {
|
||||
config: Arc::new(app_config),
|
||||
vault: Arc::new(Vault::default()),
|
||||
mcp_factory: Arc::new(McpFactory::default()),
|
||||
rag_cache: Arc::new(RagCache::default()),
|
||||
mcp_config,
|
||||
mcp_log_path: None,
|
||||
mcp_registry: None,
|
||||
functions: Functions::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn run_async<F: Future>(f: F) -> F::Output {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(f)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_tool_scope_mcp_disabled_skips_servers() {
|
||||
let app_state = app_state_with_mcp_config(false, &["github", "slack"]);
|
||||
let mut ctx = RequestContext::new(app_state, WorkingMode::Cmd);
|
||||
let app = ctx.app.config.clone();
|
||||
let abort = utils::create_abort_signal();
|
||||
|
||||
run_async(ctx.rebuild_tool_scope(&app, Some("all".to_string()), abort)).unwrap();
|
||||
|
||||
assert!(ctx.tool_scope.mcp_runtime.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_tool_scope_no_enabled_servers_yields_empty_runtime() {
|
||||
let app_state = app_state_with_mcp_config(true, &["github"]);
|
||||
let mut ctx = RequestContext::new(app_state, WorkingMode::Cmd);
|
||||
let app = ctx.app.config.clone();
|
||||
let abort = utils::create_abort_signal();
|
||||
|
||||
run_async(ctx.rebuild_tool_scope(&app, None, abort)).unwrap();
|
||||
|
||||
assert!(ctx.tool_scope.mcp_runtime.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_tool_scope_no_mcp_config_yields_empty_runtime() {
|
||||
let app_state = app_state_with_mcp_config(true, &[]);
|
||||
let mut ctx = RequestContext::new(app_state, WorkingMode::Cmd);
|
||||
let app = ctx.app.config.clone();
|
||||
let abort = utils::create_abort_signal();
|
||||
|
||||
run_async(ctx.rebuild_tool_scope(&app, Some("all".to_string()), abort)).unwrap();
|
||||
|
||||
assert!(ctx.tool_scope.mcp_runtime.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_tool_scope_preserves_tool_tracker() {
|
||||
let app_state = app_state_with_mcp_config(false, &[]);
|
||||
let mut ctx = RequestContext::new(app_state, WorkingMode::Cmd);
|
||||
let dummy = ToolCall {
|
||||
name: "test_tool".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
ctx.tool_scope.tool_tracker.record_call(dummy);
|
||||
|
||||
let app = ctx.app.config.clone();
|
||||
let abort = utils::create_abort_signal();
|
||||
run_async(ctx.rebuild_tool_scope(&app, None, abort)).unwrap();
|
||||
|
||||
let check_call = ToolCall {
|
||||
name: "test_tool".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
ctx.tool_scope
|
||||
.tool_tracker
|
||||
.check_loop(&check_call)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_tool_scope_repl_mode_appends_user_interaction_functions() {
|
||||
let app_state = app_state_with_mcp_config(false, &[]);
|
||||
let mut ctx = RequestContext::new(app_state, WorkingMode::Repl);
|
||||
let app = ctx.app.config.clone();
|
||||
let abort = utils::create_abort_signal();
|
||||
|
||||
run_async(ctx.rebuild_tool_scope(&app, None, abort)).unwrap();
|
||||
|
||||
let names: Vec<String> = ctx
|
||||
.tool_scope
|
||||
.functions
|
||||
.declarations()
|
||||
.iter()
|
||||
.map(|f| f.name.clone())
|
||||
.collect();
|
||||
assert!(
|
||||
names.iter().any(|n| n.starts_with("user__")),
|
||||
"REPL mode should include user interaction functions, got: {names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_tool_scope_cmd_mode_no_user_interaction_functions() {
|
||||
let app_state = app_state_with_mcp_config(false, &[]);
|
||||
let mut ctx = RequestContext::new(app_state, WorkingMode::Cmd);
|
||||
let app = ctx.app.config.clone();
|
||||
let abort = utils::create_abort_signal();
|
||||
|
||||
run_async(ctx.rebuild_tool_scope(&app, None, abort)).unwrap();
|
||||
|
||||
let names: Vec<String> = ctx
|
||||
.tool_scope
|
||||
.functions
|
||||
.declarations()
|
||||
.iter()
|
||||
.map(|f| f.name.clone())
|
||||
.collect();
|
||||
assert!(
|
||||
!names.iter().any(|n| n.starts_with("user__")),
|
||||
"CMD mode should NOT include user interaction functions, got: {names:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,3 +165,47 @@ impl McpRuntime {
|
||||
server_handle.call_tool(request).await.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::function::ToolCall;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mcp_runtime_new_is_empty() {
|
||||
let runtime = McpRuntime::new();
|
||||
assert!(runtime.is_empty());
|
||||
assert!(runtime.server_names().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_runtime_default_is_empty() {
|
||||
let runtime = McpRuntime::default();
|
||||
assert!(runtime.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_runtime_get_returns_none_for_missing_server() {
|
||||
let runtime = McpRuntime::new();
|
||||
assert!(runtime.get("nonexistent").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_scope_default_has_empty_mcp_runtime() {
|
||||
let scope = ToolScope::default();
|
||||
assert!(scope.mcp_runtime.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_scope_default_has_empty_functions() {
|
||||
let scope = ToolScope::default();
|
||||
assert!(scope.functions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_scope_default_tracker_has_no_loops() {
|
||||
let scope = ToolScope::default();
|
||||
let dummy_call = ToolCall::default();
|
||||
assert!(scope.tool_tracker.check_loop(&dummy_call).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
+392
@@ -440,3 +440,395 @@ async fn spawn_stdio_mcp_server(
|
||||
);
|
||||
Ok(service)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn stdio_server(command: &str) -> McpServer {
|
||||
McpServer {
|
||||
transport_type: McpTransportType::Stdio,
|
||||
command: Some(command.to_string()),
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: None,
|
||||
headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn http_server(url: &str) -> McpServer {
|
||||
McpServer {
|
||||
transport_type: McpTransportType::Http,
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: Some(url.to_string()),
|
||||
headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sse_server(url: &str) -> McpServer {
|
||||
McpServer {
|
||||
transport_type: McpTransportType::Sse,
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: Some(url.to_string()),
|
||||
headers: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_registry_with_config(server_names: &[&str]) -> McpRegistry {
|
||||
let mut mcp_servers = HashMap::new();
|
||||
for name in server_names {
|
||||
mcp_servers.insert(name.to_string(), stdio_server("echo"));
|
||||
}
|
||||
McpRegistry {
|
||||
config: Some(McpServersConfig { mcp_servers }),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_stdio_with_command_succeeds() {
|
||||
let spec = stdio_server("npx");
|
||||
assert!(spec.validate("test").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_stdio_missing_command_fails() {
|
||||
let spec = McpServer {
|
||||
transport_type: McpTransportType::Stdio,
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: None,
|
||||
headers: None,
|
||||
};
|
||||
let err = spec.validate("test").unwrap_err();
|
||||
assert!(err.to_string().contains("missing a \"command\" field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_stdio_with_url_fails() {
|
||||
let spec = McpServer {
|
||||
transport_type: McpTransportType::Stdio,
|
||||
command: Some("cmd".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: Some("http://localhost".into()),
|
||||
headers: None,
|
||||
};
|
||||
let err = spec.validate("test").unwrap_err();
|
||||
assert!(err.to_string().contains("remote fields"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_stdio_with_headers_fails() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Auth".into(), "Bearer tok".into());
|
||||
let spec = McpServer {
|
||||
transport_type: McpTransportType::Stdio,
|
||||
command: Some("cmd".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: None,
|
||||
headers: Some(headers),
|
||||
};
|
||||
let err = spec.validate("test").unwrap_err();
|
||||
assert!(err.to_string().contains("remote fields"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_http_with_url_succeeds() {
|
||||
let spec = http_server("http://localhost:8080");
|
||||
assert!(spec.validate("test").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_http_missing_url_fails() {
|
||||
let spec = McpServer {
|
||||
transport_type: McpTransportType::Http,
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: None,
|
||||
headers: None,
|
||||
};
|
||||
let err = spec.validate("test").unwrap_err();
|
||||
assert!(err.to_string().contains("missing a \"url\" field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_http_with_command_fails() {
|
||||
let spec = McpServer {
|
||||
transport_type: McpTransportType::Http,
|
||||
command: Some("npx".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: Some("http://localhost".into()),
|
||||
headers: None,
|
||||
};
|
||||
let err = spec.validate("test").unwrap_err();
|
||||
assert!(err.to_string().contains("stdio fields"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_http_with_args_fails() {
|
||||
let spec = McpServer {
|
||||
transport_type: McpTransportType::Http,
|
||||
command: None,
|
||||
args: Some(vec!["--flag".into()]),
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: Some("http://localhost".into()),
|
||||
headers: None,
|
||||
};
|
||||
let err = spec.validate("test").unwrap_err();
|
||||
assert!(err.to_string().contains("stdio fields"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_http_with_cwd_fails() {
|
||||
let spec = McpServer {
|
||||
transport_type: McpTransportType::Http,
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: Some("/tmp".into()),
|
||||
url: Some("http://localhost".into()),
|
||||
headers: None,
|
||||
};
|
||||
let err = spec.validate("test").unwrap_err();
|
||||
assert!(err.to_string().contains("stdio fields"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_sse_with_url_succeeds() {
|
||||
let spec = sse_server("http://sse.example.com");
|
||||
assert!(spec.validate("test").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_sse_missing_url_fails() {
|
||||
let spec = McpServer {
|
||||
transport_type: McpTransportType::Sse,
|
||||
command: None,
|
||||
args: None,
|
||||
env: None,
|
||||
cwd: None,
|
||||
url: None,
|
||||
headers: None,
|
||||
};
|
||||
let err = spec.validate("test").unwrap_err();
|
||||
assert!(err.to_string().contains("missing a \"url\" field"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_remote_true_for_http_and_sse() {
|
||||
assert!(http_server("http://x").is_remote());
|
||||
assert!(sse_server("http://x").is_remote());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_remote_false_for_stdio() {
|
||||
assert!(!stdio_server("cmd").is_remote());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_stdio_server_from_json() {
|
||||
let json = r#"{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server"]
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let config: McpServersConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(config.mcp_servers.contains_key("my-server"));
|
||||
let spec = &config.mcp_servers["my-server"];
|
||||
assert_eq!(spec.transport_type, McpTransportType::Stdio);
|
||||
assert_eq!(spec.command.as_deref(), Some("npx"));
|
||||
assert_eq!(
|
||||
spec.args.as_ref().unwrap(),
|
||||
&["-y", "@modelcontextprotocol/server"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_http_server_from_json() {
|
||||
let json = r#"{
|
||||
"mcpServers": {
|
||||
"remote": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:8080/mcp",
|
||||
"headers": {"Authorization": "Bearer tok"}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let config: McpServersConfig = serde_json::from_str(json).unwrap();
|
||||
let spec = &config.mcp_servers["remote"];
|
||||
assert_eq!(spec.transport_type, McpTransportType::Http);
|
||||
assert_eq!(spec.url.as_deref(), Some("http://localhost:8080/mcp"));
|
||||
assert_eq!(
|
||||
spec.headers.as_ref().unwrap()["Authorization"],
|
||||
"Bearer tok"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_env_with_mixed_types() {
|
||||
let json = r#"{
|
||||
"mcpServers": {
|
||||
"s": {
|
||||
"type": "stdio",
|
||||
"command": "cmd",
|
||||
"env": {
|
||||
"STR_VAR": "hello",
|
||||
"BOOL_VAR": true,
|
||||
"INT_VAR": 42
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let config: McpServersConfig = serde_json::from_str(json).unwrap();
|
||||
let env = config.mcp_servers["s"].env.as_ref().unwrap();
|
||||
assert!(matches!(env["STR_VAR"], JsonField::Str(ref s) if s == "hello"));
|
||||
assert!(matches!(env["BOOL_VAR"], JsonField::Bool(true)));
|
||||
assert!(matches!(env["INT_VAR"], JsonField::Int(42)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_multiple_servers() {
|
||||
let json = r#"{
|
||||
"mcpServers": {
|
||||
"github": { "type": "stdio", "command": "gh-mcp" },
|
||||
"remote-api": { "type": "http", "url": "http://api.example.com" }
|
||||
}
|
||||
}"#;
|
||||
let config: McpServersConfig = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(config.mcp_servers.len(), 2);
|
||||
assert!(config.mcp_servers.contains_key("github"));
|
||||
assert!(config.mcp_servers.contains_key("remote-api"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_empty_servers_map() {
|
||||
let json = r#"{ "mcpServers": {} }"#;
|
||||
let config: McpServersConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(config.mcp_servers.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_server_with_cwd() {
|
||||
let json = r#"{
|
||||
"mcpServers": {
|
||||
"s": {
|
||||
"type": "stdio",
|
||||
"command": "cmd",
|
||||
"cwd": "/tmp/work"
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let config: McpServersConfig = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(config.mcp_servers["s"].cwd.as_deref(), Some("/tmp/work"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_all_returns_all_configured_servers() {
|
||||
let registry = make_registry_with_config(&["github", "slack", "jira"]);
|
||||
let mut ids = registry.resolve_server_ids(Some("all".to_string()));
|
||||
ids.sort();
|
||||
assert_eq!(ids, vec!["github", "jira", "slack"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_comma_separated_returns_matching_servers() {
|
||||
let registry = make_registry_with_config(&["github", "slack", "jira"]);
|
||||
let mut ids = registry.resolve_server_ids(Some("github, jira".to_string()));
|
||||
ids.sort();
|
||||
assert_eq!(ids, vec!["github", "jira"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_single_server_name() {
|
||||
let registry = make_registry_with_config(&["github", "slack"]);
|
||||
let ids = registry.resolve_server_ids(Some("slack".to_string()));
|
||||
assert_eq!(ids, vec!["slack"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_none_returns_empty() {
|
||||
let registry = make_registry_with_config(&["github"]);
|
||||
let ids = registry.resolve_server_ids(None);
|
||||
assert!(ids.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_no_config_returns_empty() {
|
||||
let registry = McpRegistry::default();
|
||||
let ids = registry.resolve_server_ids(Some("all".to_string()));
|
||||
assert!(ids.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_nonexistent_server_filtered_out() {
|
||||
let registry = make_registry_with_config(&["github"]);
|
||||
let ids = registry.resolve_server_ids(Some("github, nonexistent".to_string()));
|
||||
assert_eq!(ids, vec!["github"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_all_nonexistent_returns_empty() {
|
||||
let registry = make_registry_with_config(&["github"]);
|
||||
let ids = registry.resolve_server_ids(Some("foo, bar".to_string()));
|
||||
assert!(ids.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_trims_whitespace() {
|
||||
let registry = make_registry_with_config(&["github", "slack"]);
|
||||
let mut ids = registry.resolve_server_ids(Some(" github , slack ".to_string()));
|
||||
ids.sort();
|
||||
assert_eq!(ids, vec!["github", "slack"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_default_is_empty() {
|
||||
let registry = McpRegistry::default();
|
||||
assert!(registry.is_empty());
|
||||
assert!(registry.list_started_servers().is_empty());
|
||||
assert!(registry.mcp_config().is_none());
|
||||
assert!(registry.log_path().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_with_config_reports_config() {
|
||||
let registry = make_registry_with_config(&["github"]);
|
||||
assert!(registry.mcp_config().is_some());
|
||||
assert!(
|
||||
registry
|
||||
.mcp_config()
|
||||
.unwrap()
|
||||
.mcp_servers
|
||||
.contains_key("github")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn meta_function_prefixes_are_correct() {
|
||||
assert_eq!(MCP_INVOKE_META_FUNCTION_NAME_PREFIX, "mcp_invoke");
|
||||
assert_eq!(MCP_SEARCH_META_FUNCTION_NAME_PREFIX, "mcp_search");
|
||||
assert_eq!(MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, "mcp_describe");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user