codex-memory 3.0.15

A simple memory storage service with MCP interface for Claude Desktop
Documentation
//! MCP Protocol Compliance Tests for CODEX-MCP-002, CODEX-MCP-004, CODEX-MCP-005

use codex_memory::{
    config::Config,
    error::Error,
    mcp_server::{handlers::MCPHandlers, MCPServer},
    storage::Storage,
};
use serde_json::{json, Value};
use std::sync::Arc;
use crate::common::test_db_manager::TestDatabaseManager;
use tokio::time::{timeout, Duration};

/// Test proper JSON-RPC error codes (CODEX-MCP-002)
#[tokio::test]
async fn test_json_rpc_error_codes() -> Result<(), Box<dyn std::error::Error>> {
    let mut db_manager = TestDatabaseManager::new()?;
    let pool = db_manager.setup_test_database().await?;

    let config = Config {
        database_url: db_manager.get_test_database_url(),
        mcp_port: 3333,
        log_level: "info".to_string(),
    };
    let storage = Arc::new(Storage::new(pool));
    let server = MCPServer::new(config, storage);

    // Test -32700 (Parse error) - malformed JSON
    let malformed_json = r#"{"jsonrpc":"2.0","method":"test","id":1"#; // Missing closing brace
    let response = server.handle_request(malformed_json).await;
    let response_json: Value = serde_json::from_str(&response)?;
    
    assert_eq!(response_json["error"]["code"], -32700);
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("Parse error"));

    // Test -32600 (Invalid Request) - missing method field
    let invalid_request = r#"{"jsonrpc":"2.0","id":1}"#;
    let response = server.handle_request(invalid_request).await;
    let response_json: Value = serde_json::from_str(&response)?;
    
    assert_eq!(response_json["error"]["code"], -32600);
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("Invalid request"));

    // Test -32601 (Method not found) - unknown method
    let unknown_method = r#"{"jsonrpc":"2.0","method":"unknown_method","id":1}"#;
    let response = server.handle_request(unknown_method).await;
    let response_json: Value = serde_json::from_str(&response)?;
    
    assert_eq!(response_json["error"]["code"], -32601);
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("Method not found"));

    // Test -32602 (Invalid params) - missing required parameter
    let invalid_params = r#"{"jsonrpc":"2.0","method":"tools/call","params":{"name":"store_memory"},"id":1}"#;
    let response = server.handle_request(invalid_params).await;
    let response_json: Value = serde_json::from_str(&response)?;
    
    assert_eq!(response_json["error"]["code"], -32602);
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("Invalid params"));

    db_manager.cleanup().await?;
    Ok(())
}

/// Test parameter validation limits (CODEX-MCP-005)
#[tokio::test]
async fn test_parameter_validation_limits() -> Result<(), Box<dyn std::error::Error>> {
    let mut db_manager = TestDatabaseManager::new()?;
    let pool = db_manager.setup_test_database().await?;

    let config = Config {
        database_url: db_manager.get_test_database_url(),
        mcp_port: 3333,
        log_level: "info".to_string(),
    };
    let storage = Arc::new(Storage::new(pool));
    let server = MCPServer::new(config, storage);

    // Test content size validation (max 1MB)
    let large_content = "a".repeat(1024 * 1024 + 1); // 1MB + 1 byte
    let large_content_request = json!({
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
            "name": "store_memory",
            "arguments": {
                "content": large_content,
                "context": "test",
                "summary": "test",
                "tags": ["test"]
            }
        },
        "id": 1
    });

    let response = server.handle_request(&large_content_request.to_string()).await;
    let response_json: Value = serde_json::from_str(&response)?;
    
    assert_eq!(response_json["error"]["code"], -32602);
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("Content size"));
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("1MB"));

    // Test context length validation (max 1000 chars)
    let long_context = "x".repeat(1001);
    let long_context_request = json!({
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
            "name": "store_memory",
            "arguments": {
                "content": "test content",
                "context": long_context,
                "summary": "test",
                "tags": ["test"]
            }
        },
        "id": 2
    });

    let response = server.handle_request(&long_context_request.to_string()).await;
    let response_json: Value = serde_json::from_str(&response)?;
    
    assert_eq!(response_json["error"]["code"], -32602);
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("Context length"));
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("1000 characters"));

    // Test summary length validation (max 500 chars)
    let long_summary = "y".repeat(501);
    let long_summary_request = json!({
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
            "name": "store_memory",
            "arguments": {
                "content": "test content",
                "context": "test context",
                "summary": long_summary,
                "tags": ["test"]
            }
        },
        "id": 3
    });

    let response = server.handle_request(&long_summary_request.to_string()).await;
    let response_json: Value = serde_json::from_str(&response)?;
    
    assert_eq!(response_json["error"]["code"], -32602);
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("Summary length"));
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("500 characters"));

    // Test tags count validation (max 50 tags)
    let many_tags: Vec<String> = (0..51).map(|i| format!("tag{}", i)).collect();
    let many_tags_request = json!({
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
            "name": "store_memory",
            "arguments": {
                "content": "test content",
                "context": "test context",
                "summary": "test summary",
                "tags": many_tags
            }
        },
        "id": 4
    });

    let response = server.handle_request(&many_tags_request.to_string()).await;
    let response_json: Value = serde_json::from_str(&response)?;
    
    assert_eq!(response_json["error"]["code"], -32602);
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("Tags count"));
    assert!(response_json["error"]["message"]
        .as_str()
        .unwrap()
        .contains("50 tags"));

    db_manager.cleanup().await?;
    Ok(())
}

