/// Comprehensive edge case tests for ToolCall and ToolCallStatus use dirigent_protocol::types::{ContentBlock, ToolCall, ToolCallContent, ToolCallStatus}; use serde_json::json; // ===== ToolCallStatus Tests ===== /// Test all ToolCallStatus variants serialize correctly #[test] fn test_all_status_variants_serialize() { let pending = ToolCallStatus::Pending; assert_eq!(serde_json::to_string(&pending).unwrap(), r#""pending""#); let running = ToolCallStatus::Running; assert_eq!(serde_json::to_string(&running).unwrap(), r#""running""#); let completed = ToolCallStatus::Completed; assert_eq!( serde_json::to_string(&completed).unwrap(), r#""completed""# ); let error = ToolCallStatus::Error; assert_eq!(serde_json::to_string(&error).unwrap(), r#""error""#); } /// Test ToolCallStatus deserialization #[test] fn test_status_deserialization() { let status: ToolCallStatus = serde_json::from_str(r#""pending""#).unwrap(); assert_eq!(status, ToolCallStatus::Pending); let status: ToolCallStatus = serde_json::from_str(r#""running""#).unwrap(); assert_eq!(status, ToolCallStatus::Running); let status: ToolCallStatus = serde_json::from_str(r#""completed""#).unwrap(); assert_eq!(status, ToolCallStatus::Completed); let status: ToolCallStatus = serde_json::from_str(r#""error""#).unwrap(); assert_eq!(status, ToolCallStatus::Error); } /// Test invalid status deserialization #[test] fn test_invalid_status_deserialization() { let result: Result = serde_json::from_str(r#""invalid""#); assert!(result.is_err(), "Should fail with invalid status"); let result: Result = serde_json::from_str(r#""PENDING""#); assert!( result.is_err(), "Should fail with uppercase (not snake_case)" ); } /// Test ToolCallStatus roundtrip #[test] fn test_status_roundtrip() { let statuses = [ ToolCallStatus::Pending, ToolCallStatus::Running, ToolCallStatus::Completed, ToolCallStatus::Error, ]; for status in statuses { let json = serde_json::to_string(&status).unwrap(); let deserialized: ToolCallStatus = serde_json::from_str(&json).unwrap(); assert_eq!(status, deserialized); } } /// Test ToolCallStatus equality and copy #[test] fn test_status_equality_and_copy() { let status1 = ToolCallStatus::Pending; let status2 = status1; // Copy assert_eq!(status1, status2); let status3 = ToolCallStatus::Running; assert_ne!(status1, status3); } // ===== ToolCall Minimal Tests ===== /// Test ToolCall with minimal required fields #[test] fn test_tool_call_minimal() { let tool_call = ToolCall { id: "call_min".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Pending, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); // Required fields present assert!(json.contains(r#""id":"call_min""#)); assert!(json.contains(r#""tool_name":"test""#)); assert!(json.contains(r#""status":"pending""#)); assert!(json.contains(r#""content":[]"#)); // Optional fields not present assert!(!json.contains(r#""raw_input""#)); assert!(!json.contains(r#""raw_output""#)); assert!(!json.contains(r#""title""#)); assert!(!json.contains(r#""error""#)); assert!(!json.contains(r#""metadata""#)); } /// Test ToolCall with all fields populated #[test] fn test_tool_call_maximal() { let tool_call = ToolCall { id: "call_max".to_string(), tool_name: "bash".to_string(), status: ToolCallStatus::Completed, content: vec![ToolCallContent::from_content_block(ContentBlock::Text { text: "Output".to_string(), })], raw_input: Some(json!({"command": "ls"})), raw_output: Some(json!({"exit_code": 0})), title: Some("List files".to_string()), error: None, metadata: Some(json!({"duration_ms": 123})), origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); // All fields present assert!(json.contains(r#""id":"call_max""#)); assert!(json.contains(r#""tool_name":"bash""#)); assert!(json.contains(r#""status":"completed""#)); assert!(json.contains(r#""content""#)); assert!(json.contains(r#""raw_input""#)); assert!(json.contains(r#""raw_output""#)); assert!(json.contains(r#""title":"List files""#)); assert!(json.contains(r#""metadata""#)); } // ===== Edge Cases: Empty Strings ===== /// Test empty tool_name #[test] fn test_empty_tool_name() { let tool_call = ToolCall { id: "call_empty_name".to_string(), tool_name: String::new(), status: ToolCallStatus::Pending, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); assert!(json.contains(r#""tool_name":"""#)); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.tool_name, ""); } /// Test empty id #[test] fn test_empty_id() { let tool_call = ToolCall { id: String::new(), tool_name: "test".to_string(), status: ToolCallStatus::Pending, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); assert!(json.contains(r#""id":"""#)); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.id, ""); } /// Test empty title (Some("")) #[test] fn test_empty_title() { let tool_call = ToolCall { id: "call_empty_title".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Pending, content: vec![], raw_input: None, raw_output: None, title: Some(String::new()), error: None, metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); assert!(json.contains(r#""title":"""#)); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.title, Some(String::new())); } /// Test empty error message (Some("")) #[test] fn test_empty_error_message() { let tool_call = ToolCall { id: "call_empty_error".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Error, content: vec![], raw_input: None, raw_output: None, title: None, error: Some(String::new()), metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); assert!(json.contains(r#""error":"""#)); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.error, Some(String::new())); } // ===== Edge Cases: Large Data ===== /// Test very long error message #[test] fn test_long_error_message() { let long_error = "Error: ".to_string() + &"x".repeat(10_000); let tool_call = ToolCall { id: "call_long_error".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Error, content: vec![], raw_input: None, raw_output: None, title: None, error: Some(long_error.clone()), metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.error.unwrap().len(), long_error.len()); } /// Test large metadata #[test] fn test_large_metadata() { let large_meta = json!({ "key1": "value".repeat(1000), "key2": [1, 2, 3, 4, 5], "nested": { "deep": { "value": "test" } } }); let tool_call = ToolCall { id: "call_large_meta".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Completed, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: Some(large_meta.clone()), origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.metadata, Some(large_meta)); } /// Test many content blocks #[test] fn test_many_content_blocks() { let mut content = vec![]; for i in 0..100 { content.push(ToolCallContent::from_content_block(ContentBlock::Text { text: format!("Line {}", i), })); } let tool_call = ToolCall { id: "call_many_blocks".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Running, content: content.clone(), raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.content.len(), 100); assert_eq!(deserialized.content, content); } // ===== Edge Cases: Special Characters ===== /// Test special characters in tool_name #[test] fn test_special_chars_in_tool_name() { let tool_call = ToolCall { id: "call_special".to_string(), tool_name: "bash::execute!@#$%^&*()".to_string(), status: ToolCallStatus::Pending, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.tool_name, "bash::execute!@#$%^&*()"); } /// Test unicode in error message #[test] fn test_unicode_in_error() { let tool_call = ToolCall { id: "call_unicode".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Error, content: vec![], raw_input: None, raw_output: None, title: None, error: Some("错误: 文件不存在 🚫".to_string()), metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.error.unwrap(), "错误: 文件不存在 🚫"); } // ===== Default Content Field ===== /// Test that content defaults to empty vec when not in JSON #[test] fn test_content_default() { let json = r#"{ "id": "call_default", "tool_name": "test", "status": "pending" }"#; let tool_call: ToolCall = serde_json::from_str(json).unwrap(); assert_eq!(tool_call.content, vec![]); } /// Test that explicit empty content works #[test] fn test_explicit_empty_content() { let json = r#"{ "id": "call_explicit", "tool_name": "test", "status": "pending", "content": [] }"#; let tool_call: ToolCall = serde_json::from_str(json).unwrap(); assert_eq!(tool_call.content, vec![]); } // ===== Error Cases ===== /// Test missing required field (id) #[test] fn test_missing_id() { let json = r#"{ "tool_name": "test", "status": "pending" }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail without id"); } /// Test missing required field (tool_name) #[test] fn test_missing_tool_name() { let json = r#"{ "id": "call_test", "status": "pending" }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail without tool_name"); } /// Test missing required field (status) #[test] fn test_missing_status() { let json = r#"{ "id": "call_test", "tool_name": "test" }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail without status"); } /// Test null values for required fields #[test] fn test_null_required_fields() { let json = r#"{ "id": null, "tool_name": "test", "status": "pending" }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail with null id"); } /// Test null values for optional fields (should be None) #[test] fn test_null_optional_fields() { let json = r#"{ "id": "call_null_opts", "tool_name": "test", "status": "pending", "raw_input": null, "raw_output": null, "title": null, "error": null, "metadata": null }"#; let tool_call: ToolCall = serde_json::from_str(json).unwrap(); assert!(tool_call.raw_input.is_none()); assert!(tool_call.raw_output.is_none()); assert!(tool_call.title.is_none()); assert!(tool_call.error.is_none()); assert!(tool_call.metadata.is_none()); } // ===== Status-Specific Tests ===== /// Test Error status with error message #[test] fn test_error_status_with_message() { let tool_call = ToolCall { id: "call_error".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Error, content: vec![], raw_input: None, raw_output: None, title: None, error: Some("Something went wrong".to_string()), metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); assert!(json.contains(r#""status":"error""#)); assert!(json.contains(r#""error":"Something went wrong""#)); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.status, ToolCallStatus::Error); assert_eq!( deserialized.error, Some("Something went wrong".to_string()) ); } /// Test Completed status with output #[test] fn test_completed_status_with_output() { let tool_call = ToolCall { id: "call_completed".to_string(), tool_name: "read".to_string(), status: ToolCallStatus::Completed, content: vec![ToolCallContent::from_content_block(ContentBlock::Text { text: "File contents".to_string(), })], raw_input: Some(json!({"path": "/tmp/test.txt"})), raw_output: Some(json!({"bytes_read": 1024})), title: Some("Read file".to_string()), error: None, metadata: Some(json!({"duration_ms": 42})), origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.status, ToolCallStatus::Completed); assert!(deserialized.raw_output.is_some()); assert!(deserialized.error.is_none()); } // ===== Roundtrip Tests ===== /// Test roundtrip for all status variants #[test] fn test_roundtrip_all_statuses() { let statuses = [ ToolCallStatus::Pending, ToolCallStatus::Running, ToolCallStatus::Completed, ToolCallStatus::Error, ]; for status in statuses { let tool_call = ToolCall { id: format!("call_{:?}", status), tool_name: "test".to_string(), status, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let json = serde_json::to_string(&tool_call).unwrap(); let deserialized: ToolCall = serde_json::from_str(&json).unwrap(); assert_eq!(tool_call, deserialized); } } // ===== Clone and Debug ===== /// Test ToolCall clone #[test] fn test_tool_call_clone() { let original = ToolCall { id: "call_clone".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Pending, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let cloned = original.clone(); assert_eq!(original, cloned); } /// Test ToolCall debug formatting #[test] fn test_tool_call_debug() { let tool_call = ToolCall { id: "call_debug".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Pending, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let debug_str = format!("{:?}", tool_call); assert!(debug_str.contains("ToolCall")); assert!(debug_str.contains("call_debug")); }