Compare commits
2 Commits
feat/802-a
...
burn/800-g
| Author | SHA1 | Date | |
|---|---|---|---|
| 7875bc26fb | |||
| 91a6a52fc9 |
213
tests/test_file_tools_image_analysis.py
Normal file
213
tests/test_file_tools_image_analysis.py
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for image analysis integration in file_tools.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Add tools to path
|
||||
import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "tools"))
|
||||
|
||||
from file_tools import read_file_tool, IMAGE_EXTENSIONS
|
||||
|
||||
|
||||
class TestImageExtensions:
|
||||
"""Test image extension detection."""
|
||||
|
||||
def test_common_extensions(self):
|
||||
"""Test that common image extensions are included."""
|
||||
common = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"}
|
||||
for ext in common:
|
||||
assert ext in IMAGE_EXTENSIONS
|
||||
|
||||
def test_tiff_extensions(self):
|
||||
"""Test TIFF extensions are included."""
|
||||
assert ".tiff" in IMAGE_EXTENSIONS
|
||||
assert ".tif" in IMAGE_EXTENSIONS
|
||||
|
||||
|
||||
class TestReadFileImageHandling:
|
||||
"""Test image file handling in read_file_tool."""
|
||||
|
||||
@patch('file_tools.vision_analyze_tool', new_callable=AsyncMock)
|
||||
def test_png_file_analyzed(self, mock_vision, tmp_path):
|
||||
"""Test that PNG files are analyzed with vision."""
|
||||
# Create a dummy PNG file
|
||||
png_file = tmp_path / "test.png"
|
||||
png_file.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
|
||||
|
||||
# Mock vision analysis
|
||||
mock_vision.return_value = "A test image showing a red circle on white background."
|
||||
|
||||
# Read the file
|
||||
result = read_file_tool(str(png_file))
|
||||
data = json.loads(result)
|
||||
|
||||
# Check that vision was called
|
||||
assert "type" in data
|
||||
assert data["type"] == "image_analysis"
|
||||
assert "analysis" in data
|
||||
assert "red circle" in data["analysis"]
|
||||
|
||||
@patch('file_tools.vision_analyze_tool', new_callable=AsyncMock)
|
||||
def test_jpg_file_analyzed(self, mock_vision, tmp_path):
|
||||
"""Test that JPG files are analyzed with vision."""
|
||||
jpg_file = tmp_path / "photo.jpg"
|
||||
jpg_file.write_bytes(b"\xff\xd8\xff" + b"\x00" * 100)
|
||||
|
||||
mock_vision.return_value = "A photograph of a sunset."
|
||||
|
||||
result = read_file_tool(str(jpg_file))
|
||||
data = json.loads(result)
|
||||
|
||||
assert data["type"] == "image_analysis"
|
||||
assert "sunset" in data["analysis"]
|
||||
|
||||
@patch('file_tools.vision_analyze_tool', new_callable=AsyncMock)
|
||||
def test_webp_file_analyzed(self, mock_vision, tmp_path):
|
||||
"""Test that WebP files are analyzed with vision."""
|
||||
webp_file = tmp_path / "image.webp"
|
||||
webp_file.write_bytes(b"RIFF" + b"\x00" * 100)
|
||||
|
||||
mock_vision.return_value = "A WebP image of a cat."
|
||||
|
||||
result = read_file_tool(str(webp_file))
|
||||
data = json.loads(result)
|
||||
|
||||
assert data["type"] == "image_analysis"
|
||||
assert "cat" in data["analysis"]
|
||||
|
||||
def test_non_image_binary_still_blocked(self, tmp_path):
|
||||
"""Test that non-image binary files are still blocked."""
|
||||
zip_file = tmp_path / "archive.zip"
|
||||
zip_file.write_bytes(b"PK" + b"\x00" * 100)
|
||||
|
||||
result = read_file_tool(str(zip_file))
|
||||
data = json.loads(result)
|
||||
|
||||
assert "error" in data
|
||||
assert "Cannot read binary file" in data["error"]
|
||||
|
||||
def test_text_file_unchanged(self, tmp_path):
|
||||
"""Test that text files are still read normally."""
|
||||
txt_file = tmp_path / "readme.txt"
|
||||
txt_file.write_text("Hello, world!\nThis is a test file.\n")
|
||||
|
||||
result = read_file_tool(str(txt_file))
|
||||
data = json.loads(result)
|
||||
|
||||
# Should have content, not image analysis
|
||||
assert "content" in data or "lines" in data
|
||||
assert "type" not in data or data.get("type") != "image_analysis"
|
||||
|
||||
@patch('file_tools.vision_analyze_tool', new_callable=AsyncMock)
|
||||
def test_image_analysis_error_handling(self, mock_vision, tmp_path):
|
||||
"""Test error handling when vision analysis fails."""
|
||||
png_file = tmp_path / "broken.png"
|
||||
png_file.write_bytes(b"\x89PNG" + b"\x00" * 100)
|
||||
|
||||
# Mock vision to raise an error
|
||||
mock_vision.side_effect = Exception("Vision API error")
|
||||
|
||||
result = read_file_tool(str(png_file))
|
||||
data = json.loads(result)
|
||||
|
||||
assert "error" in data
|
||||
assert "Image analysis failed" in data["error"]
|
||||
|
||||
@patch('file_tools.vision_analyze_tool', new_callable=AsyncMock)
|
||||
def test_image_result_structure(self, mock_vision, tmp_path):
|
||||
"""Test that image analysis result has correct structure."""
|
||||
png_file = tmp_path / "chart.png"
|
||||
png_file.write_bytes(b"\x89PNG" + b"\x00" * 100)
|
||||
|
||||
mock_vision.return_value = "A bar chart showing sales data."
|
||||
|
||||
result = read_file_tool(str(png_file))
|
||||
data = json.loads(result)
|
||||
|
||||
# Check required fields
|
||||
assert "type" in data
|
||||
assert data["type"] == "image_analysis"
|
||||
assert "file_path" in data
|
||||
assert "extension" in data
|
||||
assert data["extension"] == ".png"
|
||||
assert "analysis" in data
|
||||
assert "note" in data
|
||||
|
||||
|
||||
class TestImageAnalysisIntegration:
|
||||
"""Integration tests for image analysis."""
|
||||
|
||||
@patch('file_tools.vision_analyze_tool', new_callable=AsyncMock)
|
||||
def test_multiple_image_types(self, mock_vision, tmp_path):
|
||||
"""Test analyzing multiple image types."""
|
||||
mock_vision.return_value = "Test image analysis result."
|
||||
|
||||
# Test PNG
|
||||
png = tmp_path / "test.png"
|
||||
png.write_bytes(b"\x89PNG" + b"\x00" * 100)
|
||||
result = read_file_tool(str(png))
|
||||
data = json.loads(result)
|
||||
assert data["type"] == "image_analysis"
|
||||
|
||||
# Test JPG
|
||||
jpg = tmp_path / "test.jpg"
|
||||
jpg.write_bytes(b"\xff\xd8\xff" + b"\x00" * 100)
|
||||
result = read_file_tool(str(jpg))
|
||||
data = json.loads(result)
|
||||
assert data["type"] == "image_analysis"
|
||||
|
||||
# Test GIF
|
||||
gif = tmp_path / "test.gif"
|
||||
gif.write_bytes(b"GIF89a" + b"\x00" * 100)
|
||||
result = read_file_tool(str(gif))
|
||||
data = json.loads(result)
|
||||
assert data["type"] == "image_analysis"
|
||||
|
||||
def test_nonexistent_image_file(self):
|
||||
"""Test handling of nonexistent image files."""
|
||||
result = read_file_tool("/nonexistent/image.png")
|
||||
data = json.loads(result)
|
||||
|
||||
# Should get an error (file doesn't exist)
|
||||
assert "error" in data
|
||||
|
||||
@patch('file_tools.vision_analyze_tool', new_callable=AsyncMock)
|
||||
def test_image_with_spaces_in_name(self, mock_vision, tmp_path):
|
||||
"""Test image files with spaces in the name."""
|
||||
mock_vision.return_value = "A test image."
|
||||
|
||||
png = tmp_path / "my image file.png"
|
||||
png.write_bytes(b"\x89PNG" + b"\x00" * 100)
|
||||
|
||||
result = read_file_tool(str(png))
|
||||
data = json.loads(result)
|
||||
|
||||
assert data["type"] == "image_analysis"
|
||||
assert "my image file.png" in data["file_path"]
|
||||
|
||||
|
||||
class TestVisionToolImport:
|
||||
"""Test that vision tool is properly imported."""
|
||||
|
||||
def test_vision_tool_import(self):
|
||||
"""Test that vision_analyze_tool can be imported."""
|
||||
try:
|
||||
from tools.vision_tools import vision_analyze_tool
|
||||
assert callable(vision_analyze_tool)
|
||||
except ImportError:
|
||||
pytest.skip("vision_tools not available")
|
||||
|
||||
def test_image_extensions_constant(self):
|
||||
"""Test IMAGE_EXTENSIONS constant is accessible."""
|
||||
from file_tools import IMAGE_EXTENSIONS
|
||||
assert isinstance(IMAGE_EXTENSIONS, set)
|
||||
assert len(IMAGE_EXTENSIONS) > 0
|
||||
@@ -296,9 +296,43 @@ def read_file_tool(path: str, offset: int = 1, limit: int = 500, task_id: str =
|
||||
_resolved = Path(path).expanduser().resolve()
|
||||
|
||||
# ── Binary file guard ─────────────────────────────────────────
|
||||
# Block binary files by extension (no I/O).
|
||||
# Block binary files by extension, but handle images with vision.
|
||||
if has_binary_extension(str(_resolved)):
|
||||
_ext = _resolved.suffix.lower()
|
||||
|
||||
# Route image files through vision analysis
|
||||
if _ext in IMAGE_EXTENSIONS:
|
||||
try:
|
||||
import asyncio
|
||||
from tools.vision_tools import vision_analyze_tool
|
||||
|
||||
# Analyze the image
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
result = loop.run_until_complete(
|
||||
vision_analyze_tool(
|
||||
image_url=str(_resolved),
|
||||
user_prompt="Describe this image in detail. Extract any text, describe the content, and note any important visual elements."
|
||||
)
|
||||
)
|
||||
return json.dumps({
|
||||
"type": "image_analysis",
|
||||
"file_path": str(_resolved),
|
||||
"extension": _ext,
|
||||
"analysis": result,
|
||||
"note": "This file was analyzed using vision AI. Use vision_analyze with a specific prompt for more targeted analysis."
|
||||
})
|
||||
finally:
|
||||
loop.close()
|
||||
except Exception as e:
|
||||
return json.dumps({
|
||||
"error": f"Image analysis failed for '{path}': {str(e)}",
|
||||
"file_path": str(_resolved),
|
||||
"extension": _ext,
|
||||
})
|
||||
|
||||
# Non-image binary files
|
||||
return json.dumps({
|
||||
"error": (
|
||||
f"Cannot read binary file '{path}' ({_ext}). "
|
||||
|
||||
Reference in New Issue
Block a user