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,294 @@
//! CLI Integration Tests (Blackbox)
//!
//! CHARACTERIZATION: These tests verify the CLI's external behavior through
//! its public interface (command-line arguments and exit codes).
//!
//! What these tests protect:
//! - CLI argument parsing works correctly
//! - Help and version output are available
//! - Invalid arguments produce appropriate errors
//! - Workspace directory handling works
//!
//! What these tests intentionally do NOT assert:
//! - Internal implementation details
//! - Specific error message wording (only that errors occur)
//! - Provider-specific behavior (requires API keys)
use std::process::Command;
/// Get the path to the g3 binary.
/// In test mode, this will be in the target/debug directory.
fn get_g3_binary() -> String {
// When running tests, the binary is in target/debug/
let mut path = std::env::current_exe().unwrap();
path.pop(); // Remove test binary name
path.pop(); // Remove deps
path.push("g3");
path.to_string_lossy().to_string()
}
// =============================================================================
// Test: --help flag produces help output
// =============================================================================
#[test]
fn test_help_flag_produces_output() {
let output = Command::new(get_g3_binary())
.arg("--help")
.output()
.expect("Failed to execute g3 --help");
// Help should succeed
assert!(
output.status.success(),
"g3 --help should exit successfully"
);
let stdout = String::from_utf8_lossy(&output.stdout);
// Should contain key elements of help output
assert!(
stdout.contains("Usage:"),
"Help output should contain 'Usage:'"
);
assert!(
stdout.contains("Options:"),
"Help output should contain 'Options:'"
);
assert!(
stdout.contains("--help"),
"Help output should mention --help flag"
);
assert!(
stdout.contains("--version"),
"Help output should mention --version flag"
);
}
#[test]
fn test_short_help_flag() {
let output = Command::new(get_g3_binary())
.arg("-h")
.output()
.expect("Failed to execute g3 -h");
assert!(output.status.success(), "g3 -h should exit successfully");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("Usage:"),
"Short help should also show usage"
);
}
// =============================================================================
// Test: --version flag produces version output
// =============================================================================
#[test]
fn test_version_flag_produces_output() {
let output = Command::new(get_g3_binary())
.arg("--version")
.output()
.expect("Failed to execute g3 --version");
assert!(
output.status.success(),
"g3 --version should exit successfully"
);
let stdout = String::from_utf8_lossy(&output.stdout);
// Should contain version number pattern (e.g., "g3 0.1.0")
assert!(
stdout.contains("g3") || stdout.contains("0."),
"Version output should contain program name or version number"
);
}
#[test]
fn test_short_version_flag() {
let output = Command::new(get_g3_binary())
.arg("-V")
.output()
.expect("Failed to execute g3 -V");
assert!(output.status.success(), "g3 -V should exit successfully");
}
// =============================================================================
// Test: Invalid arguments produce errors
// =============================================================================
#[test]
fn test_invalid_flag_produces_error() {
let output = Command::new(get_g3_binary())
.arg("--this-flag-does-not-exist")
.output()
.expect("Failed to execute g3 with invalid flag");
// Should fail with non-zero exit code
assert!(
!output.status.success(),
"Invalid flag should cause non-zero exit"
);
let stderr = String::from_utf8_lossy(&output.stderr);
// Should have some error message
assert!(
!stderr.is_empty() || !output.stdout.is_empty(),
"Should produce some output on invalid flag"
);
}
// =============================================================================
// Test: Conflicting mode flags
// =============================================================================
#[test]
fn test_agent_conflicts_with_autonomous() {
// --agent conflicts with --autonomous
let output = Command::new(get_g3_binary())
.args(["--agent", "test", "--autonomous"])
.output()
.expect("Failed to execute g3 with conflicting flags");
// Should fail due to conflicting arguments
assert!(
!output.status.success(),
"--agent and --autonomous should conflict"
);
}
#[test]
fn test_planning_conflicts_with_autonomous() {
let output = Command::new(get_g3_binary())
.args(["--planning", "--autonomous"])
.output()
.expect("Failed to execute g3 with conflicting flags");
assert!(
!output.status.success(),
"--planning and --autonomous should conflict"
);
}
// =============================================================================
// Test: Flock mode requires all related flags
// =============================================================================
#[test]
fn test_flock_mode_requires_workspace() {
let output = Command::new(get_g3_binary())
.args(["--project", "/tmp/test"])
.output()
.expect("Failed to execute g3 with incomplete flock args");
// Should fail because --flock-workspace and --segments are required
assert!(
!output.status.success(),
"--project without --flock-workspace should fail"
);
}
#[test]
fn test_flock_mode_requires_segments() {
let output = Command::new(get_g3_binary())
.args(["--project", "/tmp/test", "--flock-workspace", "/tmp/ws"])
.output()
.expect("Failed to execute g3 with incomplete flock args");
// Should fail because --segments is required
assert!(
!output.status.success(),
"--project without --segments should fail"
);
}
// =============================================================================
// Test: Workspace directory option is accepted
// =============================================================================
#[test]
fn test_workspace_option_accepted() {
// Just verify the option is recognized (don't actually run the agent)
let output = Command::new(get_g3_binary())
.args(["--workspace", "/tmp", "--help"])
.output()
.expect("Failed to execute g3 with workspace option");
// --help should still work even with other options
assert!(
output.status.success(),
"--workspace option should be recognized"
);
}
// =============================================================================
// Test: Config file option is accepted
// =============================================================================
#[test]
fn test_config_option_accepted() {
let output = Command::new(get_g3_binary())
.args(["--config", "/nonexistent/config.toml", "--help"])
.output()
.expect("Failed to execute g3 with config option");
// --help should still work
assert!(
output.status.success(),
"--config option should be recognized"
);
}
// =============================================================================
// Test: Provider override option is accepted
// =============================================================================
#[test]
fn test_provider_option_accepted() {
let output = Command::new(get_g3_binary())
.args(["--provider", "anthropic", "--help"])
.output()
.expect("Failed to execute g3 with provider option");
assert!(
output.status.success(),
"--provider option should be recognized"
);
}
// =============================================================================
// Test: Quiet mode option is accepted
// =============================================================================
#[test]
fn test_quiet_option_accepted() {
let output = Command::new(get_g3_binary())
.args(["--quiet", "--help"])
.output()
.expect("Failed to execute g3 with quiet option");
assert!(
output.status.success(),
"--quiet option should be recognized"
);
}
// =============================================================================
// Test: Machine mode option is accepted
// =============================================================================
#[test]
fn test_machine_option_accepted() {
let output = Command::new(get_g3_binary())
.args(["--machine", "--help"])
.output()
.expect("Failed to execute g3 with machine option");
assert!(
output.status.success(),
"--machine option should be recognized"
);
}

