claude-sdk-rs 1.0.0

Rust SDK for Claude AI with CLI integration - type-safe async API for Claude Code and direct SDK usage
Documentation
#[cfg(test)]
mod tests {
    use crate::core::error::WorkflowError;
    use crate::clients::notion::{
        NotionClientBuilder, NotionClientNode, NotionConfig,
    };
    use crate::core::nodes::Node;
    use crate::core::nodes::external_mcp_client::RetryConfig;
    use crate::core::task::TaskContext;
    use serde_json::Value;

    #[test]
    fn test_notion_config_creation() {
        // Test HTTP configuration
        let http_config = NotionConfig::new_http(
            "http://localhost:8002".to_string(),
            Some("test-api-key".to_string()),
        );
        assert_eq!(http_config.base_config.service_name, "notion");
        assert!(matches!(
            http_config.base_config.transport,
            crate::transport::TransportType::Http { .. }
        ));
        assert!(http_config.base_config.auth.is_some());

        // Test WebSocket configuration
        let ws_config = NotionConfig::new_websocket("ws://localhost:8002".to_string());
        assert_eq!(ws_config.base_config.service_name, "notion");
        assert!(matches!(
            ws_config.base_config.transport,
            crate::transport::TransportType::WebSocket { .. }
        ));

        // Test stdio configuration
        let stdio_config =
            NotionConfig::new_stdio("python".to_string(), vec!["notion_server.py".to_string()]);
        assert_eq!(stdio_config.base_config.service_name, "notion");
        assert!(matches!(
            stdio_config.base_config.transport,
            crate::transport::TransportType::Stdio { .. }
        ));
    }

    #[test]
    fn test_notion_client_builder() {
        let client = NotionClientBuilder::new_http("http://localhost:8002".to_string())
            .with_api_key("test-key".to_string())
            .with_workspace_id("workspace-123".to_string())
            .with_default_database_id("db-456".to_string())
            .with_retry_config(RetryConfig {
                max_retries: 5,
                initial_delay_ms: 500,
                max_delay_ms: 10000,
                backoff_multiplier: 1.5,
            })
            .build();

        assert_eq!(
            client.config.workspace_id,
            Some("workspace-123".to_string())
        );
        assert_eq!(
            client.config.default_database_id,
            Some("db-456".to_string())
        );
        assert_eq!(client.config.base_config.retry_config.max_retries, 5);
    }

    #[test]
    fn test_node_implementation() {
        let client = NotionClientNode::new(NotionConfig::new_http(
            "http://localhost:8002".to_string(),
            None,
        ));

        // Test node name
        assert!(client.node_name().contains("NotionClientNode"));
        assert!(client.node_name().contains("notion"));

        // Test process method
        let mut context = TaskContext::new(
            "test-task".to_string(),
            Value::String("test-workflow".to_string()),
        );
        let result = client.process(context.clone()).unwrap();

        assert_eq!(
            result.get_data("notion_client_available").unwrap(),
            Some(Value::Bool(true))
        );
        assert!(
            result
                .get_data::<Value>("notion_client_config")
                .unwrap()
                .is_some()
        );
    }

    #[tokio::test]
    async fn test_search_pages_arguments() {
        let mut client = NotionClientNode::new(NotionConfig::new_http(
            "http://localhost:8002".to_string(),
            None,
        ));

        // Mock a successful tool call - in real tests this would use a mock MCP client
        // For now, we're testing that the arguments are properly formatted

        // This would normally call the MCP server, but since we can't mock it here,
        // we'll just verify the method compiles and can be called
        // In integration tests, we'll test against a real MCP server
    }

    #[test]
    fn test_error_parsing() {
        let client = NotionClientNode::new(NotionConfig::new_http(
            "http://localhost:8002".to_string(),
            None,
        ));

        // Test unauthorized error
        let auth_error = WorkflowError::MCPError {
            message: "Request failed with status 401 unauthorized".to_string(),
        };
        let parsed = client.parse_notion_error(&auth_error);
        match parsed {
            WorkflowError::MCPError { message } => {
                assert!(message.contains("Notion authentication failed"));
            }
            _ => panic!("Expected MCPError"),
        }

        // Test not found error
        let not_found_error = WorkflowError::MCPError {
            message: "Page not_found with status 404".to_string(),
        };
        let parsed = client.parse_notion_error(&not_found_error);
        match parsed {
            WorkflowError::MCPError { message } => {
                assert!(message.contains("Notion resource not found"));
            }
            _ => panic!("Expected MCPError"),
        }

        // Test rate limit error
        let rate_error = WorkflowError::MCPError {
            message: "Request failed with rate_limit status 429".to_string(),
        };
        let parsed = client.parse_notion_error(&rate_error);
        match parsed {
            WorkflowError::MCPError { message } => {
                assert!(message.contains("Notion rate limit exceeded"));
            }
            _ => panic!("Expected MCPError"),
        }
    }