/// Test request timeout handling (CODEX-MCP-004)
#[tokio::test]
async fn test_request_timeout_handling() -> Result<(), Box<dyn std::error::Error>> {
    let mut db_manager = TestDatabaseManager::new()?;
    let pool = db_manager.setup_test_database().await?;

    let _config = Config {
        database_url: db_manager.get_test_database_url(),
        mcp_port: 3333,
        log_level: "info".to_string(),
    };
    let storage = Arc::new(Storage::new(pool));
    let handlers = Arc::new(MCPHandlers::new(storage));

    // Create a slow operation that should timeout
    // We'll use a very large search query that could potentially be slow
    let search_params = json!({
        "query": "test query that might be slow",
        "max_results": 1000,
        "similarity_threshold": 0.1
    });

    // Test that timeout is enforced (should complete within reasonable time)
    let start = std::time::Instant::now();
    let result = timeout(
        Duration::from_secs(61), // Just over the 60s timeout
        handlers.handle_tool_call("search_memory", search_params)
    ).await;

    let elapsed = start.elapsed();

    // The operation should either:
    // 1. Complete successfully within timeout, or  
    // 2. Return a timeout error if it takes longer than 60s
    match result {
        Ok(Ok(_)) => {
            // Operation completed successfully
            assert!(elapsed.as_secs() <= 60, "Operation should complete within timeout");
        }
        Ok(Err(Error::Timeout(msg))) => {
            // Operation timed out as expected
            assert!(msg.contains("timed out after 60 seconds"));
        }
        Err(_) => {
            // Test timeout exceeded - operation took longer than 61s
            panic!("Operation should not take longer than test timeout of 61s");
        }
        Ok(Err(other)) => {
            // Other error occurred - this is acceptable for testing
            println!("Other error during timeout test: {}", other);
        }
    }

    db_manager.cleanup().await?;
    Ok(())
}

/// Test successful parameter validation cases
#[tokio::test]
async fn test_valid_parameter_acceptance() -> Result<(), Box<dyn std::error::Error>> {
    let mut db_manager = TestDatabaseManager::new()?;
    let pool = db_manager.setup_test_database().await?;

    let config = Config {
        database_url: db_manager.get_test_database_url(),
        mcp_port: 3333,
        log_level: "info".to_string(),
    };
    let storage = Arc::new(Storage::new(pool));
    let server = MCPServer::new(config, storage);

    // Test valid parameters within limits
    let valid_request = json!({
        "jsonrpc": "2.0",
        "method": "tools/call",
        "params": {
            "name": "store_memory",
            "arguments": {
                "content": "This is valid content within limits",
                "context": "Valid context under 1000 chars",
                "summary": "Valid summary under 500 chars",
                "tags": ["tag1", "tag2", "tag3"]
            }
        },
        "id": 1
    });

    let response = server.handle_request(&valid_request.to_string()).await;
    let response_json: Value = serde_json::from_str(&response)?;
    
    // Should succeed and return a result, not an error
    assert!(response_json.get("result").is_some());
    assert!(response_json.get("error").is_none());

    db_manager.cleanup().await?;
    Ok(())
}

/// Test error code consistency across different error types
#[tokio::test]
async fn test_error_code_consistency() -> Result<(), Box<dyn std::error::Error>> {
    // Test that our Error types consistently map to correct JSON-RPC codes
    let parse_error = Error::ParseError("test".to_string());
    assert_eq!(parse_error.json_rpc_code(), -32700);

    let invalid_request = Error::InvalidRequest("test".to_string());
    assert_eq!(invalid_request.json_rpc_code(), -32600);

    let method_not_found = Error::MethodNotFound("test".to_string());
    assert_eq!(method_not_found.json_rpc_code(), -32601);

    let invalid_params = Error::InvalidParams("test".to_string());
    assert_eq!(invalid_params.json_rpc_code(), -32602);

    let internal_error = Error::InternalError("test".to_string());
    assert_eq!(internal_error.json_rpc_code(), -32603);

    let timeout_error = Error::Timeout("test".to_string());
    assert_eq!(timeout_error.json_rpc_code(), -32603);

    // Test JSON-RPC error response format
    let error_response = parse_error.to_json_rpc_error(Some(json!(123)));
    assert_eq!(error_response["jsonrpc"], "2.0");
    assert_eq!(error_response["id"], 123);
    assert_eq!(error_response["error"]["code"], -32700);
    assert!(error_response["error"]["message"].as_str().unwrap().contains("test"));

    Ok(())
}