mcp-execution-server 0.7.0

MCP server for progressive loading TypeScript code generation
Documentation
//! Integration tests for mcp-server.
//!
//! These tests verify the public API and state management of the mcp-server crate.
//! Note: MCP tool methods generated by #[tool] macro are private and tested via
//! unit tests in service.rs, so we focus on testing public types and state manager.

use chrono::Duration;
use mcp_execution_core::{ServerConfig, ServerId, ToolName};
use mcp_execution_introspector::{ServerCapabilities, ServerInfo, ToolInfo};
use mcp_execution_server::{CategorizedTool, GeneratorService, PendingGeneration, StateManager};
use rmcp::handler::server::ServerHandler;
use std::sync::Arc;

// ============================================================================
// Service Initialization Tests
// ============================================================================

#[test]
fn test_service_info_has_correct_capabilities() {
    let service = GeneratorService::new();
    let info = service.get_info();

    // Verify protocol version
    assert_eq!(
        info.protocol_version,
        rmcp::model::ProtocolVersion::V_2025_06_18
    );

    // Verify tools are enabled
    assert!(info.capabilities.tools.is_some());
    let tools = info.capabilities.tools.as_ref().unwrap();
    assert!(!tools.list_changed.unwrap_or(false));

    // Verify instructions are provided
    assert!(info.instructions.is_some());
    let instructions = info.instructions.unwrap();
    assert!(instructions.contains("progressive loading"));
    assert!(instructions.contains("introspect_server"));
    assert!(instructions.contains("save_categorized_tools"));
}

#[test]
fn test_service_new_creates_fresh_instance() {
    let service1 = GeneratorService::new();
    let service2 = GeneratorService::new();

    // Each service should be a separate instance
    let info1 = service1.get_info();
    let info2 = service2.get_info();

    // Both should have the same capabilities
    assert_eq!(info1.protocol_version, info2.protocol_version);
    assert!(info1.capabilities.tools.is_some());
    assert!(info2.capabilities.tools.is_some());
}

// ============================================================================
// State Management Integration Tests
// ============================================================================

#[tokio::test]
async fn test_state_manager_workflow() {
    let state = StateManager::new();

    // Create test server info
    let server_id = ServerId::new("test-server");
    let server_info = create_test_server_info(server_id.clone());

    let config = ServerConfig::builder().command("echo".to_string()).build();
    let output_dir = std::env::temp_dir().join("mcp-server-test");

    // Store pending generation
    let pending = PendingGeneration::new(server_id, server_info, config, output_dir);
    let session_id = state.store(pending.clone()).await;

    // Verify it's stored
    assert_eq!(state.pending_count().await, 1);

    // Retrieve it
    let retrieved = state.take(session_id).await;
    assert!(retrieved.is_some(), "Should retrieve stored session");
    assert_eq!(retrieved.unwrap().server_id, pending.server_id);

    // Verify it's consumed
    assert_eq!(state.pending_count().await, 0);

    // Second take should fail
    let second = state.take(session_id).await;
    assert!(second.is_none(), "Session should be consumed");
}

#[tokio::test]
async fn test_multiple_concurrent_sessions() {
    let state = StateManager::new();

    // Create multiple pending generations
    let mut sessions = Vec::new();
    for i in 0..5 {
        let server_id = ServerId::new(&format!("server-{i}"));
        let server_info = ServerInfo {
            id: server_id.clone(),
            name: format!("Server {i}"),
            version: "1.0.0".to_string(),
            capabilities: ServerCapabilities {
                supports_tools: true,
                supports_resources: false,
                supports_prompts: false,
            },
            tools: vec![],
        };

        let config = ServerConfig::builder().command("echo".to_string()).build();
        let output_dir = std::env::temp_dir().join(format!("mcp-test-{i}"));

        let pending = PendingGeneration::new(server_id, server_info, config, output_dir);
        let session_id = state.store(pending).await;
        sessions.push(session_id);
    }

    // Verify all sessions are accessible
    assert_eq!(state.pending_count().await, 5);

    // Retrieve each session
    for session_id in sessions {
        let retrieved = state.take(session_id).await;
        assert!(retrieved.is_some(), "Session should be retrievable");
    }

    // All sessions should be consumed
    assert_eq!(state.pending_count().await, 0);
}

#[tokio::test]
async fn test_state_manager_handles_expiration() {
    let state = StateManager::new();

    let server_id = ServerId::new("test");
    let server_info = create_test_server_info(server_id.clone());
    let config = ServerConfig::builder().command("echo".to_string()).build();
    let output_dir = std::env::temp_dir().join("mcp-expire-test");

    // Create and manually expire a session
    let mut pending = PendingGeneration::new(server_id, server_info, config, output_dir);
    pending.expires_at = chrono::Utc::now() - Duration::hours(1);

    let session_id = state.store(pending).await;

    // Should not be retrievable (expired)
    let retrieved = state.take(session_id).await;
    assert!(
        retrieved.is_none(),
        "Expired session should not be retrievable"
    );

    // Pending count should be 0 (expired sessions are cleaned up)
    assert_eq!(state.pending_count().await, 0);
}

