Files
2026-05-08 01:59:04 +02:00

797 lines
24 KiB
Rust

/// 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<SessionUpdate, _> = 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<SessionUpdate, _> = 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<SessionUpdate, _> = 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<SessionUpdate, _> = 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<SessionUpdate, _> = 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<SessionUpdate, _> = 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<SessionUpdate, _> = 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"),
}
}