prvng_platform/crates/ai-service/tests/phase4_integration_test.rs
Jesús Pérez 09a97ac8f5
chore: update platform submodule to monorepo crates structure
Platform restructured into crates/, added AI service and detector,
       migrated control-center-ui to Leptos 0.8
2026-01-08 21:32:59 +00:00

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"));
}
}
}