Platform restructured into crates/, added AI service and detector,
migrated control-center-ui to Leptos 0.8
452 lines
14 KiB
Rust
452 lines
14 KiB
Rust
//! 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"));
|
|
}
|
|
}
|
|
}
|