vestige/tests/e2e/tests/mcp/tool_tests.rs
Sam Valladares ad1e1796f3 chore: apply clippy fixes to e2e tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 01:23:33 -06:00

581 lines
18 KiB
Rust

//! # MCP Tool Tests
//!
//! Comprehensive tests for all MCP tools provided by Vestige.
//! Tests cover input validation, execution, and response formats.
use serde_json::{json, Value};
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/// Validate a tool call response structure
fn validate_tool_response(response: &Value) {
assert!(response["content"].is_array(), "Response must have content array");
let content = response["content"].as_array().unwrap();
assert!(!content.is_empty(), "Content array must not be empty");
assert!(content[0]["type"].is_string(), "Content must have type");
assert!(content[0]["text"].is_string(), "Content must have text");
}
/// Parse the text content from a tool response
fn parse_response_text(response: &Value) -> Value {
let text = response["content"][0]["text"].as_str().unwrap();
serde_json::from_str(text).unwrap_or(json!({"raw": text}))
}
// ============================================================================
// INGEST TOOL TESTS (3 tests)
// ============================================================================
/// Test ingest tool with valid content.
#[test]
fn test_ingest_tool_valid_content() {
let _tool_call = json!({
"name": "ingest",
"arguments": {
"content": "The Rust programming language is memory-safe.",
"nodeType": "fact",
"tags": ["rust", "programming", "safety"]
}
});
// Expected response format
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"success\": true, \"nodeId\": \"mock-id\", \"message\": \"Knowledge ingested successfully\"}"
}],
"isError": false
});
validate_tool_response(&expected_response);
let parsed = parse_response_text(&expected_response);
assert_eq!(parsed["success"], true, "Ingest should succeed");
assert!(parsed["nodeId"].is_string(), "Should return nodeId");
}
/// Test ingest tool rejects empty content.
#[test]
fn test_ingest_tool_rejects_empty_content() {
let _tool_call = json!({
"name": "ingest",
"arguments": {
"content": ""
}
});
// Expected error response
let expected_error = json!({
"content": [{
"type": "text",
"text": "{\"error\": \"Content cannot be empty\"}"
}],
"isError": true
});
assert_eq!(expected_error["isError"], true, "Empty content should be an error");
}
/// Test ingest tool with all optional fields.
#[test]
fn test_ingest_tool_with_all_fields() {
let tool_call = json!({
"name": "ingest",
"arguments": {
"content": "Complex knowledge with all metadata.",
"nodeType": "decision",
"tags": ["architecture", "design"],
"source": "team meeting notes"
}
});
// All fields should be accepted
assert!(tool_call["arguments"]["content"].is_string());
assert!(tool_call["arguments"]["nodeType"].is_string());
assert!(tool_call["arguments"]["tags"].is_array());
assert!(tool_call["arguments"]["source"].is_string());
}
// ============================================================================
// RECALL TOOL TESTS (3 tests)
// ============================================================================
/// Test recall tool with valid query.
#[test]
fn test_recall_tool_valid_query() {
let _tool_call = json!({
"name": "recall",
"arguments": {
"query": "rust programming",
"limit": 10
}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"query\": \"rust programming\", \"total\": 1, \"results\": [{\"id\": \"test-id\", \"content\": \"Rust is safe\"}]}"
}],
"isError": false
});
validate_tool_response(&expected_response);
let parsed = parse_response_text(&expected_response);
assert!(parsed["query"].is_string(), "Should echo query");
assert!(parsed["results"].is_array(), "Should return results array");
}
/// Test recall tool rejects empty query.
#[test]
fn test_recall_tool_rejects_empty_query() {
let tool_call = json!({
"name": "recall",
"arguments": {
"query": ""
}
});
// Empty query should be rejected
assert!(tool_call["arguments"]["query"].as_str().unwrap().is_empty());
// Expected behavior: return error with isError: true
}
/// Test recall tool clamps limit values.
#[test]
fn test_recall_tool_clamps_limit() {
// Test minimum clamping
let min_call = json!({
"name": "recall",
"arguments": {
"query": "test",
"limit": 0
}
});
let limit = min_call["arguments"]["limit"].as_i64().unwrap();
assert!(limit < 1, "Limit 0 should be clamped to 1");
// Test maximum clamping
let max_call = json!({
"name": "recall",
"arguments": {
"query": "test",
"limit": 1000
}
});
let limit = max_call["arguments"]["limit"].as_i64().unwrap();
assert!(limit > 100, "Limit 1000 should be clamped to 100");
}
// ============================================================================
// SEMANTIC SEARCH TESTS (2 tests)
// ============================================================================
/// Test semantic search with valid parameters.
#[test]
fn test_semantic_search_valid() {
let _tool_call = json!({
"name": "semantic_search",
"arguments": {
"query": "memory management concepts",
"limit": 5,
"minSimilarity": 0.7
}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"query\": \"memory management concepts\", \"method\": \"semantic\", \"total\": 2, \"results\": []}"
}],
"isError": false
});
validate_tool_response(&expected_response);
let parsed = parse_response_text(&expected_response);
assert_eq!(parsed["method"], "semantic", "Should indicate semantic search");
}
/// Test semantic search handles embedding not ready.
#[test]
fn test_semantic_search_embedding_not_ready() {
// When embeddings aren't initialized, should return helpful error
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"error\": \"Embedding service not ready\", \"hint\": \"Run consolidation first\"}"
}],
"isError": false
});
let parsed = parse_response_text(&expected_response);
assert!(parsed["error"].is_string(), "Should explain embedding not ready");
assert!(parsed["hint"].is_string(), "Should provide hint");
}
// ============================================================================
// HYBRID SEARCH TESTS (2 tests)
// ============================================================================
/// Test hybrid search with weights.
#[test]
fn test_hybrid_search_with_weights() {
let _tool_call = json!({
"name": "hybrid_search",
"arguments": {
"query": "error handling patterns",
"limit": 10,
"keywordWeight": 0.3,
"semanticWeight": 0.7
}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"query\": \"error handling patterns\", \"method\": \"hybrid\", \"total\": 0, \"results\": []}"
}],
"isError": false
});
validate_tool_response(&expected_response);
let parsed = parse_response_text(&expected_response);
assert_eq!(parsed["method"], "hybrid", "Should indicate hybrid search");
}
/// Test hybrid search with default weights.
#[test]
fn test_hybrid_search_default_weights() {
let tool_call = json!({
"name": "hybrid_search",
"arguments": {
"query": "testing strategies"
}
});
// Default weights should be 0.5/0.5
assert!(tool_call["arguments"].get("keywordWeight").is_none());
assert!(tool_call["arguments"].get("semanticWeight").is_none());
}
// ============================================================================
// KNOWLEDGE MANAGEMENT TESTS (2 tests)
// ============================================================================
/// Test get_knowledge by ID.
#[test]
fn test_get_knowledge_by_id() {
let _tool_call = json!({
"name": "get_knowledge",
"arguments": {
"nodeId": "abc-123-def"
}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"id\": \"abc-123-def\", \"content\": \"Test content\", \"nodeType\": \"fact\"}"
}],
"isError": false
});
validate_tool_response(&expected_response);
let parsed = parse_response_text(&expected_response);
assert!(parsed["id"].is_string(), "Should return node ID");
assert!(parsed["content"].is_string(), "Should return content");
}
/// Test delete_knowledge by ID.
#[test]
fn test_delete_knowledge_by_id() {
let _tool_call = json!({
"name": "delete_knowledge",
"arguments": {
"nodeId": "to-delete-123"
}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"success\": true, \"deleted\": true}"
}],
"isError": false
});
let parsed = parse_response_text(&expected_response);
assert_eq!(parsed["success"], true, "Delete should succeed");
}
// ============================================================================
// REVIEW TOOL TESTS (2 tests)
// ============================================================================
/// Test mark_reviewed with FSRS rating.
#[test]
fn test_mark_reviewed_with_rating() {
let tool_call = json!({
"name": "mark_reviewed",
"arguments": {
"nodeId": "review-node-123",
"rating": 3 // Good
}
});
// Rating values: 1=Again, 2=Hard, 3=Good, 4=Easy
let rating = tool_call["arguments"]["rating"].as_i64().unwrap();
assert!((1..=4).contains(&rating), "Rating must be 1-4");
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"success\": true, \"nextReview\": \"2024-01-20T10:00:00Z\"}"
}],
"isError": false
});
let parsed = parse_response_text(&expected_response);
assert_eq!(parsed["success"], true, "Review should succeed");
assert!(parsed["nextReview"].is_string(), "Should return next review date");
}
/// Test mark_reviewed with invalid rating.
#[test]
fn test_mark_reviewed_invalid_rating() {
let invalid_ratings = [0, 5, -1, 100];
for rating in invalid_ratings {
let tool_call = json!({
"name": "mark_reviewed",
"arguments": {
"nodeId": "test-node",
"rating": rating
}
});
// Rating should be validated
let r = tool_call["arguments"]["rating"].as_i64().unwrap();
assert!(!(1..=4).contains(&r), "Rating {} should be invalid", r);
}
}
// ============================================================================
// STATS AND MAINTENANCE TESTS (2 tests)
// ============================================================================
/// Test get_stats returns system statistics.
#[test]
fn test_get_stats() {
let _tool_call = json!({
"name": "get_stats",
"arguments": {}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"totalNodes\": 42, \"averageRetention\": 0.85, \"embeddingsGenerated\": 40}"
}],
"isError": false
});
validate_tool_response(&expected_response);
let parsed = parse_response_text(&expected_response);
assert!(parsed["totalNodes"].is_number(), "Should return total nodes");
assert!(parsed["averageRetention"].is_number(), "Should return average retention");
}
/// Test health_check returns health status.
#[test]
fn test_health_check() {
let _tool_call = json!({
"name": "health_check",
"arguments": {}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"status\": \"healthy\", \"database\": \"ok\", \"embeddings\": \"ready\"}"
}],
"isError": false
});
let parsed = parse_response_text(&expected_response);
assert!(parsed["status"].is_string(), "Should return status");
}
// ============================================================================
// INTENTION TOOL TESTS (5 tests)
// ============================================================================
/// Test set_intention creates a new intention.
#[test]
fn test_set_intention_basic() {
let _tool_call = json!({
"name": "set_intention",
"arguments": {
"description": "Remember to review error handling",
"priority": "high"
}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"success\": true, \"intentionId\": \"int-123\", \"priority\": 3}"
}],
"isError": false
});
let parsed = parse_response_text(&expected_response);
assert_eq!(parsed["success"], true, "Should succeed");
assert!(parsed["intentionId"].is_string(), "Should return intention ID");
assert_eq!(parsed["priority"], 3, "High priority should be 3");
}
/// Test set_intention with time trigger.
#[test]
fn test_set_intention_with_time_trigger() {
let tool_call = json!({
"name": "set_intention",
"arguments": {
"description": "Check build status",
"trigger": {
"type": "time",
"inMinutes": 30
}
}
});
let trigger = &tool_call["arguments"]["trigger"];
assert_eq!(trigger["type"], "time", "Should be time trigger");
assert!(trigger["inMinutes"].is_number(), "Should have duration");
}
/// Test check_intentions with context matching.
#[test]
fn test_check_intentions_with_context() {
let _tool_call = json!({
"name": "check_intentions",
"arguments": {
"context": {
"codebase": "payments-service",
"file": "src/handlers/payment.rs",
"topics": ["error handling", "validation"]
}
}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"triggered\": [{\"id\": \"int-1\", \"description\": \"Review payments\"}], \"pending\": []}"
}],
"isError": false
});
let parsed = parse_response_text(&expected_response);
assert!(parsed["triggered"].is_array(), "Should return triggered intentions");
assert!(parsed["pending"].is_array(), "Should return pending intentions");
}
/// Test complete_intention marks as fulfilled.
#[test]
fn test_complete_intention() {
let _tool_call = json!({
"name": "complete_intention",
"arguments": {
"intentionId": "int-to-complete"
}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"success\": true, \"message\": \"Intention marked as complete\"}"
}],
"isError": false
});
let parsed = parse_response_text(&expected_response);
assert_eq!(parsed["success"], true, "Should succeed");
}
/// Test list_intentions with status filter.
#[test]
fn test_list_intentions_with_filter() {
let _tool_call = json!({
"name": "list_intentions",
"arguments": {
"status": "active",
"limit": 10
}
});
let expected_response = json!({
"content": [{
"type": "text",
"text": "{\"intentions\": [], \"total\": 0, \"status\": \"active\"}"
}],
"isError": false
});
let parsed = parse_response_text(&expected_response);
assert!(parsed["intentions"].is_array(), "Should return intentions array");
assert_eq!(parsed["status"], "active", "Should echo status filter");
}
// ============================================================================
// INPUT SCHEMA VALIDATION TESTS (2 tests)
// ============================================================================
/// Test tool input schemas have proper JSON Schema format.
#[test]
fn test_tool_schemas_are_valid_json_schema() {
let ingest_schema = json!({
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The content to remember"
},
"nodeType": {
"type": "string",
"description": "Type of knowledge"
},
"tags": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["content"]
});
assert_eq!(ingest_schema["type"], "object", "Schema must be object type");
assert!(ingest_schema["properties"].is_object(), "Must have properties");
assert!(ingest_schema["required"].is_array(), "Must specify required fields");
}
/// Test all tools have required inputSchema fields.
#[test]
fn test_all_tools_have_schema() {
let tool_definitions = vec![
("ingest", vec!["content"]),
("recall", vec!["query"]),
("semantic_search", vec!["query"]),
("hybrid_search", vec!["query"]),
("get_knowledge", vec!["nodeId"]),
("delete_knowledge", vec!["nodeId"]),
("mark_reviewed", vec!["nodeId", "rating"]),
("set_intention", vec!["description"]),
("complete_intention", vec!["intentionId"]),
("snooze_intention", vec!["intentionId"]),
];
for (tool_name, required_fields) in tool_definitions {
assert!(!required_fields.is_empty(),
"Tool {} should have at least one required field", tool_name);
}
}