Skip to main content

crates_docs/cli/
test_cmd.rs

1//! Test command implementation
2
3use rust_mcp_sdk::schema::ContentBlock;
4use std::path::Path;
5use std::sync::Arc;
6
7/// Test tool command
8#[allow(clippy::too_many_arguments)]
9pub async fn run_test_command(
10    config_path: &Path,
11    tool: &str,
12    crate_name: Option<&str>,
13    item_path: Option<&str>,
14    query: Option<&str>,
15    sort: Option<&str>,
16    version: Option<&str>,
17    limit: u32,
18    format: &str,
19) -> Result<(), Box<dyn std::error::Error>> {
20    tracing::info!("Testing tool: {}", tool);
21
22    // Honor the global `--config` flag: load cache and performance settings
23    // from the config file when present, falling back to defaults otherwise.
24    let app_config = if config_path.exists() {
25        crate::config::AppConfig::from_file(config_path)
26            .map_err(|e| format!("Failed to load config file: {e}"))?
27    } else {
28        crate::config::AppConfig::default()
29    };
30
31    // Initialize the global HTTP client from the configured performance
32    // settings (timeouts, user-agent, pool). Ignore the error if it was
33    // already initialized elsewhere in the process.
34    let _ = crate::utils::init_global_http_client(&app_config.performance);
35
36    let cache = crate::cache::create_cache(&app_config.cache)?;
37    let cache_arc: Arc<dyn crate::cache::Cache> = Arc::from(cache);
38
39    // Create document service honoring the configured cache TTLs.
40    let doc_service = Arc::new(crate::tools::docs::DocService::with_config(
41        cache_arc,
42        &app_config.cache,
43    )?);
44
45    // Create tool registry
46    let registry = crate::tools::create_default_registry(&doc_service);
47
48    match tool {
49        "lookup_crate" => {
50            execute_lookup_crate(crate_name, version, format, &registry).await?;
51        }
52        "search_crates" => {
53            execute_search_crates(query, sort, limit, format, &registry).await?;
54        }
55        "lookup_item" => {
56            execute_lookup_item(crate_name, item_path, version, format, &registry).await?;
57        }
58        "health_check" => {
59            execute_health_check(&registry).await?;
60        }
61        _ => {
62            return Err(format!("Unknown tool: {tool}").into());
63        }
64    }
65
66    println!("Tool test completed");
67    Ok(())
68}
69
70/// Execute `lookup_crate` tool
71async fn execute_lookup_crate(
72    crate_name: Option<&str>,
73    version: Option<&str>,
74    format: &str,
75    registry: &crate::tools::ToolRegistry,
76) -> Result<(), Box<dyn std::error::Error>> {
77    if let Some(name) = crate_name {
78        println!("Testing crate lookup: {name} (version: {version:?})");
79        println!("Output format: {format}");
80
81        // Prepare arguments
82        let mut arguments = serde_json::json!({
83            "crate_name": name,
84            "format": format
85        });
86
87        if let Some(v) = version {
88            arguments["version"] = serde_json::Value::String(v.to_string());
89        }
90
91        // Execute tool
92        match registry.execute_tool("lookup_crate", arguments).await {
93            Ok(result) => print_tool_result(&result),
94            Err(e) => return Err(format!("Tool execution failed: {e}").into()),
95        }
96    } else {
97        return Err("lookup_crate requires --crate-name parameter".into());
98    }
99    Ok(())
100}
101
102/// Execute `search_crates` tool
103async fn execute_search_crates(
104    query: Option<&str>,
105    sort: Option<&str>,
106    limit: u32,
107    format: &str,
108    registry: &crate::tools::ToolRegistry,
109) -> Result<(), Box<dyn std::error::Error>> {
110    if let Some(q) = query {
111        println!("Testing crate search: {q} (limit: {limit})");
112        println!("Sort order: {}", sort.unwrap_or("relevance"));
113        println!("Output format: {format}");
114
115        // Prepare arguments
116        let mut arguments = serde_json::json!({
117            "query": q,
118            "limit": limit,
119            "format": format
120        });
121
122        if let Some(sort) = sort {
123            arguments["sort"] = serde_json::Value::String(sort.to_string());
124        }
125
126        // Execute tool
127        match registry.execute_tool("search_crates", arguments).await {
128            Ok(result) => print_tool_result(&result),
129            Err(e) => return Err(format!("Tool execution failed: {e}").into()),
130        }
131    } else {
132        return Err("search_crates requires --query parameter".into());
133    }
134    Ok(())
135}
136
137/// Build the human-readable item path shown in the `lookup_item` test echo.
138///
139/// The `lookup_item` tool drops a redundant leading crate-name segment from
140/// `item_path` when resolving the docs URL (e.g. `std::string::String` under
141/// crate `std`). Mirror that here so the diagnostic line does not print a
142/// doubled prefix such as `std::std::string::String`. Crate-name hyphens are
143/// normalized to underscores for the comparison, matching the resolver.
144fn display_item_path(crate_name: &str, item_path: &str) -> String {
145    let krate = crate_name.replace('-', "_");
146    let first = item_path.split("::").map(str::trim).find(|s| !s.is_empty());
147    if first.map(|s| s.replace('-', "_")) == Some(krate) {
148        item_path.trim().to_string()
149    } else {
150        format!("{crate_name}::{item_path}")
151    }
152}
153
154/// Execute `lookup_item` tool
155async fn execute_lookup_item(
156    crate_name: Option<&str>,
157    item_path: Option<&str>,
158    version: Option<&str>,
159    format: &str,
160    registry: &crate::tools::ToolRegistry,
161) -> Result<(), Box<dyn std::error::Error>> {
162    if let (Some(name), Some(path)) = (crate_name, item_path) {
163        println!(
164            "Testing item lookup: {} (version: {version:?})",
165            display_item_path(name, path)
166        );
167        println!("Output format: {format}");
168
169        // Prepare arguments
170        let mut arguments = serde_json::json!({
171            "crate_name": name,
172            "item_path": path,
173            "format": format
174        });
175
176        if let Some(v) = version {
177            arguments["version"] = serde_json::Value::String(v.to_string());
178        }
179
180        // Execute tool
181        match registry.execute_tool("lookup_item", arguments).await {
182            Ok(result) => print_tool_result(&result),
183            Err(e) => return Err(format!("Tool execution failed: {e}").into()),
184        }
185    } else {
186        return Err("lookup_item requires --crate-name and --item-path parameters".into());
187    }
188    Ok(())
189}
190
191/// Execute `health_check` tool
192async fn execute_health_check(
193    registry: &crate::tools::ToolRegistry,
194) -> Result<(), Box<dyn std::error::Error>> {
195    println!("Testing health check");
196
197    // Prepare arguments
198    let arguments = serde_json::json!({
199        "check_type": "all",
200        "verbose": true
201    });
202
203    // Execute tool
204    match registry.execute_tool("health_check", arguments).await {
205        Ok(result) => print_tool_result(&result),
206        Err(e) => return Err(format!("Tool execution failed: {e}").into()),
207    }
208    Ok(())
209}
210
211/// Print tool execution result
212fn print_tool_result(result: &rust_mcp_sdk::schema::CallToolResult) {
213    println!("Tool executed successfully:");
214    if let Some(content) = result.content.first() {
215        match content {
216            ContentBlock::TextContent(text_content) => {
217                println!("{}", text_content.text);
218            }
219            other => {
220                println!("Non-text content: {other:?}");
221            }
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::display_item_path;
229
230    #[test]
231    fn drops_redundant_leading_crate_segment() {
232        // A path that already includes the crate prefix must not be doubled.
233        assert_eq!(
234            display_item_path("std", "std::string::String"),
235            "std::string::String"
236        );
237        assert_eq!(
238            display_item_path("std", "std::collections"),
239            "std::collections"
240        );
241    }
242
243    #[test]
244    fn prepends_crate_when_prefix_absent() {
245        assert_eq!(
246            display_item_path("std", "string::String"),
247            "std::string::String"
248        );
249        assert_eq!(display_item_path("std", "collections"), "std::collections");
250    }
251
252    #[test]
253    fn normalizes_crate_name_hyphens() {
254        // docs.rs uses the underscore form of the crate name in paths.
255        assert_eq!(
256            display_item_path("tokio-util", "tokio_util::codec::Framed"),
257            "tokio_util::codec::Framed"
258        );
259        // Without the prefix, the original crate name is preserved verbatim.
260        assert_eq!(
261            display_item_path("tokio-util", "codec::Framed"),
262            "tokio-util::codec::Framed"
263        );
264    }
265}