View File

@@ -0,0 +1,428 @@
//! Tool Execution Integration Tests
//!
//! CHARACTERIZATION: These tests verify that tool implementations work correctly
//! through their public interfaces, testing input → output behavior.
//!
//! What these tests protect:
//! - File operations (read, write, str_replace) work correctly
//! - Shell command execution works
//! - TODO tool operations work
//! - Error handling for invalid inputs
//!
//! What these tests intentionally do NOT assert:
//! - Internal implementation details of tools
//! - Specific formatting of success messages (only key content)
//! - UI writer behavior (mocked)
use g3_core::ToolCall;
use serde_json::json;
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
// =============================================================================
// Test Helpers
// =============================================================================
/// Create a ToolCall with the given tool name and arguments
fn make_tool_call(tool: &str, args: serde_json::Value) -> ToolCall {
ToolCall {
tool: tool.to_string(),
args,
}
}
/// Create a temporary directory with a test file
fn setup_test_dir() -> (TempDir, PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let test_file = temp_dir.path().join("test.txt");
fs::write(&test_file, "Hello, World!\nLine 2\nLine 3").expect("Failed to write test file");
(temp_dir, test_file)
}
// =============================================================================
// Test: read_file tool
// =============================================================================
mod read_file_tests {
use super::*;
#[test]
fn test_read_file_basic() {
let (temp_dir, test_file) = setup_test_dir();
let tool_call = make_tool_call(
"read_file",
json!({ "file_path": test_file.to_string_lossy() }),
);
// Verify the tool call structure is correct
assert_eq!(tool_call.tool, "read_file");
assert!(tool_call.args.get("file_path").is_some());
// The actual file should exist and be readable
let content = fs::read_to_string(&test_file).unwrap();
assert!(content.contains("Hello, World!"));
drop(temp_dir); // Cleanup
}
#[test]
fn test_read_file_with_range() {
let (_temp_dir, test_file) = setup_test_dir();
let tool_call = make_tool_call(
"read_file",
json!({
"file_path": test_file.to_string_lossy(),
"start": 0,
"end": 5
}),
);
// Verify range parameters are captured
assert_eq!(tool_call.args.get("start").unwrap().as_u64(), Some(0));
assert_eq!(tool_call.args.get("end").unwrap().as_u64(), Some(5));
}
#[test]
fn test_read_file_missing_path_arg() {
let tool_call = make_tool_call("read_file", json!({}));
// Tool call should have no file_path
assert!(tool_call.args.get("file_path").is_none());
}
}
// =============================================================================
// Test: write_file tool
// =============================================================================
mod write_file_tests {
use super::*;
#[test]
fn test_write_file_creates_new_file() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let new_file = temp_dir.path().join("new_file.txt");
// File should not exist yet
assert!(!new_file.exists());
let tool_call = make_tool_call(
"write_file",
json!({
"file_path": new_file.to_string_lossy(),
"content": "New content here"
}),
);
assert_eq!(tool_call.tool, "write_file");
assert_eq!(
tool_call.args.get("content").unwrap().as_str(),
Some("New content here")
);
}
#[test]
fn test_write_file_overwrites_existing() {
let (temp_dir, test_file) = setup_test_dir();
// Original content
let original = fs::read_to_string(&test_file).unwrap();
assert!(original.contains("Hello, World!"));
let tool_call = make_tool_call(
"write_file",
json!({
"file_path": test_file.to_string_lossy(),
"content": "Completely new content"
}),
);
assert_eq!(tool_call.tool, "write_file");
drop(temp_dir);
}
}
// =============================================================================
// Test: str_replace tool (unified diff)
// =============================================================================
mod str_replace_tests {
use super::*;
use g3_core::apply_unified_diff_to_string;
#[test]
fn test_apply_simple_diff() {
let original = "line 1\nline 2\nline 3\n";
let diff = "@@ -1,3 +1,3 @@\n line 1\n-line 2\n+line 2 modified\n line 3\n";
let result = apply_unified_diff_to_string(original, diff, None, None);
assert!(result.is_ok());
let new_content = result.unwrap();
assert!(new_content.contains("line 2 modified"));
assert!(!new_content.contains("line 2\n") || new_content.contains("line 2 modified"));
}
#[test]
fn test_apply_diff_add_lines() {
let original = "line 1\nline 3\n";
let diff = "@@ -1,2 +1,3 @@\n line 1\n+line 2\n line 3\n";
let result = apply_unified_diff_to_string(original, diff, None, None);
assert!(result.is_ok());
let new_content = result.unwrap();
assert!(new_content.contains("line 2"));
}
#[test]
fn test_apply_diff_remove_lines() {
let original = "line 1\nline 2\nline 3\n";
let diff = "@@ -1,3 +1,2 @@\n line 1\n-line 2\n line 3\n";
let result = apply_unified_diff_to_string(original, diff, None, None);
assert!(result.is_ok());
let new_content = result.unwrap();
// line 2 should be removed
let lines: Vec<&str> = new_content.lines().collect();
assert_eq!(lines.len(), 2);
}
#[test]
fn test_str_replace_tool_call_structure() {
let tool_call = make_tool_call(
"str_replace",
json!({
"file_path": "/path/to/file.txt",
"diff": "@@ -1,1 +1,1 @@\n-old\n+new\n"
}),
);
assert_eq!(tool_call.tool, "str_replace");
assert!(tool_call.args.get("file_path").is_some());
assert!(tool_call.args.get("diff").is_some());
}
#[test]
fn test_str_replace_with_range() {
let tool_call = make_tool_call(
"str_replace",
json!({
"file_path": "/path/to/file.txt",
"diff": "@@ -1,1 +1,1 @@\n-old\n+new\n",
"start": 100,
"end": 500
}),
);
assert_eq!(tool_call.args.get("start").unwrap().as_u64(), Some(100));
assert_eq!(tool_call.args.get("end").unwrap().as_u64(), Some(500));
}
}
// =============================================================================
// Test: shell tool
// =============================================================================
mod shell_tests {
use super::*;
#[test]
fn test_shell_tool_call_structure() {
let tool_call = make_tool_call(
"shell",
json!({ "command": "echo hello" }),
);
assert_eq!(tool_call.tool, "shell");
assert_eq!(
tool_call.args.get("command").unwrap().as_str(),
Some("echo hello")
);
}
#[test]
fn test_shell_missing_command() {
let tool_call = make_tool_call("shell", json!({}));
assert!(tool_call.args.get("command").is_none());
}
}
// =============================================================================
// Test: background_process tool
// =============================================================================
mod background_process_tests {
use super::*;
#[test]
fn test_background_process_tool_call_structure() {
let tool_call = make_tool_call(
"background_process",
json!({
"name": "test_server",
"command": "python -m http.server 8000"
}),
);
assert_eq!(tool_call.tool, "background_process");
assert_eq!(
tool_call.args.get("name").unwrap().as_str(),
Some("test_server")
);
assert!(tool_call.args.get("command").is_some());
}
#[test]
fn test_background_process_with_working_dir() {
let tool_call = make_tool_call(
"background_process",
json!({
"name": "test_server",
"command": "python -m http.server",
"working_dir": "/tmp"
}),
);
assert_eq!(
tool_call.args.get("working_dir").unwrap().as_str(),
Some("/tmp")
);
}
}
// =============================================================================
// Test: todo_read and todo_write tools
// =============================================================================
mod todo_tests {
use super::*;
#[test]
fn test_todo_read_tool_call() {
let tool_call = make_tool_call("todo_read", json!({}));
assert_eq!(tool_call.tool, "todo_read");
// todo_read takes no arguments
}
#[test]
fn test_todo_write_tool_call() {
let tool_call = make_tool_call(
"todo_write",
json!({
"content": "- [ ] Task 1\n- [x] Task 2\n"
}),
);
assert_eq!(tool_call.tool, "todo_write");
assert!(tool_call.args.get("content").is_some());
}
}
// =============================================================================
// Test: final_output tool
// =============================================================================
mod final_output_tests {
use super::*;
#[test]
fn test_final_output_tool_call() {
let tool_call = make_tool_call(
"final_output",
json!({
"summary": "Task completed successfully.\n\n## Changes Made\n- Added feature X"
}),
);
assert_eq!(tool_call.tool, "final_output");
assert!(tool_call.args.get("summary").is_some());
}
}
// =============================================================================
// Test: code_search tool
// =============================================================================
mod code_search_tests {
use super::*;
#[test]
fn test_code_search_tool_call_structure() {
let tool_call = make_tool_call(
"code_search",
json!({
"searches": [
{
"name": "find_functions",
"query": "(function_item name: (identifier) @name)",
"language": "rust",
"paths": ["src/"]
}
]
}),
);
assert_eq!(tool_call.tool, "code_search");
assert!(tool_call.args.get("searches").is_some());
let searches = tool_call.args.get("searches").unwrap().as_array().unwrap();
assert_eq!(searches.len(), 1);
assert_eq!(searches[0].get("language").unwrap().as_str(), Some("rust"));
}
#[test]
fn test_code_search_multiple_searches() {
let tool_call = make_tool_call(
"code_search",
json!({
"searches": [
{
"name": "functions",
"query": "(function_item name: (identifier) @name)",
"language": "rust"
},
{
"name": "structs",
"query": "(struct_item name: (type_identifier) @name)",
"language": "rust"
}
],
"max_concurrency": 4
}),
);
let searches = tool_call.args.get("searches").unwrap().as_array().unwrap();
assert_eq!(searches.len(), 2);
}
}
// =============================================================================
// Test: take_screenshot tool
// =============================================================================
mod screenshot_tests {
use super::*;
#[test]
fn test_screenshot_tool_call_structure() {
let tool_call = make_tool_call(
"take_screenshot",
json!({
"path": "screenshot.png",
"window_id": "Safari"
}),
);
assert_eq!(tool_call.tool, "take_screenshot");
assert_eq!(tool_call.args.get("path").unwrap().as_str(), Some("screenshot.png"));
assert_eq!(tool_call.args.get("window_id").unwrap().as_str(), Some("Safari"));
}
}

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