602 lines
17 KiB
Rust
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"));
|
|
}
|