Compare commits
2 Commits
fix/format
...
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()
|
_resolved = Path(path).expanduser().resolve()
|
||||||
|
|
||||||
# ── Binary file guard ─────────────────────────────────────────
|
# ── 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)):
|
if has_binary_extension(str(_resolved)):
|
||||||
_ext = _resolved.suffix.lower()
|
_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({
|
return json.dumps({
|
||||||
"error": (
|
"error": (
|
||||||
f"Cannot read binary file '{path}' ({_ext}). "
|
f"Cannot read binary file '{path}' ({_ext}). "
|
||||||
|
|||||||
Reference in New Issue
Block a user