    #[test]
    fn test_config_serialization() {
        let config = NotionConfig::new_http(
            "http://localhost:8002".to_string(),
            Some("api-key".to_string()),
        );

        // Test that config can be serialized
        let serialized = serde_json::to_string(&config).unwrap();
        let deserialized: NotionConfig = serde_json::from_str(&serialized).unwrap();

        assert_eq!(
            config.base_config.service_name,
            deserialized.base_config.service_name
        );
    }
}

#[cfg(test)]
mod mock_tests {
    use super::*;
    use crate::core::error::WorkflowError;
    use crate::protocol::{CallToolResult, ToolContent, ToolDefinition};
    use async_trait::async_trait;
    use serde_json::json;
    use std::{collections::HashMap, sync::Arc};
    use tokio::sync::Mutex;

    /// Mock MCP client for testing
    #[derive(Debug)]
    struct MockMCPClient {
        tools: Vec<ToolDefinition>,
        responses: Arc<Mutex<HashMap<String, CallToolResult>>>,
        connected: bool,
    }

    impl MockMCPClient {
        fn new() -> Self {
            let mut tools = vec![];

            // Add mock Notion tools
            tools.push(ToolDefinition {
                name: "search_pages".to_string(),
                description: Some("Search for pages in Notion".to_string()),
                input_schema: json!({
                    "type": "object",
                    "properties": {
                        "query": {
                            "type": "string",
                            "description": "Search query"
                        },
                        "limit": {
                            "type": "number",
                            "description": "Maximum number of results"
                        }
                    },
                    "required": ["query"]
                }),
            });

            tools.push(ToolDefinition {
                name: "create_page".to_string(),
                description: Some("Create a new page in Notion".to_string()),
                input_schema: json!({
                    "type": "object",
                    "properties": {
                        "title": {
                            "type": "string",
                            "description": "Page title"
                        },
                        "content": {
                            "type": "string",
                            "description": "Page content"
                        },
                        "parent_id": {
                            "type": "string",
                            "description": "Parent page or database ID"
                        }
                    },
                    "required": ["title", "content"]
                }),
            });

            Self {
                tools,
                responses: Arc::new(Mutex::new(HashMap::new())),
                connected: false,
            }
        }

        fn add_response(&self, tool_name: &str, response: CallToolResult) {
            let responses = self.responses.clone();
            let tool_name = tool_name.to_string();
            tokio::spawn(async move {
                let mut responses = responses.lock().await;
                responses.insert(tool_name, response);
            });
        }
    }

    #[async_trait]
    impl crate::clients::MCPClient for MockMCPClient {
        async fn connect(&mut self) -> Result<(), WorkflowError> {
            self.connected = true;
            Ok(())
        }

        async fn initialize(
            &mut self,
            _client_name: &str,
            _client_version: &str,
        ) -> Result<(), WorkflowError> {
            if !self.connected {
                return Err(WorkflowError::MCPConnectionError {
                    message: "Not connected".to_string(),
                });
            }
            Ok(())
        }

        async fn list_tools(&mut self) -> Result<Vec<ToolDefinition>, WorkflowError> {
            if !self.connected {
                return Err(WorkflowError::MCPConnectionError {
                    message: "Not connected".to_string(),
                });
            }
            Ok(self.tools.clone())
        }

        async fn call_tool(
            &mut self,
            name: &str,
            _arguments: Option<HashMap<String, serde_json::Value>>,
        ) -> Result<CallToolResult, WorkflowError> {
            if !self.connected {
                return Err(WorkflowError::MCPConnectionError {
                    message: "Not connected".to_string(),
                });
            }

            let responses = self.responses.lock().await;
            if let Some(response) = responses.get(name) {
                Ok(response.clone())
            } else {
                Ok(CallToolResult {
                    content: vec![ToolContent::Text {
                        text: json!({
                            "status": "success",
                            "tool": name,
                            "message": "Mock response"
                        })
                        .to_string(),
                    }],
                    is_error: Some(false),
                })
            }
        }

        async fn disconnect(&mut self) -> Result<(), WorkflowError> {
            self.connected = false;
            Ok(())
        }

        fn is_connected(&self) -> bool {
            self.connected
        }
    }
}