diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 0f7ddc9..7f788a9 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -219,6 +219,25 @@ impl ToolRegistry { tools.extend(identity::tool_definitions()); } + // Code search (OpenSearch code index) + tools.push(Tool::new( + "search_code".into(), + "Search the code index for functions, types, patterns, or concepts across \ + the current project and Gitea repositories. Supports keyword and semantic search." + .into(), + json!({ + "type": "object", + "properties": { + "query": { "type": "string", "description": "Search query (natural language or code pattern)" }, + "language": { "type": "string", "description": "Filter by language: rust, typescript, python (optional)" }, + "repo": { "type": "string", "description": "Filter by repo name (optional)" }, + "semantic": { "type": "boolean", "description": "Use semantic search (optional)" }, + "limit": { "type": "integer", "description": "Max results (default 10)" } + }, + "required": ["query"] + }), + )); + // Web search (SearXNG — free, self-hosted) tools.push(web_search::tool_definition()); @@ -436,3 +455,123 @@ impl ToolRegistry { .await } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tool_definitions_base() { + let tools = ToolRegistry::tool_definitions(false, false); + assert!(tools.len() >= 5, "Should have at least 5 base tools"); + + let names: Vec = tools.iter().map(|t| t.function.name.clone()).collect(); + assert!(names.contains(&"search_archive".into())); + assert!(names.contains(&"run_script".into())); + assert!(names.contains(&"search_web".into())); + assert!(names.contains(&"search_code".into())); + // No gitea or identity tools when disabled + assert!(!names.iter().any(|n| n.starts_with("gitea_"))); + assert!(!names.iter().any(|n| n.starts_with("identity_"))); + } + + #[test] + fn test_tool_definitions_with_gitea() { + let tools = ToolRegistry::tool_definitions(true, false); + let names: Vec = tools.iter().map(|t| t.function.name.clone()).collect(); + assert!(names.iter().any(|n| n.starts_with("gitea_")), "Should have gitea tools"); + assert!(!names.iter().any(|n| n.starts_with("identity_"))); + } + + #[test] + fn test_tool_definitions_with_kratos() { + let tools = ToolRegistry::tool_definitions(false, true); + let names: Vec = tools.iter().map(|t| t.function.name.clone()).collect(); + assert!(names.iter().any(|n| n.starts_with("identity_")), "Should have identity tools"); + assert!(!names.iter().any(|n| n.starts_with("gitea_"))); + } + + #[test] + fn test_tool_definitions_all_enabled() { + let tools = ToolRegistry::tool_definitions(true, true); + let names: Vec = tools.iter().map(|t| t.function.name.clone()).collect(); + assert!(names.iter().any(|n| n.starts_with("gitea_"))); + assert!(names.iter().any(|n| n.starts_with("identity_"))); + } + + #[test] + fn test_agent_tool_definitions() { + let tools = ToolRegistry::agent_tool_definitions(false, false); + assert!(!tools.is_empty(), "Should have agent tools"); + } + + #[test] + fn test_minimal_registry() { + let config = Arc::new(crate::config::Config::from_str(r#" + [matrix] + homeserver_url = "http://localhost:8008" + user_id = "@test:localhost" + state_store_path = "/tmp/test" + db_path = ":memory:" + [opensearch] + url = "http://localhost:9200" + index = "test" + [mistral] + [behavior] + "#).unwrap()); + let registry = ToolRegistry::new_minimal(config); + assert!(!registry.has_gitea()); + assert!(!registry.has_kratos()); + } + + #[tokio::test] + async fn test_execute_unknown_tool() { + let config = Arc::new(crate::config::Config::from_str(r#" + [matrix] + homeserver_url = "http://localhost:8008" + user_id = "@test:localhost" + state_store_path = "/tmp/test" + db_path = ":memory:" + [opensearch] + url = "http://localhost:9200" + index = "test" + [mistral] + [behavior] + "#).unwrap()); + let registry = ToolRegistry::new_minimal(config); + let ctx = crate::orchestrator::event::ToolContext { + user_id: "test".into(), + scope_key: "test-room".into(), + is_direct: true, + }; + let result = registry.execute_with_context("nonexistent_tool", "{}", &ctx).await; + assert!(result.is_err(), "Unknown tool should error"); + } + + #[tokio::test] + async fn test_execute_search_code_without_opensearch() { + let config = Arc::new(crate::config::Config::from_str(r#" + [matrix] + homeserver_url = "http://localhost:8008" + user_id = "@test:localhost" + state_store_path = "/tmp/test" + db_path = ":memory:" + [opensearch] + url = "http://localhost:9200" + index = "test" + [mistral] + [behavior] + "#).unwrap()); + let registry = ToolRegistry::new_minimal(config); + let ctx = crate::context::ResponseContext { + matrix_user_id: String::new(), + user_id: "test".into(), + display_name: None, + is_dm: true, + is_reply: false, + room_id: "test".into(), + }; + let result = registry.execute("search_code", r#"{"query":"test"}"#, &ctx).await; + assert!(result.is_err(), "search_code without OpenSearch should error"); + } +}