use g3_providers::ImageContent; use std::fs; #[test] fn test_image_content_media_type_detection() { assert_eq!(ImageContent::media_type_from_extension("png"), Some("image/png")); assert_eq!(ImageContent::media_type_from_extension("PNG"), Some("image/png")); assert_eq!(ImageContent::media_type_from_extension("jpg"), Some("image/jpeg")); assert_eq!(ImageContent::media_type_from_extension("jpeg"), Some("image/jpeg")); assert_eq!(ImageContent::media_type_from_extension("JPEG"), Some("image/jpeg")); assert_eq!(ImageContent::media_type_from_extension("gif"), Some("image/gif")); assert_eq!(ImageContent::media_type_from_extension("webp"), Some("image/webp")); assert_eq!(ImageContent::media_type_from_extension("bmp"), None); // Not supported assert_eq!(ImageContent::media_type_from_extension("txt"), None); } #[test] fn test_image_content_creation() { let image = ImageContent::new("image/png", "base64data".to_string()); assert_eq!(image.media_type, "image/png"); assert_eq!(image.data, "base64data"); } #[test] fn test_read_and_encode_image() { // Create a minimal valid PNG let test_dir = std::env::temp_dir().join("g3_read_image_test"); let _ = fs::remove_dir_all(&test_dir); fs::create_dir_all(&test_dir).unwrap(); // Minimal 1x1 red PNG (hand-crafted) let png_bytes: Vec = vec![ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature 0x00, 0x00, 0x00, 0x0D, // IHDR length 0x49, 0x48, 0x44, 0x52, // IHDR 0x00, 0x00, 0x00, 0x01, // width = 1 0x00, 0x00, 0x00, 0x01, // height = 1 0x08, 0x02, 0x00, 0x00, 0x00, // bit depth, color type, etc. 0x90, 0x77, 0x53, 0xDE, // CRC 0x00, 0x00, 0x00, 0x0C, // IDAT length 0x49, 0x44, 0x41, 0x54, // IDAT 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, // compressed data 0x01, 0x01, 0x01, 0x00, // CRC (approximate) 0x00, 0x00, 0x00, 0x00, // IEND length 0x49, 0x45, 0x4E, 0x44, // IEND 0xAE, 0x42, 0x60, 0x82, // CRC ]; let image_path = test_dir.join("test.png"); fs::write(&image_path, &png_bytes).unwrap(); // Read and encode let bytes = fs::read(&image_path).unwrap(); use base64::Engine; let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes); // Verify it's valid base64 assert!(!encoded.is_empty()); assert!(encoded.len() > 10); // Verify we can decode it back let decoded = base64::engine::general_purpose::STANDARD.decode(&encoded).unwrap(); assert_eq!(decoded, bytes); // Create ImageContent let ext = image_path.extension().unwrap().to_str().unwrap(); let media_type = ImageContent::media_type_from_extension(ext).unwrap(); let image = ImageContent::new(media_type, encoded); assert_eq!(image.media_type, "image/png"); assert!(!image.data.is_empty()); // Cleanup let _ = fs::remove_dir_all(&test_dir); } #[test] fn test_media_type_from_bytes_png() { // PNG magic bytes let png_bytes: Vec = vec![ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature 0x00, 0x00, 0x00, 0x0D, // IHDR length 0x49, 0x48, 0x44, 0x52, // IHDR ]; assert_eq!(ImageContent::media_type_from_bytes(&png_bytes), Some("image/png")); } #[test] fn test_media_type_from_bytes_jpeg() { // JPEG magic bytes (FF D8 FF) let jpeg_bytes: Vec = vec![ 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, ]; assert_eq!(ImageContent::media_type_from_bytes(&jpeg_bytes), Some("image/jpeg")); } #[test] fn test_media_type_from_bytes_gif() { // GIF magic bytes (GIF89a) let gif_bytes: Vec = vec![ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; assert_eq!(ImageContent::media_type_from_bytes(&gif_bytes), Some("image/gif")); // GIF87a variant let gif87_bytes: Vec = vec![ 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; assert_eq!(ImageContent::media_type_from_bytes(&gif87_bytes), Some("image/gif")); } #[test] fn test_media_type_from_bytes_webp() { // WebP magic bytes (RIFF....WEBP) let webp_bytes: Vec = vec![ 0x52, 0x49, 0x46, 0x46, // RIFF 0x00, 0x00, 0x00, 0x00, // file size (placeholder) 0x57, 0x45, 0x42, 0x50, // WEBP 0x56, 0x50, 0x38, 0x20, // VP8 (additional data) ]; assert_eq!(ImageContent::media_type_from_bytes(&webp_bytes), Some("image/webp")); } #[test] fn test_media_type_from_bytes_unknown() { // Random bytes that don't match any format let unknown_bytes: Vec = vec![ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, ]; assert_eq!(ImageContent::media_type_from_bytes(&unknown_bytes), None); } #[test] fn test_media_type_from_bytes_too_short() { // Too short to detect let short_bytes: Vec = vec![0x89, 0x50, 0x4E]; assert_eq!(ImageContent::media_type_from_bytes(&short_bytes), None); // Empty let empty_bytes: Vec = vec![]; assert_eq!(ImageContent::media_type_from_bytes(&empty_bytes), None); } #[test] fn test_read_image_multiple_paths_schema() { // This test verifies the tool accepts file_paths array // Single path in array let single_args = serde_json::json!({ "file_paths": ["/path/to/image.png"] }); let paths = single_args.get("file_paths").unwrap().as_array().unwrap(); assert_eq!(paths.len(), 1); // Multiple paths in array let multi_args = serde_json::json!({ "file_paths": ["/path/to/image1.png", "/path/to/image2.jpg"] }); let paths = multi_args.get("file_paths").unwrap().as_array().unwrap(); assert_eq!(paths.len(), 2); } #[test] fn test_image_dimensions_png() { // Minimal PNG with known dimensions (1x1) let png_bytes: Vec = vec![ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature 0x00, 0x00, 0x00, 0x0D, // IHDR length 0x49, 0x48, 0x44, 0x52, // IHDR 0x00, 0x00, 0x00, 0x01, // width = 1 0x00, 0x00, 0x00, 0x01, // height = 1 0x08, 0x02, 0x00, 0x00, 0x00, // bit depth, color type, etc. ]; // PNG dimensions are at bytes 16-19 (width) and 20-23 (height) if png_bytes.len() >= 24 { let width = u32::from_be_bytes([png_bytes[16], png_bytes[17], png_bytes[18], png_bytes[19]]); let height = u32::from_be_bytes([png_bytes[20], png_bytes[21], png_bytes[22], png_bytes[23]]); assert_eq!(width, 1); assert_eq!(height, 1); } } #[test] fn test_image_dimensions_gif() { // GIF with known dimensions let gif_bytes: Vec = vec![ 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, // GIF89a 0x64, 0x00, // width = 100 (little-endian) 0xC8, 0x00, // height = 200 (little-endian) ]; let width = u16::from_le_bytes([gif_bytes[6], gif_bytes[7]]) as u32; let height = u16::from_le_bytes([gif_bytes[8], gif_bytes[9]]) as u32; assert_eq!(width, 100); assert_eq!(height, 200); } #[test] fn test_resize_image_if_needed_small_image() { use g3_core::tools::file_ops::resize_image_if_needed; use std::path::Path; // Small image should not be resized let small_bytes = vec![0u8; 1000]; // 1KB let path = Path::new("test.jpg"); let target_size = 5 * 1024 * 1024; // 5MB let result = resize_image_if_needed(&small_bytes, path, target_size).unwrap(); assert_eq!(result.len(), small_bytes.len(), "Small image should not be resized"); } #[test] fn test_resize_image_if_needed_returns_original_on_failure() { use g3_core::tools::file_ops::resize_image_if_needed; use std::path::Path; // Invalid image data - ImageMagick will fail, should return original let invalid_bytes = vec![0u8; 6 * 1024 * 1024]; // 6MB of zeros let path = Path::new("test.jpg"); let target_size = 5 * 1024 * 1024; // 5MB let result = resize_image_if_needed(&invalid_bytes, path, target_size).unwrap(); // Should return original since ImageMagick can't process invalid data assert_eq!(result.len(), invalid_bytes.len(), "Invalid image should return original"); } /// CHARACTERIZATION: Test that resize_image_to_dimensions returns original when resize /// produces a larger file. /// /// This documents the fix from commit af8b849: when resize doesn't reduce size, /// the original bytes should be returned so the original media type is preserved. /// Previously, was_resized was incorrectly set to true even when falling back to /// original bytes, causing media type mismatch errors with the Anthropic API. /// /// What this test protects: /// - resize_image_to_dimensions returns original bytes when resize doesn't help /// /// What this test intentionally does NOT assert: /// - The actual media type selection logic (that's in execute_read_image) /// - API-level behavior (would require integration test with real images) #[test] fn test_resize_returns_original_when_resize_increases_size() { use g3_core::tools::file_ops::resize_image_to_dimensions; use std::path::Path; // A very small PNG that can't be compressed further - resize would increase size // Using invalid data that ImageMagick can't process, which triggers the fallback path let small_png_bytes = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; // PNG header only let path = Path::new("test.png"); // Try to resize - should return original since it can't process incomplete PNG let result = resize_image_to_dimensions(&small_png_bytes, path, 1568, 1024 * 1024); assert!(result.is_ok(), "Should not error, should return original"); assert_eq!(result.unwrap().len(), small_png_bytes.len(), "Should return original bytes when resize fails"); }