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:
Dhanji R. Prasanna
2026-01-07 09:23:34 +11:00
parent 9cb6282719
commit 5d20da2609
3 changed files with 1073 additions and 0 deletions

View 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?");
}