//! Phase 4 Integration Tests: MCP Tool Integration with RAG //! //! Tests for tool registry, explicit tool calls, hybrid mode, and all tool //! categories. use std::net::SocketAddr; use ai_service::mcp::ToolCategory; use ai_service::service::{AiService, AskRequest, McpToolRequest}; use serde_json::json; #[tokio::test] async fn test_tool_registry_initialization() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); // Test that all tool categories are registered let all_tools = service.list_all_tools(); assert!(!all_tools.is_empty(), "Tool registry should not be empty"); // Verify we have tools from each category let categories: Vec<_> = all_tools.iter().map(|t| t.category).collect(); assert!( categories.contains(&ToolCategory::Rag), "RAG tools should be registered" ); assert!( categories.contains(&ToolCategory::Guidance), "Guidance tools should be registered" ); assert!( categories.contains(&ToolCategory::Settings), "Settings tools should be registered" ); assert!( categories.contains(&ToolCategory::Iac), "IaC tools should be registered" ); } #[tokio::test] async fn test_rag_tool_count() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let rag_tools = service.tools_by_category(ToolCategory::Rag); assert_eq!( rag_tools.len(), 3, "Should have 3 RAG tools: ask, search, status" ); let tool_names: Vec<_> = rag_tools.iter().map(|t| t.name.as_str()).collect(); assert!(tool_names.contains(&"rag_ask_question")); assert!(tool_names.contains(&"rag_semantic_search")); assert!(tool_names.contains(&"rag_get_status")); } #[tokio::test] async fn test_guidance_tool_count() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let guidance_tools = service.tools_by_category(ToolCategory::Guidance); assert_eq!(guidance_tools.len(), 5, "Should have 5 Guidance tools"); let tool_names: Vec<_> = guidance_tools.iter().map(|t| t.name.as_str()).collect(); assert!(tool_names.contains(&"guidance_check_system_status")); assert!(tool_names.contains(&"guidance_suggest_next_action")); assert!(tool_names.contains(&"guidance_find_docs")); assert!(tool_names.contains(&"guidance_troubleshoot")); assert!(tool_names.contains(&"guidance_validate_config")); } #[tokio::test] async fn test_settings_tool_count() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let settings_tools = service.tools_by_category(ToolCategory::Settings); assert_eq!(settings_tools.len(), 7, "Should have 7 Settings tools"); } #[tokio::test] async fn test_iac_tool_count() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let iac_tools = service.tools_by_category(ToolCategory::Iac); assert_eq!(iac_tools.len(), 3, "Should have 3 IaC tools"); } #[tokio::test] async fn test_explicit_tool_call_rag_ask() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let req = McpToolRequest { tool_name: "rag_ask_question".to_string(), args: json!({"question": "What is Nushell?"}), }; let response = service.call_mcp_tool(req).await.unwrap(); assert_eq!(response.result["status"], "success"); assert_eq!(response.result["tool"], "rag_ask_question"); } #[tokio::test] async fn test_explicit_tool_call_guidance_status() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let req = McpToolRequest { tool_name: "guidance_check_system_status".to_string(), args: json!({}), }; let response = service.call_mcp_tool(req).await.unwrap(); assert_eq!(response.result["status"], "healthy"); assert_eq!(response.result["tool"], "guidance_check_system_status"); } #[tokio::test] async fn test_explicit_tool_call_settings() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let req = McpToolRequest { tool_name: "installer_get_settings".to_string(), args: json!({}), }; let response = service.call_mcp_tool(req).await.unwrap(); assert_eq!(response.result["status"], "success"); // Verify real SettingsTools data is returned (not empty placeholder) assert!( response.result.get("platforms").is_some() || response.result.get("modes").is_some() || response.result.get("available_services").is_some(), "Should return real settings data from SettingsTools" ); } #[tokio::test] async fn test_settings_tools_platform_recommendations() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let req = McpToolRequest { tool_name: "installer_platform_recommendations".to_string(), args: json!({}), }; let response = service.call_mcp_tool(req).await.unwrap(); assert_eq!(response.result["status"], "success"); // Should have real recommendations array from SettingsTools platform detection assert!(response.result.get("recommendations").is_some()); } #[tokio::test] async fn test_settings_tools_mode_defaults() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let req = McpToolRequest { tool_name: "installer_get_defaults".to_string(), args: json!({"mode": "solo"}), }; let response = service.call_mcp_tool(req).await.unwrap(); assert_eq!(response.result["status"], "success"); // Verify real mode defaults (resource requirements) assert!(response.result.get("min_cpu_cores").is_some()); assert!(response.result.get("min_memory_gb").is_some()); } #[tokio::test] async fn test_explicit_tool_call_iac() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let req = McpToolRequest { tool_name: "iac_detect_technologies".to_string(), args: json!({"path": "/tmp/infra"}), }; let response = service.call_mcp_tool(req).await.unwrap(); assert_eq!(response.result["status"], "success"); // Verify real technology detection (returns technologies array) assert!(response.result.get("technologies").is_some()); } #[tokio::test] async fn test_iac_detect_technologies_real() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); // Test with provisioning directory that has Nickel files let req = McpToolRequest { tool_name: "iac_detect_technologies".to_string(), args: json!({"path": "../../provisioning"}), }; let response = service.call_mcp_tool(req).await.unwrap(); assert_eq!(response.result["status"], "success"); // Should detect technologies as an array let techs = response.result.get("technologies"); assert!(techs.is_some(), "Should have technologies array"); assert!(techs.unwrap().is_array()); } #[tokio::test] async fn test_iac_analyze_completeness() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let req = McpToolRequest { tool_name: "iac_analyze_completeness".to_string(), args: json!({"path": "/tmp/test-infra"}), }; let response = service.call_mcp_tool(req).await.unwrap(); assert_eq!(response.result["status"], "success"); // Verify real analysis data assert!(response.result.get("complete").is_some()); assert!(response.result.get("completeness_score").is_some()); assert!(response.result.get("missing_files").is_some()); } #[tokio::test] async fn test_unknown_tool_error() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let req = McpToolRequest { tool_name: "unknown_tool_xyz".to_string(), args: json!({}), }; let result = service.call_mcp_tool(req).await; assert!(result.is_err(), "Should fail with unknown tool"); } #[tokio::test] async fn test_hybrid_mode_disabled() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); // Load knowledge base (required for ask) service .load_knowledge_base("../../config/knowledge-base") .await .ok(); let req = AskRequest { question: "What are deployment best practices?".to_string(), context: None, enable_tool_execution: Some(false), // Explicitly disabled max_tool_calls: None, }; let response = service.ask(req).await.unwrap(); // Should not have tool executions when disabled assert!( response.tool_executions.is_none() || response.tool_executions.as_ref().unwrap().is_empty(), "Tool executions should be empty when disabled" ); } #[tokio::test] async fn test_hybrid_mode_enabled() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); // Load knowledge base service .load_knowledge_base("../../config/knowledge-base") .await .ok(); let req = AskRequest { question: "What is the current system status and best practices?".to_string(), context: None, enable_tool_execution: Some(true), // Enable hybrid mode max_tool_calls: Some(3), }; let response = service.ask(req).await.unwrap(); // Should have RAG answer assert!(!response.answer.is_empty(), "Should have RAG answer"); // Tool executions may or may not occur depending on tool suggestions // The important thing is that when enabled, the mechanism works } #[tokio::test] async fn test_max_tool_calls_limit() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); service .load_knowledge_base("../../config/knowledge-base") .await .ok(); let req = AskRequest { question: "What is system status and what should I do next and how do I find \ documentation?" .to_string(), context: None, enable_tool_execution: Some(true), max_tool_calls: Some(1), // Limit to 1 tool }; let response = service.ask(req).await.unwrap(); // Even if multiple tools are suggested, only max_tool_calls should execute if let Some(executions) = &response.tool_executions { assert!( executions.len() <= 1, "Should respect max_tool_calls limit of 1" ); } } #[tokio::test] async fn test_tool_definition_schemas() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); let all_tools = service.list_all_tools(); // Verify all tools have proper definitions for tool in all_tools { assert!(!tool.name.is_empty(), "Tool name should not be empty"); assert!( !tool.description.is_empty(), "Tool description should not be empty" ); // Verify input schema is valid JSON assert!( tool.input_schema.is_object(), "Input schema should be an object" ); assert!( tool.input_schema.get("type").is_some(), "Input schema should have 'type' field" ); } } #[tokio::test] async fn test_tool_execution_with_required_args() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); // Tool that requires arguments should work when provided let req = McpToolRequest { tool_name: "rag_semantic_search".to_string(), args: json!({"query": "kubernetes"}), }; let response = service.call_mcp_tool(req).await.unwrap(); assert_eq!(response.result["status"], "success"); } #[tokio::test] async fn test_tool_execution_error_handling() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); // Tool that requires arguments should fail when not provided let req = McpToolRequest { tool_name: "rag_semantic_search".to_string(), args: json!({}), // Missing required 'query' }; let result = service.call_mcp_tool(req).await; // Should either fail or return an error in the result match result { Ok(response) => { // Even if it doesn't fail, it should indicate an error assert!( response.result.get("status").is_some() || response.result.get("error").is_some() ); } Err(_) => { // Expected: missing required parameter } } } #[tokio::test] #[ignore] // Requires Nushell to be available async fn test_guidance_tools_nushell_execution() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); // Test system status tool (requires Nushell) let req = McpToolRequest { tool_name: "guidance_check_system_status".to_string(), args: json!({}), }; let response = service.call_mcp_tool(req).await; // Should either succeed with Nushell data or fail with Nushell not found match response { Ok(result) => { // If Nushell is available, should have JSON data assert!(result.result.is_object()); } Err(e) => { // Expected if Nushell not available let err_msg = e.to_string(); assert!(err_msg.contains("Nushell") || err_msg.contains("command")); } } } #[tokio::test] #[ignore] // Requires Nushell to be available async fn test_guidance_find_docs() { let addr: SocketAddr = "127.0.0.1:8083".parse().unwrap(); let service = AiService::new(addr); // Test documentation finding tool (requires Nushell) let req = McpToolRequest { tool_name: "guidance_find_docs".to_string(), args: json!({"query": "deployment"}), }; let response = service.call_mcp_tool(req).await; match response { Ok(result) => { // If Nushell is available, should have JSON data assert!(result.result.is_object()); } Err(e) => { // Expected if Nushell not available let err_msg = e.to_string(); assert!(err_msg.contains("Nushell") || err_msg.contains("command")); } } }