/// Comprehensive edge case tests for SessionUpdate variants use dirigent_protocol::types::{ ContentBlock, Meta, SessionUpdate, ToolCall, ToolCallContent, ToolCallStatus, }; use serde_json::json; // ===== UserMessageChunk Tests ===== /// Test UserMessageChunk minimal (no _meta) #[test] fn test_user_message_chunk_minimal() { let update = SessionUpdate::UserMessageChunk { message_id: "msg_001".to_string(), content: ContentBlock::Text { text: "Hello".to_string(), }, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""type":"user_message_chunk"#)); assert!(json.contains(r#""message_id":"msg_001"#)); assert!(!json.contains(r#""_meta""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } /// Test UserMessageChunk with empty message_id #[test] fn test_user_message_chunk_empty_message_id() { let update = SessionUpdate::UserMessageChunk { message_id: String::new(), content: ContentBlock::Text { text: "Test".to_string(), }, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""message_id":"""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); match deserialized { SessionUpdate::UserMessageChunk { message_id, .. } => { assert_eq!(message_id, ""); } _ => panic!("Expected UserMessageChunk"), } } /// Test UserMessageChunk with ResourceLink content #[test] fn test_user_message_chunk_with_resource_link() { let update = SessionUpdate::UserMessageChunk { message_id: "msg_002".to_string(), content: ContentBlock::ResourceLink { uri: "file:///path/to/file.txt".to_string(), name: Some("file.txt".to_string()), mime_type: Some("text/plain".to_string()), }, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""type":"user_message_chunk"#)); assert!(json.contains(r#""type":"resource_link"#)); // nested type assert!(json.contains(r#""uri":"file:///path/to/file.txt"#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } /// Test UserMessageChunk with _meta #[test] fn test_user_message_chunk_with_meta() { let update = SessionUpdate::UserMessageChunk { message_id: "msg_003".to_string(), content: ContentBlock::Text { text: "Test".to_string(), }, _meta: Some(Meta::default()), }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""_meta":{}"#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } // ===== AgentMessageChunk Tests ===== /// Test AgentMessageChunk minimal #[test] fn test_agent_message_chunk_minimal() { let update = SessionUpdate::AgentMessageChunk { message_id: "msg_agent_001".to_string(), content: ContentBlock::Text { text: "Agent response".to_string(), }, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""type":"agent_message_chunk"#)); assert!(json.contains(r#""message_id":"msg_agent_001"#)); assert!(!json.contains(r#""_meta""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } /// Test AgentMessageChunk with empty message_id #[test] fn test_agent_message_chunk_empty_message_id() { let update = SessionUpdate::AgentMessageChunk { message_id: String::new(), content: ContentBlock::Text { text: "Response".to_string(), }, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); match deserialized { SessionUpdate::AgentMessageChunk { message_id, .. } => { assert_eq!(message_id, ""); } _ => panic!("Expected AgentMessageChunk"), } } /// Test AgentMessageChunk with complex meta #[test] fn test_agent_message_chunk_with_complex_meta() { let mut extra = std::collections::HashMap::new(); extra.insert("timestamp".to_string(), json!("2025-11-10T12:00:00Z")); extra.insert("duration_ms".to_string(), json!(123)); let update = SessionUpdate::AgentMessageChunk { message_id: "msg_agent_002".to_string(), content: ContentBlock::Text { text: "Response".to_string(), }, _meta: Some(Meta { provider: None, extra, }), }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""_meta""#)); assert!(json.contains(r#""timestamp""#)); assert!(json.contains(r#""duration_ms""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } // ===== AgentThoughtChunk Tests ===== /// Test AgentThoughtChunk minimal #[test] fn test_agent_thought_chunk_minimal() { let update = SessionUpdate::AgentThoughtChunk { message_id: "msg_thought_001".to_string(), content: ContentBlock::Text { text: "Thinking...".to_string(), }, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""type":"agent_thought_chunk"#)); assert!(json.contains(r#""message_id":"msg_thought_001"#)); assert!(!json.contains(r#""_meta""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } /// Test AgentThoughtChunk with empty text #[test] fn test_agent_thought_chunk_empty_text() { let update = SessionUpdate::AgentThoughtChunk { message_id: "msg_thought_002".to_string(), content: ContentBlock::Text { text: String::new(), }, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""text":"""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } /// Test AgentThoughtChunk with very long text #[test] fn test_agent_thought_chunk_long_text() { let long_text = "Analyzing the problem...\n".repeat(1000); let update = SessionUpdate::AgentThoughtChunk { message_id: "msg_thought_003".to_string(), content: ContentBlock::Text { text: long_text.clone(), }, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); match deserialized { SessionUpdate::AgentThoughtChunk { content, .. } => { if let ContentBlock::Text { text } = content { assert_eq!(text.len(), long_text.len()); } else { panic!("Expected Text content"); } } _ => panic!("Expected AgentThoughtChunk"), } } // ===== ToolCall Tests ===== /// Test ToolCall variant minimal #[test] fn test_tool_call_variant_minimal() { let tool_call = ToolCall { id: "call_001".to_string(), tool_name: "bash".to_string(), status: ToolCallStatus::Pending, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let update = SessionUpdate::ToolCall { message_id: "msg_tool_001".to_string(), tool_call, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""type":"tool_call"#)); assert!(json.contains(r#""message_id":"msg_tool_001"#)); assert!(json.contains(r#""tool_call""#)); assert!(!json.contains(r#""_meta""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } /// Test ToolCall variant with complex nested ToolCall #[test] fn test_tool_call_variant_complex() { let tool_call = ToolCall { id: "call_002".to_string(), tool_name: "read_file".to_string(), status: ToolCallStatus::Completed, content: vec![ ToolCallContent::from_content_block(ContentBlock::Text { text: "Line 1".to_string(), }), ToolCallContent::from_content_block(ContentBlock::Text { text: "Line 2".to_string(), }), ], raw_input: Some(json!({"path": "/tmp/test.txt"})), raw_output: Some(json!({"bytes": 1024})), title: Some("Read file".to_string()), error: None, metadata: Some(json!({"duration_ms": 42})), origin: None, }; let update = SessionUpdate::ToolCall { message_id: "msg_tool_002".to_string(), tool_call, _meta: Some(Meta::default()), }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""type":"tool_call"#)); assert!(json.contains(r#""tool_call""#)); assert!(json.contains(r#""raw_input""#)); assert!(json.contains(r#""raw_output""#)); assert!(json.contains(r#""_meta""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } /// Test ToolCall variant with Error status #[test] fn test_tool_call_variant_with_error() { let tool_call = ToolCall { id: "call_003".to_string(), tool_name: "bash".to_string(), status: ToolCallStatus::Error, content: vec![], raw_input: Some(json!({"command": "invalid"})), raw_output: None, title: Some("Failed command".to_string()), error: Some("Command not found".to_string()), metadata: None, origin: None, }; let update = SessionUpdate::ToolCall { message_id: "msg_tool_003".to_string(), tool_call, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""status":"failed""#)); assert!(json.contains(r#""error":"Command not found""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } // ===== ToolCallUpdate Tests ===== /// Test ToolCallUpdate variant minimal #[test] fn test_tool_call_update_minimal() { let tool_call = ToolCall { id: "call_004".to_string(), tool_name: "bash".to_string(), status: ToolCallStatus::Running, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let update = SessionUpdate::ToolCallUpdate { message_id: "msg_update_001".to_string(), tool_call_id: "call_004".to_string(), tool_call, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""type":"tool_call_update"#)); assert!(json.contains(r#""message_id":"msg_update_001"#)); assert!(json.contains(r#""tool_call_id":"call_004"#)); assert!(json.contains(r#""tool_call""#)); assert!(!json.contains(r#""_meta""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } /// Test ToolCallUpdate with mismatched IDs #[test] fn test_tool_call_update_mismatched_ids() { // This is technically allowed by the type system, though semantically odd let tool_call = ToolCall { id: "call_005".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Running, content: vec![], raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }; let update = SessionUpdate::ToolCallUpdate { message_id: "msg_update_002".to_string(), tool_call_id: "call_DIFFERENT".to_string(), // Different from tool_call.id tool_call, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""tool_call_id":"call_DIFFERENT"#)); assert!(json.contains(r#""id":"call_005"#)); // nested id let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } /// Test ToolCallUpdate with completed status #[test] fn test_tool_call_update_completed() { let tool_call = ToolCall { id: "call_006".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/file"})), raw_output: Some(json!({"success": true})), title: Some("Read operation".to_string()), error: None, metadata: Some(json!({"lines": 42})), origin: None, }; let update = SessionUpdate::ToolCallUpdate { message_id: "msg_update_003".to_string(), tool_call_id: "call_006".to_string(), tool_call, _meta: Some(Meta::default()), }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""status":"completed""#)); assert!(json.contains(r#""raw_output""#)); assert!(json.contains(r#""_meta""#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } // ===== Type Tag Tests ===== /// Test all variants have correct snake_case type tags #[test] fn test_all_type_tags_snake_case() { let user_chunk = SessionUpdate::UserMessageChunk { message_id: "m1".to_string(), content: ContentBlock::Text { text: "test".to_string(), }, _meta: None, }; let json = serde_json::to_string(&user_chunk).unwrap(); assert!(json.contains(r#""type":"user_message_chunk"#)); assert!(!json.contains(r#""type":"UserMessageChunk"#)); let agent_chunk = SessionUpdate::AgentMessageChunk { message_id: "m2".to_string(), content: ContentBlock::Text { text: "test".to_string(), }, _meta: None, }; let json = serde_json::to_string(&agent_chunk).unwrap(); assert!(json.contains(r#""type":"agent_message_chunk"#)); let thought_chunk = SessionUpdate::AgentThoughtChunk { message_id: "m3".to_string(), content: ContentBlock::Text { text: "test".to_string(), }, _meta: None, }; let json = serde_json::to_string(&thought_chunk).unwrap(); assert!(json.contains(r#""type":"agent_thought_chunk"#)); let tool_call = SessionUpdate::ToolCall { message_id: "m4".to_string(), tool_call: ToolCall { id: "c1".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, }, _meta: None, }; let json = serde_json::to_string(&tool_call).unwrap(); assert!(json.contains(r#""type":"tool_call"#)); assert!(!json.contains(r#""type":"ToolCall"#)); let tool_call_update = SessionUpdate::ToolCallUpdate { message_id: "m5".to_string(), tool_call_id: "c2".to_string(), tool_call: ToolCall { id: "c2".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, }, _meta: None, }; let json = serde_json::to_string(&tool_call_update).unwrap(); assert!(json.contains(r#""type":"tool_call_update"#)); } // ===== Deserialization Error Cases ===== /// Test missing type field #[test] fn test_missing_type_field() { let json = r#"{ "message_id": "msg_001", "content": { "type": "text", "text": "Hello" } }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail without type field"); } /// Test invalid type value #[test] fn test_invalid_type_value() { let json = r#"{ "type": "invalid_message_chunk", "message_id": "msg_001", "content": { "type": "text", "text": "Hello" } }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail with invalid type"); } /// Test missing message_id #[test] fn test_missing_message_id() { let json = r#"{ "type": "user_message_chunk", "content": { "type": "text", "text": "Hello" } }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail without message_id"); } /// Test missing content field #[test] fn test_missing_content_field() { let json = r#"{ "type": "user_message_chunk", "message_id": "msg_001" }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail without content"); } /// Test missing tool_call field #[test] fn test_missing_tool_call_field() { let json = r#"{ "type": "tool_call", "message_id": "msg_001" }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail without tool_call"); } /// Test missing tool_call_id in ToolCallUpdate #[test] fn test_missing_tool_call_id() { let json = r#"{ "type": "tool_call_update", "message_id": "msg_001", "tool_call": { "id": "call_001", "tool_name": "test", "status": "running" } }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail without tool_call_id"); } /// Test null values for required fields #[test] fn test_null_required_values() { let json = r#"{ "type": "user_message_chunk", "message_id": null, "content": { "type": "text", "text": "Hello" } }"#; let result: Result = serde_json::from_str(json); assert!(result.is_err(), "Should fail with null message_id"); } /// Test null _meta (should deserialize as None) #[test] fn test_null_meta() { let json = r#"{ "type": "user_message_chunk", "message_id": "msg_001", "content": { "type": "text", "text": "Hello" }, "_meta": null }"#; let update: SessionUpdate = serde_json::from_str(json).unwrap(); match update { SessionUpdate::UserMessageChunk { _meta, .. } => { assert!(_meta.is_none()); } _ => panic!("Expected UserMessageChunk"), } } // ===== Roundtrip Tests ===== /// Test roundtrip for all variants #[test] fn test_all_variants_roundtrip() { let variants = vec![ SessionUpdate::UserMessageChunk { message_id: "msg_1".to_string(), content: ContentBlock::Text { text: "User message".to_string(), }, _meta: None, }, SessionUpdate::AgentMessageChunk { message_id: "msg_2".to_string(), content: ContentBlock::Text { text: "Agent response".to_string(), }, _meta: Some(Meta::default()), }, SessionUpdate::AgentThoughtChunk { message_id: "msg_3".to_string(), content: ContentBlock::Text { text: "Thinking...".to_string(), }, _meta: None, }, SessionUpdate::ToolCall { message_id: "msg_4".to_string(), tool_call: ToolCall { id: "call_1".to_string(), tool_name: "bash".to_string(), status: ToolCallStatus::Pending, content: vec![], raw_input: Some(json!({"cmd": "ls"})), raw_output: None, title: Some("List files".to_string()), error: None, metadata: None, origin: None, }, _meta: None, }, SessionUpdate::ToolCallUpdate { message_id: "msg_5".to_string(), tool_call_id: "call_1".to_string(), tool_call: ToolCall { id: "call_1".to_string(), tool_name: "bash".to_string(), status: ToolCallStatus::Completed, content: vec![ToolCallContent::from_content_block(ContentBlock::Text { text: "file1.txt\nfile2.txt".to_string(), })], raw_input: Some(json!({"cmd": "ls"})), raw_output: Some(json!({"exit_code": 0})), title: Some("List files".to_string()), error: None, metadata: Some(json!({"duration_ms": 100})), origin: None, }, _meta: Some(Meta::default()), }, ]; for variant in variants { let json = serde_json::to_string(&variant).unwrap(); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(variant, deserialized); } } // ===== Edge Cases: Complex Content ===== /// Test UserMessageChunk with complex nested content #[test] fn test_complex_nested_content() { let update = SessionUpdate::UserMessageChunk { message_id: "msg_complex".to_string(), content: ContentBlock::ResourceLink { uri: "data:text/plain;base64,SGVsbG8gV29ybGQh".to_string(), name: Some("embedded.txt".to_string()), mime_type: Some("text/plain; charset=utf-8".to_string()), }, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); assert_eq!(update, deserialized); } // ===== Clone and Debug ===== /// Test SessionUpdate clone #[test] fn test_session_update_clone() { let original = SessionUpdate::UserMessageChunk { message_id: "msg_clone".to_string(), content: ContentBlock::Text { text: "Test".to_string(), }, _meta: None, }; let cloned = original.clone(); assert_eq!(original, cloned); } /// Test SessionUpdate debug formatting #[test] fn test_session_update_debug() { let update = SessionUpdate::UserMessageChunk { message_id: "msg_debug".to_string(), content: ContentBlock::Text { text: "Debug test".to_string(), }, _meta: None, }; let debug_str = format!("{:?}", update); assert!(debug_str.contains("UserMessageChunk")); assert!(debug_str.contains("msg_debug")); } // ===== Edge Cases: Empty Collections ===== /// Test ToolCall with empty content array persists correctly #[test] fn test_tool_call_empty_content_persists() { let update = SessionUpdate::ToolCall { message_id: "msg_empty".to_string(), tool_call: ToolCall { id: "call_empty".to_string(), tool_name: "test".to_string(), status: ToolCallStatus::Pending, content: vec![], // Explicitly empty raw_input: None, raw_output: None, title: None, error: None, metadata: None, origin: None, }, _meta: None, }; let json = serde_json::to_string(&update).unwrap(); assert!(json.contains(r#""content":[]"#)); let deserialized: SessionUpdate = serde_json::from_str(&json).unwrap(); match deserialized { SessionUpdate::ToolCall { tool_call, .. } => { assert_eq!(tool_call.content.len(), 0); } _ => panic!("Expected ToolCall"), } }