#[tokio::test]
async fn test_state_manager_lazy_cleanup() {
    let state = StateManager::new();

    // Create valid session
    let valid_pending = create_test_pending("valid-server");
    state.store(valid_pending).await;

    // Create expired session
    let mut expired_pending = create_test_pending("expired-server");
    expired_pending.expires_at = chrono::Utc::now() - Duration::hours(1);
    state.store(expired_pending).await;

    // Pending count should only include valid sessions
    assert_eq!(state.pending_count().await, 1);

    // Explicit cleanup
    let removed = state.cleanup_expired().await;
    assert_eq!(removed, 1, "Should remove 1 expired session");
}

#[tokio::test]
async fn test_state_manager_get_without_consuming() {
    let state = StateManager::new();

    let pending = create_test_pending("test");
    let session_id = state.store(pending).await;

    // Get without consuming
    let first = state.get(session_id).await;
    assert!(first.is_some());

    // Should still be available
    let second = state.get(session_id).await;
    assert!(second.is_some());

    // Should still be available for take
    let taken = state.take(session_id).await;
    assert!(taken.is_some());

    // Now it should be gone
    let gone = state.get(session_id).await;
    assert!(gone.is_none());
}

// ============================================================================
// PendingGeneration Tests
// ============================================================================

#[test]
fn test_pending_generation_not_expired_initially() {
    let pending = create_test_pending("test");
    assert!(!pending.is_expired());
}

#[test]
fn test_pending_generation_expires_correctly() {
    let mut pending = create_test_pending("test");
    pending.expires_at = chrono::Utc::now() - Duration::minutes(1);
    assert!(pending.is_expired());
}

#[test]
fn test_pending_generation_has_correct_timeout() {
    let pending = create_test_pending("test");

    let duration = pending.expires_at - pending.created_at;
    let minutes = duration.num_minutes();

    assert_eq!(
        minutes,
        PendingGeneration::DEFAULT_TIMEOUT_MINUTES,
        "Should use default timeout"
    );
}

// ============================================================================
// CategorizedTool Tests
// ============================================================================

#[test]
fn test_categorized_tool_serialization_roundtrip() {
    let tool = CategorizedTool {
        name: "test_tool".to_string(),
        category: "testing".to_string(),
        keywords: "test,tool,demo".to_string(),
        short_description: "A test tool".to_string(),
    };

    let json = serde_json::to_string(&tool).unwrap();
    let deserialized: CategorizedTool = serde_json::from_str(&json).unwrap();

    assert_eq!(deserialized.name, tool.name);
    assert_eq!(deserialized.category, tool.category);
    assert_eq!(deserialized.keywords, tool.keywords);
    assert_eq!(deserialized.short_description, tool.short_description);
}

// ============================================================================
// Concurrent Access Tests
// ============================================================================

#[tokio::test]
async fn test_state_manager_concurrent_access() {
    let state = Arc::new(StateManager::new());
    let mut handles = vec![];

    // Spawn 10 concurrent store operations
    for i in 0..10 {
        let state_clone = Arc::clone(&state);
        handles.push(tokio::spawn(async move {
            let pending = create_test_pending(&format!("server-{i}"));
            state_clone.store(pending).await
        }));
    }

    // Wait for all operations to complete
    let mut session_ids = Vec::new();
    for handle in handles {
        let session_id = handle.await.unwrap();
        session_ids.push(session_id);
    }

    // All sessions should be stored
    assert_eq!(state.pending_count().await, 10);

    // All session IDs should be unique
    let unique_count = session_ids
        .iter()
        .collect::<std::collections::HashSet<_>>()
        .len();
    assert_eq!(unique_count, 10, "All session IDs should be unique");
}

#[tokio::test]
async fn test_state_manager_concurrent_read_write() {
    let state = Arc::new(StateManager::new());

    // Store initial session
    let pending = create_test_pending("test");
    let session_id = state.store(pending).await;

    let state_clone1 = Arc::clone(&state);
    let state_clone2 = Arc::clone(&state);

    // Concurrent reads should work
    let handle1 = tokio::spawn(async move { state_clone1.get(session_id).await });

    let handle2 = tokio::spawn(async move { state_clone2.get(session_id).await });

    let result1 = handle1.await.unwrap();
    let result2 = handle2.await.unwrap();

    assert!(result1.is_some());
    assert!(result2.is_some());
}

// ============================================================================
// Helper Functions
// ============================================================================

fn create_test_server_info(server_id: ServerId) -> ServerInfo {
    ServerInfo {
        id: server_id,
        name: "Test Server".to_string(),
        version: "1.0.0".to_string(),
        capabilities: ServerCapabilities {
            supports_tools: true,
            supports_resources: false,
            supports_prompts: false,
        },
        tools: vec![ToolInfo {
            name: ToolName::new("test_tool"),
            description: "A test tool".to_string(),
            input_schema: serde_json::json!({
                "type": "object",
                "properties": {
                    "message": {"type": "string"}
                },
                "required": ["message"]
            }),
            output_schema: None,
        }],
    }
}

fn create_test_pending(server_id_str: &str) -> PendingGeneration {
    let server_id = ServerId::new(server_id_str);
    let server_info = create_test_server_info(server_id.clone());
    let config = ServerConfig::builder().command("echo".to_string()).build();
    let output_dir = std::env::temp_dir().join(format!("mcp-test-{server_id_str}"));

    PendingGeneration::new(server_id, server_info, config, output_dir)
}