use mcp_execution_skill::{
ParsedToolFile, build_skill_context, parse_tool_file, scan_tools_directory,
};
use std::fmt::Write;
use tempfile::TempDir;
use tokio::fs;
async fn create_test_tool_file(dir: &std::path::Path, name: &str, category: &str) {
let content = format!(
r"/**
* @tool {name}
* @server test
* @category {category}
* @keywords test,{name}
* @description Test tool: {name}
*/
interface {pascal_name}Params {{
required_param: string;
optional_param?: number;
}}
export async function {pascal_name}(params: {pascal_name}Params): Promise<void> {{
// Implementation
}}
",
name = name,
category = category,
pascal_name = to_pascal_case(name),
);
let filename = format!("{}.ts", to_pascal_case(name));
fs::write(dir.join(&filename), content).await.unwrap();
}
fn to_pascal_case(s: &str) -> String {
s.split('_')
.map(|word| {
let mut chars = word.chars();
chars.next().map_or_else(String::new, |first| {
first.to_uppercase().chain(chars).collect()
})
})
.collect()
}
#[tokio::test]
async fn test_parse_tool_file_integration() {
let content = r"
/**
* @tool create_issue
* @server github
* @category issues
* @keywords create,issue,new,bug
* @description Create a new issue in a repository
*/
interface CreateIssueParams {
owner: string;
repo: string;
title: string;
body?: string;
labels?: string[];
}
export async function createIssue(params: CreateIssueParams): Promise<Issue> {
// Implementation
}
";
let result = parse_tool_file(content, "createIssue.ts").unwrap();
assert_eq!(result.name, "create_issue");
assert_eq!(result.typescript_name, "createIssue");
assert_eq!(result.server_id, "github");
assert_eq!(result.category, Some("issues".to_string()));
assert_eq!(result.keywords.len(), 4);
assert!(result.description.is_some());
assert_eq!(result.parameters.len(), 5);
let required_count = result.parameters.iter().filter(|p| p.required).count();
let optional_count = result.parameters.iter().filter(|p| !p.required).count();
assert_eq!(required_count, 3); assert_eq!(optional_count, 2); }
#[tokio::test]
async fn test_scan_tools_directory_integration() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_tool_file(dir, "create_issue", "issues").await;
create_test_tool_file(dir, "list_repos", "repos").await;
create_test_tool_file(dir, "get_user", "users").await;
fs::write(dir.join("index.ts"), "export * from './createIssue';")
.await
.unwrap();
fs::create_dir(dir.join("_runtime")).await.unwrap();
fs::write(dir.join("_runtime/mcp-bridge.ts"), "// Bridge")
.await
.unwrap();
let tools = scan_tools_directory(dir).await.unwrap();
assert_eq!(tools.len(), 3);
assert!(tools.iter().any(|t| t.name == "create_issue"));
assert!(tools.iter().any(|t| t.name == "list_repos"));
assert!(tools.iter().any(|t| t.name == "get_user"));
}
#[tokio::test]
async fn test_build_skill_context_integration() {
let tools = vec![
ParsedToolFile {
name: "create_issue".to_string(),
typescript_name: "createIssue".to_string(),
server_id: "github".to_string(),
category: Some("issues".to_string()),
keywords: vec!["create".to_string(), "issue".to_string()],
description: Some("Create a new issue".to_string()),
parameters: vec![],
},
ParsedToolFile {
name: "list_repos".to_string(),
typescript_name: "listRepos".to_string(),
server_id: "github".to_string(),
category: Some("repos".to_string()),
keywords: vec!["list".to_string(), "repos".to_string()],
description: Some("List repositories".to_string()),
parameters: vec![],
},
];
let use_case_hints = vec!["CI/CD".to_string()];
let context = build_skill_context("github", &tools, Some(&use_case_hints));
assert_eq!(context.server_id, "github");
assert_eq!(context.skill_name, "github-progressive");
assert_eq!(context.tool_count, 2);
assert_eq!(context.categories.len(), 2);
assert!(!context.generation_prompt.is_empty());
assert!(context.generation_prompt.contains("CI/CD"));
}
#[tokio::test]
async fn test_scan_nonexistent_directory() {
let result = scan_tools_directory(std::path::Path::new("/nonexistent/path")).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_parse_tool_file_missing_required_tags() {
let content = r"
/**
* @server github
*/
";
let result = parse_tool_file(content, "test.ts");
assert!(result.is_err());
let content = r"
/**
* @tool test
*/
";
let result = parse_tool_file(content, "test.ts");
assert!(result.is_err());
}
#[tokio::test]
async fn test_skill_metadata_extraction() {
let content = r"---
name: github-progressive
description: GitHub operations via MCP tools
---
# GitHub Progressive
Introduction paragraph.
## Quick Start
Steps here.
## Common Tasks
Tasks here.
## Troubleshooting
Troubleshooting here.
";
assert!(content.starts_with("---"));
assert!(content.contains("name:"));
assert!(content.contains("description:"));
let section_count = content.lines().filter(|l| l.starts_with("## ")).count();
assert_eq!(section_count, 3);
}
#[tokio::test]
async fn test_scan_directory_with_many_files() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
for i in 0..50 {
let tool_name = format!("tool_{i}");
create_test_tool_file(dir, &tool_name, "test-category").await;
}
let tools = scan_tools_directory(dir).await.unwrap();
assert_eq!(tools.len(), 50);
for i in 1..tools.len() {
assert!(tools[i - 1].name <= tools[i].name);
}
}
#[tokio::test]
async fn test_scan_directory_with_invalid_files() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_tool_file(dir, "valid_tool", "category").await;
let invalid_content = r"
/**
* @server github
*/
export function invalid() {}
";
fs::write(dir.join("invalid.ts"), invalid_content)
.await
.unwrap();
fs::write(dir.join("readme.txt"), "Not a TypeScript file")
.await
.unwrap();
let tools = scan_tools_directory(dir).await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "valid_tool");
}
#[tokio::test]
async fn test_scan_directory_skips_index_and_runtime() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
create_test_tool_file(dir, "valid_tool", "category").await;
fs::write(dir.join("index.ts"), "export * from './validTool';")
.await
.unwrap();
fs::create_dir(dir.join("_runtime")).await.unwrap();
fs::write(dir.join("_runtime/bridge.ts"), "// Runtime bridge")
.await
.unwrap();
create_test_tool_file(dir, "_internal", "category").await;
let internal_file = dir.join(format!("{}.ts", to_pascal_case("_internal")));
if internal_file.exists() {
fs::remove_file(&internal_file).await.ok();
}
fs::write(dir.join("_internal.ts"), "// Internal")
.await
.unwrap();
let tools = scan_tools_directory(dir).await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name, "valid_tool");
}
#[tokio::test]
async fn test_parse_tool_file_large_description() {
let long_description = "a".repeat(1000);
let content = format!(
r"/**
* @tool test_tool
* @server github
* @description {long_description}
*/
"
);
let result = parse_tool_file(&content, "test.ts").unwrap();
assert_eq!(result.description, Some(long_description));
}
#[tokio::test]
async fn test_parse_tool_file_many_keywords() {
let keywords: Vec<String> = (0..100).map(|i| format!("keyword{i}")).collect();
let keywords_str = keywords.join(",");
let content = format!(
r"/**
* @tool test_tool
* @server github
* @keywords {keywords_str}
*/
"
);
let result = parse_tool_file(&content, "test.ts").unwrap();
assert_eq!(result.keywords.len(), 100);
}
#[tokio::test]
async fn test_parse_tool_file_many_parameters() {
let mut params = String::new();
for i in 0..50 {
writeln!(params, " param{i}: string;").unwrap();
}
let content = format!(
r"/**
* @tool test_tool
* @server github
*/
interface TestToolParams {{
{params}}}
"
);
let result = parse_tool_file(&content, "test.ts").unwrap();
assert_eq!(result.parameters.len(), 50);
}
#[tokio::test]
async fn test_build_skill_context_many_categories() {
let tools: Vec<ParsedToolFile> = (0..20)
.map(|i| ParsedToolFile {
name: format!("tool_{i}"),
typescript_name: format!("tool{i}"),
server_id: "test".to_string(),
category: Some(format!("category-{}", i % 5)), keywords: vec![format!("keyword{i}")],
description: Some(format!("Tool {i}")),
parameters: vec![],
})
.collect();
let context = build_skill_context("test", &tools, None);
assert_eq!(context.tool_count, 20);
assert_eq!(context.categories.len(), 5);
for category in &context.categories {
assert_eq!(category.tools.len(), 4); }
}
#[tokio::test]
async fn test_build_skill_context_empty_tools() {
let tools: Vec<ParsedToolFile> = vec![];
let context = build_skill_context("test", &tools, None);
assert_eq!(context.tool_count, 0);
assert_eq!(context.categories.len(), 0);
assert_eq!(context.example_tools.len(), 0);
assert!(context.generation_prompt.contains('0')); }
#[tokio::test]
async fn test_scan_directory_concurrent_access() {
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
for i in 0..10 {
create_test_tool_file(dir, &format!("tool_{i}"), "category").await;
}
let dir_path = dir.to_path_buf();
let handles: Vec<_> = (0..5)
.map(|_| {
let dir = dir_path.clone();
tokio::spawn(async move { scan_tools_directory(&dir).await })
})
.collect();
for handle in handles {
let result = handle.await.unwrap();
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 10);
}
}
#[tokio::test]
async fn test_scan_directory_permission_denied() {
if cfg!(windows) {
return;
}
let temp_dir = TempDir::new().unwrap();
let restricted_dir = temp_dir.path().join("restricted");
fs::create_dir(&restricted_dir).await.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = tokio::fs::metadata(&restricted_dir)
.await
.unwrap()
.permissions();
perms.set_mode(0o000);
tokio::fs::set_permissions(&restricted_dir, perms)
.await
.unwrap();
let result = scan_tools_directory(&restricted_dir).await;
assert!(result.is_err());
let mut perms = tokio::fs::metadata(&restricted_dir)
.await
.unwrap()
.permissions();
perms.set_mode(0o755);
tokio::fs::set_permissions(&restricted_dir, perms)
.await
.unwrap();
}
}
#[tokio::test]
async fn test_parse_tool_file_binary_content() {
let binary_content = vec![0xFF, 0xFE, 0xFD, 0xFC];
let content_str = String::from_utf8_lossy(&binary_content);
let result = parse_tool_file(&content_str, "binary.ts");
assert!(result.is_err());
}
#[tokio::test]
async fn test_scan_directory_too_many_files() {
use mcp_execution_skill::{MAX_TOOL_FILES, ScanError};
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
for i in 0..=MAX_TOOL_FILES {
create_test_tool_file(dir, &format!("tool_{i}"), "test").await;
}
let result = scan_tools_directory(dir).await;
assert!(result.is_err());
match result.unwrap_err() {
ScanError::TooManyFiles { count, limit } => {
assert_eq!(count, MAX_TOOL_FILES + 1);
assert_eq!(limit, MAX_TOOL_FILES);
}
other => panic!("Expected TooManyFiles error, got: {other:?}"),
}
}
#[tokio::test]
async fn test_scan_directory_file_too_large() {
use mcp_execution_skill::{MAX_FILE_SIZE, ScanError};
let temp_dir = TempDir::new().unwrap();
let dir = temp_dir.path();
#[allow(clippy::cast_possible_truncation)]
let large_content = "a".repeat((MAX_FILE_SIZE as usize) + 1);
let content = format!(
r"/**
* @tool large_tool
* @server test
* @keywords large
* @description Large tool for testing
*/
interface LargeToolParams {{
param: string;
}}
{large_content}
"
);
let large_file = dir.join("LargeTool.ts");
fs::write(&large_file, content).await.unwrap();
let result = scan_tools_directory(dir).await;
assert!(result.is_err());
match result.unwrap_err() {
ScanError::FileTooLarge { path, size, limit } => {
assert!(path.contains("LargeTool.ts"));
assert!(size > MAX_FILE_SIZE);
assert_eq!(limit, MAX_FILE_SIZE);
}
other => panic!("Expected FileTooLarge error, got: {other:?}"),
}
}