Add 54 integration tests for CLI, tools, and message serialization
New test files: - crates/g3-cli/tests/cli_integration_test.rs (14 tests) Blackbox CLI tests: help/version flags, argument validation, conflicting modes, flock mode requirements - crates/g3-core/tests/tool_execution_test.rs (20 tests) Tool call structure tests and unified diff application: read_file, write_file, str_replace, shell, background_process, todo, final_output, code_search, take_screenshot - crates/g3-providers/tests/message_serialization_test.rs (20 tests) Round-trip serialization tests for Message, MessageRole, CacheControl, and Tool types. Covers Unicode, special chars, and edge cases. All tests follow blackbox/integration-first principles with documentation of what they protect and intentionally do not assert.
This commit is contained in:
351
crates/g3-providers/tests/message_serialization_test.rs
Normal file
351
crates/g3-providers/tests/message_serialization_test.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
//! Message Serialization Integration Tests
|
||||
//!
|
||||
//! CHARACTERIZATION: These tests verify that Message and related types
|
||||
//! serialize and deserialize correctly, ensuring data integrity across
|
||||
//! the system boundary (JSON API communication).
|
||||
//!
|
||||
//! What these tests protect:
|
||||
//! - Message round-trip serialization/deserialization
|
||||
//! - MessageRole enum serialization
|
||||
//! - Cache control serialization
|
||||
//! - Tool definitions serialization
|
||||
//!
|
||||
//! What these tests intentionally do NOT assert:
|
||||
//! - Provider-specific API formats (tested elsewhere)
|
||||
//! - Internal field ordering in JSON
|
||||
//! - Whitespace or formatting in serialized output
|
||||
|
||||
use g3_providers::{Message, MessageRole, CacheControl, Tool};
|
||||
use serde_json::json;
|
||||
|
||||
// =============================================================================
|
||||
// Helper functions for comparison (types don't implement PartialEq)
|
||||
// =============================================================================
|
||||
|
||||
fn role_matches(role: &MessageRole, expected: &str) -> bool {
|
||||
let json = serde_json::to_string(role).unwrap();
|
||||
json.to_lowercase().contains(&expected.to_lowercase())
|
||||
}
|
||||
|
||||
fn cache_control_is_ephemeral(cc: &Option<CacheControl>) -> bool {
|
||||
match cc {
|
||||
Some(c) => {
|
||||
let json = serde_json::to_string(c).unwrap();
|
||||
json.contains("ephemeral")
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_control_is_five_minute(cc: &Option<CacheControl>) -> bool {
|
||||
match cc {
|
||||
Some(c) => {
|
||||
let json = serde_json::to_string(c).unwrap();
|
||||
// Check for "5m" TTL
|
||||
json.contains("5m")
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_control_is_one_hour(cc: &Option<CacheControl>) -> bool {
|
||||
match cc {
|
||||
Some(c) => {
|
||||
let json = serde_json::to_string(c).unwrap();
|
||||
// Check for "1h" TTL
|
||||
json.contains("1h")
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: Message round-trip serialization
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_message_roundtrip_user() {
|
||||
let original = Message::new(MessageRole::User, "Hello, world!".to_string());
|
||||
|
||||
// Serialize to JSON
|
||||
let json = serde_json::to_string(&original).expect("Failed to serialize");
|
||||
|
||||
// Deserialize back
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert!(role_matches(&restored.role, "user"));
|
||||
assert_eq!(restored.content, "Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_roundtrip_assistant() {
|
||||
let original = Message::new(MessageRole::Assistant, "I can help with that.".to_string());
|
||||
|
||||
let json = serde_json::to_string(&original).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert!(role_matches(&restored.role, "assistant"));
|
||||
assert_eq!(restored.content, "I can help with that.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_roundtrip_system() {
|
||||
let original = Message::new(MessageRole::System, "You are a helpful assistant.".to_string());
|
||||
|
||||
let json = serde_json::to_string(&original).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert!(role_matches(&restored.role, "system"));
|
||||
assert_eq!(restored.content, "You are a helpful assistant.");
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: Message with special characters
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_message_with_unicode() {
|
||||
let content = "Hello 世界! 🌍 Привет мир!";
|
||||
let original = Message::new(MessageRole::User, content.to_string());
|
||||
|
||||
let json = serde_json::to_string(&original).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert_eq!(restored.content, content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_with_newlines() {
|
||||
let content = "Line 1\nLine 2\nLine 3";
|
||||
let original = Message::new(MessageRole::User, content.to_string());
|
||||
|
||||
let json = serde_json::to_string(&original).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert_eq!(restored.content, content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_with_quotes() {
|
||||
let content = r#"He said "Hello" and she replied 'Hi'"#;
|
||||
let original = Message::new(MessageRole::User, content.to_string());
|
||||
|
||||
let json = serde_json::to_string(&original).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert_eq!(restored.content, content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_with_json_content() {
|
||||
// Message containing JSON as content (common for tool calls)
|
||||
let content = r#"{"tool": "shell", "args": {"command": "ls -la"}}"#;
|
||||
let original = Message::new(MessageRole::Assistant, content.to_string());
|
||||
|
||||
let json = serde_json::to_string(&original).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert_eq!(restored.content, content);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: MessageRole serialization
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_message_role_user_serialization() {
|
||||
let role = MessageRole::User;
|
||||
let json = serde_json::to_string(&role).expect("Failed to serialize");
|
||||
|
||||
// Should serialize to lowercase "user"
|
||||
assert!(json.to_lowercase().contains("user"));
|
||||
|
||||
let restored: MessageRole = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
assert!(role_matches(&restored, "user"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_role_assistant_serialization() {
|
||||
let role = MessageRole::Assistant;
|
||||
let json = serde_json::to_string(&role).expect("Failed to serialize");
|
||||
|
||||
let restored: MessageRole = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
assert!(role_matches(&restored, "assistant"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_role_system_serialization() {
|
||||
let role = MessageRole::System;
|
||||
let json = serde_json::to_string(&role).expect("Failed to serialize");
|
||||
|
||||
let restored: MessageRole = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
assert!(role_matches(&restored, "system"));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: Message with cache control
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_message_with_ephemeral_cache() {
|
||||
let mut msg = Message::new(MessageRole::User, "Cached content".to_string());
|
||||
msg.cache_control = Some(CacheControl::ephemeral());
|
||||
|
||||
let json = serde_json::to_string(&msg).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert!(cache_control_is_ephemeral(&restored.cache_control));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_with_five_minute_cache() {
|
||||
let mut msg = Message::new(MessageRole::System, "System prompt".to_string());
|
||||
msg.cache_control = Some(CacheControl::five_minute());
|
||||
|
||||
let json = serde_json::to_string(&msg).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert!(cache_control_is_five_minute(&restored.cache_control));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_with_one_hour_cache() {
|
||||
let mut msg = Message::new(MessageRole::System, "Long-lived content".to_string());
|
||||
msg.cache_control = Some(CacheControl::one_hour());
|
||||
|
||||
let json = serde_json::to_string(&msg).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert!(cache_control_is_one_hour(&restored.cache_control));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_without_cache_control() {
|
||||
let msg = Message::new(MessageRole::User, "No cache".to_string());
|
||||
|
||||
let json = serde_json::to_string(&msg).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert!(restored.cache_control.is_none());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: Tool definition serialization
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_tool_roundtrip() {
|
||||
let tool = Tool {
|
||||
name: "shell".to_string(),
|
||||
description: "Execute shell commands".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The command to execute"
|
||||
}
|
||||
},
|
||||
"required": ["command"]
|
||||
}),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&tool).expect("Failed to serialize");
|
||||
let restored: Tool = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert_eq!(restored.name, "shell");
|
||||
assert_eq!(restored.description, "Execute shell commands");
|
||||
assert!(restored.input_schema.get("properties").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tool_with_complex_schema() {
|
||||
let tool = Tool {
|
||||
name: "code_search".to_string(),
|
||||
description: "Search code using tree-sitter".to_string(),
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"searches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"query": { "type": "string" },
|
||||
"language": { "type": "string" }
|
||||
},
|
||||
"required": ["name", "query", "language"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["searches"]
|
||||
}),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&tool).expect("Failed to serialize");
|
||||
let restored: Tool = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert_eq!(restored.name, "code_search");
|
||||
|
||||
// Verify nested schema structure is preserved
|
||||
let searches = restored.input_schema
|
||||
.get("properties")
|
||||
.and_then(|p| p.get("searches"))
|
||||
.and_then(|s| s.get("items"));
|
||||
assert!(searches.is_some());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: Empty and edge cases
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_message_with_empty_content() {
|
||||
let original = Message::new(MessageRole::User, "".to_string());
|
||||
|
||||
let json = serde_json::to_string(&original).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert_eq!(restored.content, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_with_very_long_content() {
|
||||
// Simulate a large file content or long conversation
|
||||
let content = "x".repeat(100_000);
|
||||
let original = Message::new(MessageRole::Assistant, content.clone());
|
||||
|
||||
let json = serde_json::to_string(&original).expect("Failed to serialize");
|
||||
let restored: Message = serde_json::from_str(&json).expect("Failed to deserialize");
|
||||
|
||||
assert_eq!(restored.content.len(), 100_000);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: Deserialization from external JSON format
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_from_api_format() {
|
||||
// Simulate JSON that might come from an API response
|
||||
let json = r#"{
|
||||
"role": "assistant",
|
||||
"content": "Here is my response."
|
||||
}"#;
|
||||
|
||||
let msg: Message = serde_json::from_str(json).expect("Failed to deserialize");
|
||||
|
||||
assert!(role_matches(&msg.role, "assistant"));
|
||||
assert_eq!(msg.content, "Here is my response.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_user_message_from_api() {
|
||||
let json = r#"{"role": "user", "content": "What is 2+2?"}"#;
|
||||
|
||||
let msg: Message = serde_json::from_str(json).expect("Failed to deserialize");
|
||||
|
||||
assert!(role_matches(&msg.role, "user"));
|
||||
assert_eq!(msg.content, "What is 2+2?");
|
||||
}
|
||||
Reference in New Issue
Block a user