#![cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use crate::mcp_integration::registry::{
PromptRegistry, ResourceContent, ResourceContentType, ResourceRegistry, ResourceTemplate,
ToolRegistry,
};
use crate::mcp_integration::types::{error_codes, McpError, ServerInfo, MCP_VERSION};
#[test]
fn test_server_info() {
let info = ServerInfo::default();
assert_eq!(info.protocol_version, MCP_VERSION);
assert_eq!(info.name, "PMAT Agent Server");
}
#[test]
fn test_tool_registry() {
let registry = ToolRegistry::new();
assert_eq!(registry.list().len(), 0);
}
#[test]
fn test_resource_registry() {
let registry = ResourceRegistry::new();
assert_eq!(registry.list().len(), 0);
}
#[test]
fn test_prompt_registry() {
let registry = PromptRegistry::new();
assert_eq!(registry.list().len(), 0);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod coverage_tests {
use crate::mcp_integration::registry::{
ContentPart, McpPrompt, McpResource, McpTool, PromptArgument, PromptContent, PromptMessage,
PromptMetadata, PromptRegistry, ResourceContent, ResourceContentType, ResourceRegistry,
ResourceTemplate, ToolMetadata, ToolRegistry,
};
use crate::mcp_integration::types::{
error_codes, LoggingCapabilities, McpError, McpNotification, McpRequest, McpResponse,
PromptsCapability, RequestId, ResourcesCapability, ServerCapabilities, ServerInfo,
ToolsCapability, MCP_VERSION,
};
use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
struct MockTool {
name: String,
description: String,
execute_count: AtomicUsize,
}
impl MockTool {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
description: format!("Mock tool: {}", name),
execute_count: AtomicUsize::new(0),
}
}
}
#[async_trait]
impl McpTool for MockTool {
fn metadata(&self) -> ToolMetadata {
ToolMetadata {
name: self.name.clone(),
description: self.description.clone(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"input": { "type": "string" }
}
}),
}
}
async fn execute(&self, params: Value) -> Result<Value, McpError> {
self.execute_count.fetch_add(1, Ordering::SeqCst);
Ok(serde_json::json!({
"result": "executed",
"params": params
}))
}
}
struct MockResource {
uri_template: String,
name: String,
}
impl MockResource {
fn new(uri_template: &str, name: &str) -> Self {
Self {
uri_template: uri_template.to_string(),
name: name.to_string(),
}
}
}
#[async_trait]
impl McpResource for MockResource {
fn template(&self) -> ResourceTemplate {
ResourceTemplate {
uri_template: self.uri_template.clone(),
name: self.name.clone(),
description: Some(format!("Mock resource: {}", self.name)),
mime_type: Some("text/plain".to_string()),
}
}
async fn read(&self, uri: &str) -> Result<ResourceContent, McpError> {
Ok(ResourceContent {
uri: uri.to_string(),
mime_type: Some("text/plain".to_string()),
content: ResourceContentType::Text {
text: format!("Content for {}", uri),
},
})
}
fn subscribe(&self, _uri: &str) -> Option<tokio::sync::watch::Receiver<ResourceContent>> {
None
}
}
struct MockPrompt {
name: String,
description: String,
}
impl MockPrompt {
fn new(name: &str, description: &str) -> Self {
Self {
name: name.to_string(),
description: description.to_string(),
}
}
}
#[async_trait]
impl McpPrompt for MockPrompt {
fn metadata(&self) -> PromptMetadata {
PromptMetadata {
name: self.name.clone(),
description: Some(self.description.clone()),
arguments: Some(vec![PromptArgument {
name: "context".to_string(),
description: Some("Context for the prompt".to_string()),
required: Some(true),
}]),
}
}
async fn get(
&self,
arguments: Option<HashMap<String, String>>,
) -> Result<Vec<PromptMessage>, McpError> {
let context = arguments
.as_ref()
.and_then(|a| a.get("context"))
.map(|s| s.as_str())
.unwrap_or("default");
Ok(vec![PromptMessage {
role: "user".to_string(),
content: PromptContent::Text(format!("Prompt with context: {}", context)),
}])
}
}
#[test]
fn test_server_info_default_values() {
let info = ServerInfo::default();
assert_eq!(info.name, "PMAT Agent Server");
assert_eq!(info.protocol_version, MCP_VERSION);
assert!(!info.version.is_empty());
}
#[test]
fn test_server_info_clone() {
let info = ServerInfo::default();
let cloned = info.clone();
assert_eq!(info.name, cloned.name);
assert_eq!(info.version, cloned.version);
assert_eq!(info.protocol_version, cloned.protocol_version);
}
#[test]
fn test_server_info_debug() {
let info = ServerInfo::default();
let debug = format!("{:?}", info);
assert!(debug.contains("ServerInfo"));
assert!(debug.contains("PMAT"));
}
#[test]
fn test_tool_registry_new_is_empty() {
let registry = ToolRegistry::new();
assert_eq!(registry.list().len(), 0);
}
#[test]
fn test_tool_registry_default_is_empty() {
let registry = ToolRegistry::default();
assert_eq!(registry.list().len(), 0);
}
#[test]
fn test_tool_registry_register_single() {
let mut registry = ToolRegistry::new();
let tool = Arc::new(MockTool::new("test_tool"));
registry.register(tool);
assert_eq!(registry.list().len(), 1);
assert!(registry.get("test_tool").is_some());
}
#[test]
fn test_tool_registry_register_multiple() {
let mut registry = ToolRegistry::new();
for i in 0..5 {
let tool = Arc::new(MockTool::new(&format!("tool_{}", i)));
registry.register(tool);
}
assert_eq!(registry.list().len(), 5);
for i in 0..5 {
assert!(registry.get(&format!("tool_{}", i)).is_some());
}
}
#[test]
fn test_tool_registry_get_nonexistent() {
let registry = ToolRegistry::new();
assert!(registry.get("nonexistent").is_none());
}
#[test]
fn test_tool_registry_list_returns_metadata() {
let mut registry = ToolRegistry::new();
let tool = Arc::new(MockTool::new("my_tool"));
registry.register(tool);
let list = registry.list();
assert_eq!(list.len(), 1);
assert_eq!(list[0].name, "my_tool");
assert!(list[0].description.contains("Mock tool"));
}
#[test]
fn test_resource_registry_new_is_empty() {
let registry = ResourceRegistry::new();
assert_eq!(registry.list().len(), 0);
}
#[test]
fn test_resource_registry_default_is_empty() {
let registry = ResourceRegistry::default();
assert_eq!(registry.list().len(), 0);
}
#[test]
fn test_resource_registry_register_single() {
let mut registry = ResourceRegistry::new();
let resource = Arc::new(MockResource::new("file:///{}", "files"));
registry.register(resource);
assert_eq!(registry.list().len(), 1);
assert!(registry.get("file:///{}").is_some());
}
#[test]
fn test_resource_registry_register_multiple() {
let mut registry = ResourceRegistry::new();
registry.register(Arc::new(MockResource::new("file:///{}", "files")));
registry.register(Arc::new(MockResource::new("git:///{}", "git")));
registry.register(Arc::new(MockResource::new("http:///{}", "http")));
assert_eq!(registry.list().len(), 3);
}
#[test]
fn test_resource_registry_get_nonexistent() {
let registry = ResourceRegistry::new();
assert!(registry.get("nonexistent").is_none());
}
#[test]
fn test_resource_registry_find_matching() {
let mut registry = ResourceRegistry::new();
registry.register(Arc::new(MockResource::new("file:///{}", "files")));
let found = registry.find_matching("file:///path/to/file");
assert!(found.is_some());
let not_found = registry.find_matching("git:///repo");
assert!(not_found.is_none());
}
#[test]
fn test_resource_registry_list_returns_templates() {
let mut registry = ResourceRegistry::new();
registry.register(Arc::new(MockResource::new("file:///{}", "files")));
let list = registry.list();
assert_eq!(list.len(), 1);
assert_eq!(list[0].name, "files");
assert_eq!(list[0].uri_template, "file:///{}");
}
#[test]
fn test_prompt_registry_new_is_empty() {
let registry = PromptRegistry::new();
assert_eq!(registry.list().len(), 0);
}
#[test]
fn test_prompt_registry_default_is_empty() {
let registry = PromptRegistry::default();
assert_eq!(registry.list().len(), 0);
}
#[test]
fn test_prompt_registry_register_single() {
let mut registry = PromptRegistry::new();
let prompt = Arc::new(MockPrompt::new("test_prompt", "A test prompt"));
registry.register(prompt);
assert_eq!(registry.list().len(), 1);
assert!(registry.get("test_prompt").is_some());
}
#[test]
fn test_prompt_registry_register_multiple() {
let mut registry = PromptRegistry::new();
for i in 0..5 {
let prompt = Arc::new(MockPrompt::new(
&format!("prompt_{}", i),
&format!("Description {}", i),
));
registry.register(prompt);
}
assert_eq!(registry.list().len(), 5);
}
#[test]
fn test_prompt_registry_get_nonexistent() {
let registry = PromptRegistry::new();
assert!(registry.get("nonexistent").is_none());
}
#[test]
fn test_prompt_registry_list_returns_metadata() {
let mut registry = PromptRegistry::new();
registry.register(Arc::new(MockPrompt::new("my_prompt", "My description")));
let list = registry.list();
assert_eq!(list.len(), 1);
assert_eq!(list[0].name, "my_prompt");
assert_eq!(list[0].description, Some("My description".to_string()));
}
#[test]
fn test_request_id_string() {
let id = RequestId::String("abc-123".to_string());
let json = serde_json::to_string(&id).unwrap();
assert!(json.contains("abc-123"));
}
#[test]
fn test_request_id_number() {
let id = RequestId::Number(42);
let json = serde_json::to_string(&id).unwrap();
assert!(json.contains("42"));
}
#[test]
fn test_request_id_deserialize_string() {
let json = r#""request-id""#;
let id: RequestId = serde_json::from_str(json).unwrap();
match id {
RequestId::String(s) => assert_eq!(s, "request-id"),
_ => panic!("Expected string id"),
}
}
#[test]
fn test_request_id_deserialize_number() {
let json = "123";
let id: RequestId = serde_json::from_str(json).unwrap();
match id {
RequestId::Number(n) => assert_eq!(n, 123),
_ => panic!("Expected number id"),
}
}
#[test]
fn test_mcp_error_display() {
let error = McpError {
code: error_codes::INVALID_PARAMS,
message: "Missing parameter".to_string(),
data: None,
};
let display = format!("{}", error);
assert!(display.contains("MCP Error"));
assert!(display.contains(&error_codes::INVALID_PARAMS.to_string()));
assert!(display.contains("Missing parameter"));
}
#[test]
fn test_mcp_error_with_data() {
let error = McpError {
code: error_codes::INTERNAL_ERROR,
message: "Something went wrong".to_string(),
data: Some(serde_json::json!({ "details": "extra info" })),
};
assert_eq!(error.code, error_codes::INTERNAL_ERROR);
assert!(error.data.is_some());
}
#[test]
fn test_mcp_error_clone() {
let error = McpError {
code: error_codes::METHOD_NOT_FOUND,
message: "Not found".to_string(),
data: None,
};
let cloned = error.clone();
assert_eq!(error.code, cloned.code);
assert_eq!(error.message, cloned.message);
}
#[test]
fn test_error_codes_values() {
assert_eq!(error_codes::PARSE_ERROR, -32700);
assert_eq!(error_codes::INVALID_REQUEST, -32600);
assert_eq!(error_codes::METHOD_NOT_FOUND, -32601);
assert_eq!(error_codes::INVALID_PARAMS, -32602);
assert_eq!(error_codes::INTERNAL_ERROR, -32603);
}
#[test]
fn test_mcp_request_serialize() {
let request = McpRequest {
id: RequestId::Number(1),
method: "tools/list".to_string(),
params: None,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("tools/list"));
}
#[test]
fn test_mcp_request_with_params() {
let request = McpRequest {
id: RequestId::String("req-1".to_string()),
method: "tools/call".to_string(),
params: Some(serde_json::json!({
"name": "analyze",
"arguments": { "path": "/src" }
})),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("tools/call"));
assert!(json.contains("analyze"));
}
#[test]
fn test_mcp_request_deserialize() {
let json = r#"{"id":1,"method":"initialize","params":null}"#;
let request: McpRequest = serde_json::from_str(json).unwrap();
assert_eq!(request.method, "initialize");
match request.id {
RequestId::Number(n) => assert_eq!(n, 1),
_ => panic!("Expected number id"),
}
}
#[test]
fn test_mcp_response_success() {
let response = McpResponse {
id: RequestId::Number(1),
result: Some(serde_json::json!({ "success": true })),
error: None,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("success"));
assert!(!json.contains("error"));
}
#[test]
fn test_mcp_response_error() {
let response = McpResponse {
id: RequestId::Number(1),
result: None,
error: Some(McpError {
code: error_codes::INTERNAL_ERROR,
message: "Failed".to_string(),
data: None,
}),
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("error"));
assert!(json.contains("Failed"));
}
#[test]
fn test_mcp_notification_serialize() {
let notification = McpNotification {
method: "notifications/resources/updated".to_string(),
params: Some(serde_json::json!({ "uri": "file:///test" })),
};
let json = serde_json::to_string(¬ification).unwrap();
assert!(json.contains("notifications/resources/updated"));
assert!(json.contains("file:///test"));
}
#[test]
fn test_mcp_notification_without_params() {
let notification = McpNotification {
method: "initialized".to_string(),
params: None,
};
let json = serde_json::to_string(¬ification).unwrap();
assert!(json.contains("initialized"));
}
#[test]
fn test_server_capabilities_serialize() {
let capabilities = ServerCapabilities {
experimental: None,
logging: Some(LoggingCapabilities {
level: "info".to_string(),
}),
prompts: Some(PromptsCapability {
list_changed: Some(true),
}),
resources: Some(ResourcesCapability {
subscribe: Some(true),
list_changed: Some(true),
}),
tools: Some(ToolsCapability {
list_changed: Some(true),
}),
};
let json = serde_json::to_string(&capabilities).unwrap();
assert!(json.contains("logging"));
assert!(json.contains("prompts"));
assert!(json.contains("resources"));
assert!(json.contains("tools"));
}
#[test]
fn test_server_capabilities_minimal() {
let capabilities = ServerCapabilities {
experimental: None,
logging: None,
prompts: None,
resources: None,
tools: None,
};
let json = serde_json::to_string(&capabilities).unwrap();
assert!(!json.is_empty());
}
#[test]
fn test_resource_content_text() {
let content = ResourceContent {
uri: "file:///test.txt".to_string(),
mime_type: Some("text/plain".to_string()),
content: ResourceContentType::Text {
text: "Hello, World!".to_string(),
},
};
let json = serde_json::to_string(&content).unwrap();
assert!(json.contains("file:///test.txt"));
assert!(json.contains("Hello, World!"));
}
#[test]
fn test_resource_content_blob() {
let content = ResourceContent {
uri: "file:///image.png".to_string(),
mime_type: Some("image/png".to_string()),
content: ResourceContentType::Blob {
blob: "aGVsbG8=".to_string(), },
};
let json = serde_json::to_string(&content).unwrap();
assert!(json.contains("image/png"));
assert!(json.contains("aGVsbG8="));
}
#[test]
fn test_resource_template_serialize() {
let template = ResourceTemplate {
uri_template: "file:///{path}".to_string(),
name: "files".to_string(),
description: Some("File system access".to_string()),
mime_type: Some("text/plain".to_string()),
};
let json = serde_json::to_string(&template).unwrap();
assert!(json.contains("file:///{path}"));
assert!(json.contains("files"));
}
#[test]
fn test_resource_template_minimal() {
let template = ResourceTemplate {
uri_template: "test://".to_string(),
name: "test".to_string(),
description: None,
mime_type: None,
};
let json = serde_json::to_string(&template).unwrap();
assert!(json.contains("test://"));
}
#[test]
fn test_prompt_metadata_full() {
let metadata = PromptMetadata {
name: "code_review".to_string(),
description: Some("Review code for issues".to_string()),
arguments: Some(vec![
PromptArgument {
name: "file".to_string(),
description: Some("File to review".to_string()),
required: Some(true),
},
PromptArgument {
name: "depth".to_string(),
description: Some("Review depth".to_string()),
required: Some(false),
},
]),
};
let json = serde_json::to_string(&metadata).unwrap();
assert!(json.contains("code_review"));
assert!(json.contains("file"));
assert!(json.contains("depth"));
}
#[test]
fn test_prompt_metadata_minimal() {
let metadata = PromptMetadata {
name: "simple".to_string(),
description: None,
arguments: None,
};
let json = serde_json::to_string(&metadata).unwrap();
assert!(json.contains("simple"));
}
#[test]
fn test_prompt_message_text() {
let message = PromptMessage {
role: "user".to_string(),
content: PromptContent::Text("Hello".to_string()),
};
let json = serde_json::to_string(&message).unwrap();
assert!(json.contains("user"));
assert!(json.contains("Hello"));
}
#[test]
fn test_prompt_message_parts() {
let message = PromptMessage {
role: "assistant".to_string(),
content: PromptContent::Parts(vec![
ContentPart::Text {
text: "Here's the code:".to_string(),
},
ContentPart::Resource {
uri: "file:///code.rs".to_string(),
},
]),
};
let json = serde_json::to_string(&message).unwrap();
assert!(json.contains("assistant"));
assert!(json.contains("Here's the code"));
assert!(json.contains("file:///code.rs"));
}
#[test]
fn test_content_part_text() {
let part = ContentPart::Text {
text: "Some text".to_string(),
};
let json = serde_json::to_string(&part).unwrap();
assert!(json.contains("text"));
assert!(json.contains("Some text"));
}
#[test]
fn test_content_part_image() {
let part = ContentPart::Image {
data: "base64data".to_string(),
mime_type: "image/png".to_string(),
};
let json = serde_json::to_string(&part).unwrap();
assert!(json.contains("image"));
assert!(json.contains("base64data"));
assert!(json.contains("image/png"));
}
#[test]
fn test_content_part_resource() {
let part = ContentPart::Resource {
uri: "file:///path".to_string(),
};
let json = serde_json::to_string(&part).unwrap();
assert!(json.contains("resource"));
assert!(json.contains("file:///path"));
}
#[test]
fn test_tool_metadata_serialize() {
let metadata = ToolMetadata {
name: "analyze".to_string(),
description: "Analyze code".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"path": { "type": "string" }
}
}),
};
let json = serde_json::to_string(&metadata).unwrap();
assert!(json.contains("analyze"));
assert!(json.contains("Analyze code"));
assert!(json.contains("properties"));
}
#[test]
fn test_tool_metadata_clone() {
let metadata = ToolMetadata {
name: "test".to_string(),
description: "desc".to_string(),
input_schema: serde_json::json!({}),
};
let cloned = metadata.clone();
assert_eq!(metadata.name, cloned.name);
assert_eq!(metadata.description, cloned.description);
}
#[test]
fn test_mcp_version_format() {
assert!(MCP_VERSION.contains("-"));
let parts: Vec<&str> = MCP_VERSION.split('-').collect();
assert_eq!(parts.len(), 3);
let year: i32 = parts[0].parse().unwrap();
assert!(year >= 2024);
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn test_tool_registry_preserves_tools(count in 1usize..20) {
let mut registry = ToolRegistry::new();
for i in 0..count {
let tool = Arc::new(MockTool::new(&format!("tool_{}", i)));
registry.register(tool);
}
prop_assert_eq!(registry.list().len(), count);
}
#[test]
fn test_resource_registry_preserves_resources(count in 1usize..20) {
let mut registry = ResourceRegistry::new();
for i in 0..count {
let resource = Arc::new(MockResource::new(
&format!("scheme_{}:///{{}}", i),
&format!("resource_{}", i)
));
registry.register(resource);
}
prop_assert_eq!(registry.list().len(), count);
}
#[test]
fn test_prompt_registry_preserves_prompts(count in 1usize..20) {
let mut registry = PromptRegistry::new();
for i in 0..count {
let prompt = Arc::new(MockPrompt::new(
&format!("prompt_{}", i),
&format!("Description {}", i)
));
registry.register(prompt);
}
prop_assert_eq!(registry.list().len(), count);
}
#[test]
fn test_request_id_roundtrip_number(n in 0i64..10000) {
let id = RequestId::Number(n);
let json = serde_json::to_string(&id).unwrap();
let parsed: RequestId = serde_json::from_str(&json).unwrap();
match parsed {
RequestId::Number(parsed_n) => prop_assert_eq!(n, parsed_n),
_ => prop_assert!(false, "Expected number"),
}
}
#[test]
fn test_request_id_roundtrip_string(s in "[a-z0-9-]{1,20}") {
let id = RequestId::String(s.clone());
let json = serde_json::to_string(&id).unwrap();
let parsed: RequestId = serde_json::from_str(&json).unwrap();
match parsed {
RequestId::String(parsed_s) => prop_assert_eq!(s, parsed_s),
_ => prop_assert!(false, "Expected string"),
}
}
#[test]
fn test_mcp_error_preserves_code(code in -50000i32..0) {
let error = McpError {
code,
message: "test".to_string(),
data: None,
};
let json = serde_json::to_string(&error).unwrap();
let parsed: McpError = serde_json::from_str(&json).unwrap();
prop_assert_eq!(error.code, parsed.code);
}
#[test]
fn test_tool_metadata_name_preserved(name in "[a-z_]{1,30}") {
let metadata = ToolMetadata {
name: name.clone(),
description: "test".to_string(),
input_schema: serde_json::json!({}),
};
let json = serde_json::to_string(&metadata).unwrap();
let parsed: ToolMetadata = serde_json::from_str(&json).unwrap();
prop_assert_eq!(name, parsed.name);
}
}
}
#[test]
fn test_empty_method_name() {
let request = McpRequest {
id: RequestId::Number(1),
method: "".to_string(),
params: None,
};
let json = serde_json::to_string(&request).unwrap();
let parsed: McpRequest = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.method, "");
}
#[test]
fn test_very_long_error_message() {
let long_message = "x".repeat(10000);
let error = McpError {
code: error_codes::INTERNAL_ERROR,
message: long_message.clone(),
data: None,
};
let display = format!("{}", error);
assert!(display.contains(&long_message[..100])); }
#[test]
fn test_special_characters_in_strings() {
let notification = McpNotification {
method: "test\n\t\"special".to_string(),
params: Some(serde_json::json!({
"unicode": "\u{1F600}",
"newline": "line1\nline2"
})),
};
let json = serde_json::to_string(¬ification).unwrap();
let parsed: McpNotification = serde_json::from_str(&json).unwrap();
assert!(parsed.method.contains("special"));
}
#[test]
fn test_nested_json_in_params() {
let request = McpRequest {
id: RequestId::Number(1),
method: "complex".to_string(),
params: Some(serde_json::json!({
"level1": {
"level2": {
"level3": {
"level4": [1, 2, 3]
}
}
}
})),
};
let json = serde_json::to_string(&request).unwrap();
let parsed: McpRequest = serde_json::from_str(&json).unwrap();
assert!(parsed.params.is_some());
}
#[test]
fn test_null_values_in_params() {
let request = McpRequest {
id: RequestId::Number(1),
method: "test".to_string(),
params: Some(serde_json::json!({
"key": null,
"array": [null, null],
"nested": { "nullval": null }
})),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("null"));
}
}