""" Test suite for AI vision extraction with image_processing field parsing. Tests the image_processing field returned by enhanced AI prompt. """ import pytest from unittest.mock import patch, MagicMock from backend.ai_vision import extract_label_info # Minimal valid 1x1 PNG bytes MINIMAL_PNG = ( b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01' b'\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00' b'\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05\x18' b'\xd8N\x00\x00\x00\x00IEND\xaeB`\x82' ) class TestImageProcessingParsing: """Test parsing of image_processing field from AI extraction.""" def test_extract_label_info_returns_image_processing(self): """Test that extract_label_info returns image_processing field when present.""" ai_response = { "items": [ { "Item": "1.6TB NVMe HPE U.3 P66093-002", "Type": "NVMe", "Description": "High-speed storage", "Category": "Storage", "Connector": "U.3", "Size": "1.6TB", "Color": "Black", "PartNr": "P66093-002", "OCR": "NVME 1.6TB HPE U3 P66093002", "image_processing": { "crop_bounds": {"x": 50, "y": 100, "width": 300, "height": 200}, "rotation_degrees": 15, "confidence": 0.92 } } ] } with patch("backend.ai_vision.gemini.extract") as mock_extract: mock_extract.return_value = ai_response result = extract_label_info(MINIMAL_PNG, mode="item") # Verify image_processing is in result assert "items" in result assert len(result["items"]) > 0 item = result["items"][0] assert "image_processing" in item assert item["image_processing"] is not None def test_image_processing_crop_bounds_structure(self): """Test that crop_bounds has correct structure: {x, y, width, height}.""" ai_response = { "items": [ { "Item": "256GB SSD Samsung SAS SK-8765", "Type": "SSD", "image_processing": { "crop_bounds": {"x": 10, "y": 20, "width": 400, "height": 350}, "rotation_degrees": 0, "confidence": 0.95 } } ] } with patch("backend.ai_vision.gemini.extract") as mock_extract: mock_extract.return_value = ai_response result = extract_label_info(MINIMAL_PNG, mode="item") bounds = result["items"][0]["image_processing"]["crop_bounds"] assert isinstance(bounds, dict) assert "x" in bounds assert "y" in bounds assert "width" in bounds assert "height" in bounds assert isinstance(bounds["x"], int) assert isinstance(bounds["y"], int) assert isinstance(bounds["width"], int) assert isinstance(bounds["height"], int) # All values should be non-negative assert bounds["x"] >= 0 assert bounds["y"] >= 0 assert bounds["width"] >= 0 assert bounds["height"] >= 0 def test_image_processing_rotation_degrees_range(self): """Test that rotation_degrees is within -360 to +360 range.""" test_cases = [ {"rotation_degrees": 0, "expected": True}, {"rotation_degrees": 90, "expected": True}, {"rotation_degrees": -45, "expected": True}, {"rotation_degrees": 180, "expected": True}, {"rotation_degrees": -180, "expected": True}, {"rotation_degrees": 360, "expected": True}, {"rotation_degrees": -360, "expected": True}, {"rotation_degrees": 15.5, "expected": True}, # Float is valid {"rotation_degrees": -90.5, "expected": True}, ] for test_case in test_cases: ai_response = { "items": [ { "Item": "Test Item", "Type": "Test", "image_processing": { "crop_bounds": {"x": 0, "y": 0, "width": 100, "height": 100}, "rotation_degrees": test_case["rotation_degrees"], "confidence": 0.85 } } ] } with patch("backend.ai_vision.gemini.extract") as mock_extract: mock_extract.return_value = ai_response result = extract_label_info(MINIMAL_PNG, mode="item") rotation = result["items"][0]["image_processing"]["rotation_degrees"] assert isinstance(rotation, (int, float)) assert -360 <= rotation <= 360 def test_image_processing_confidence_float_0_to_1(self): """Test that confidence is a float between 0.0 and 1.0.""" test_cases = [0.0, 0.5, 0.85, 0.92, 1.0] for confidence_val in test_cases: ai_response = { "items": [ { "Item": "Test Item", "Type": "Test", "image_processing": { "crop_bounds": {"x": 0, "y": 0, "width": 100, "height": 100}, "rotation_degrees": 0, "confidence": confidence_val } } ] } with patch("backend.ai_vision.gemini.extract") as mock_extract: mock_extract.return_value = ai_response result = extract_label_info(MINIMAL_PNG, mode="item") confidence = result["items"][0]["image_processing"]["confidence"] assert isinstance(confidence, (int, float)) assert 0.0 <= confidence <= 1.0 def test_image_processing_missing_gracefully_handled(self): """Test graceful handling when image_processing field is missing.""" ai_response = { "items": [ { "Item": "128GB DDR4 Hynix", "Type": "DDR4", "Description": "Memory module", "Category": "Memory", "Size": "128GB", "PartNr": "HYX-12345" # Note: no image_processing field } ] } with patch("backend.ai_vision.gemini.extract") as mock_extract: mock_extract.return_value = ai_response result = extract_label_info(MINIMAL_PNG, mode="item") # Should not crash, just return item without image_processing assert "items" in result assert len(result["items"]) > 0 item = result["items"][0] # image_processing might not be in the response, or it might be None # Either way, extraction should succeed assert item.get("name") == "128GB DDR4 Hynix" or item.get("Item") == "128GB DDR4 Hynix" def test_multiple_items_with_image_processing(self): """Test multiple items each with their own image_processing data.""" ai_response = { "items": [ { "Item": "1.6TB NVMe HPE U.3 P66093-002", "Type": "NVMe", "image_processing": { "crop_bounds": {"x": 50, "y": 100, "width": 300, "height": 200}, "rotation_degrees": 15, "confidence": 0.92 } }, { "Item": "256GB SSD Samsung SAS SK-8765", "Type": "SSD", "image_processing": { "crop_bounds": {"x": 10, "y": 20, "width": 400, "height": 350}, "rotation_degrees": -45, "confidence": 0.88 } }, { "Item": "5m Patchcord LC-LC", "Type": "Patchcord", "image_processing": { "crop_bounds": {"x": 0, "y": 0, "width": 500, "height": 150}, "rotation_degrees": 0, "confidence": 0.95 } } ] } with patch("backend.ai_vision.gemini.extract") as mock_extract: mock_extract.return_value = ai_response result = extract_label_info(MINIMAL_PNG, mode="item") assert len(result["items"]) == 3 for i, item in enumerate(result["items"]): assert "image_processing" in item assert item["image_processing"]["confidence"] in [0.92, 0.88, 0.95] def test_image_processing_with_partial_data(self): """Test handling when image_processing has partial data.""" ai_response = { "items": [ { "Item": "Test Item", "Type": "Test", "image_processing": { "crop_bounds": {"x": 50, "y": 100, "width": 300, "height": 200}, # rotation_degrees missing (optional case) "confidence": 0.75 } } ] } with patch("backend.ai_vision.gemini.extract") as mock_extract: mock_extract.return_value = ai_response result = extract_label_info(MINIMAL_PNG, mode="item") # Should handle gracefully - either include partial data or skip assert result is not None assert "items" in result or "error" not in result def test_crop_bounds_zero_values_valid(self): """Test that crop_bounds with zero values (x=0, y=0) are valid.""" ai_response = { "items": [ { "Item": "Test Item", "Type": "Test", "image_processing": { "crop_bounds": {"x": 0, "y": 0, "width": 100, "height": 100}, "rotation_degrees": 0, "confidence": 0.80 } } ] } with patch("backend.ai_vision.gemini.extract") as mock_extract: mock_extract.return_value = ai_response result = extract_label_info(MINIMAL_PNG, mode="item") bounds = result["items"][0]["image_processing"]["crop_bounds"] assert bounds["x"] == 0 assert bounds["y"] == 0 assert bounds["width"] == 100 assert bounds["height"] == 100 def test_image_processing_box_mode_ignored(self): """Test that image_processing works even in box mode (container discovery).""" ai_response = { "box_label": "Storage Box 1", "name": "Storage Box 1", "category": "Storage", "image_processing": { "crop_bounds": {"x": 100, "y": 50, "width": 400, "height": 300}, "rotation_degrees": 0, "confidence": 0.89 } } with patch("backend.ai_vision.extract_label_info") as mock_extract: # Call the real function but mock just the AI backend with patch("backend.ai_vision.gemini.extract") as mock_gemini: mock_gemini.return_value = ai_response # For box mode, we expect simpler response result = extract_label_info(MINIMAL_PNG, mode="box") # Box mode might not use image_processing, but function shouldn't crash assert result is not None def test_large_crop_bounds_values(self): """Test handling of large crop bound values (e.g., 4K image dimensions).""" ai_response = { "items": [ { "Item": "Test Item", "Type": "Test", "image_processing": { "crop_bounds": {"x": 1000, "y": 2000, "width": 3000, "height": 2000}, "rotation_degrees": 180, "confidence": 0.91 } } ] } with patch("backend.ai_vision.gemini.extract") as mock_extract: mock_extract.return_value = ai_response result = extract_label_info(MINIMAL_PNG, mode="item") bounds = result["items"][0]["image_processing"]["crop_bounds"] assert bounds["x"] == 1000 assert bounds["y"] == 2000 assert bounds["width"] == 3000 assert bounds["height"] == 2000 assert bounds["width"] > 0 and bounds["height"] > 0 def test_claude_provider_with_image_processing(self): """Test image_processing parsing with Claude provider.""" ai_response = { "items": [ { "Item": "512MB Cache Samsung SATA", "Type": "SATA", "image_processing": { "crop_bounds": {"x": 75, "y": 125, "width": 250, "height": 180}, "rotation_degrees": -30, "confidence": 0.87 } } ] } with patch("backend.ai_vision.claude.extract") as mock_claude: mock_claude.return_value = ai_response # Mock the provider selection with patch("backend.ai_vision.extract_label_info") as mock_extract: mock_extract.return_value = ai_response result = mock_extract(MINIMAL_PNG, mode="item") assert result["items"][0]["image_processing"]["confidence"] == 0.87