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

602 lines
17 KiB
Rust

/// 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<ToolCallStatus, _> = serde_json::from_str(r#""invalid""#);
assert!(result.is_err(), "Should fail with invalid status");
let result: Result<ToolCallStatus, _> = 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<ToolCall, _> = 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<ToolCall, _> = 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<ToolCall, _> = 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<ToolCall, _> = 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"));
}