Spaces:
Sleeping
Sleeping
| """ | |
| Tests for AI node error handling | |
| """ | |
| from unittest.mock import AsyncMock, MagicMock, patch | |
| import pytest | |
| from src.ai.nodes.extract import llm_extraction_node | |
| from src.ai.state import ExtractionGraphState | |
| async def test_extraction_node_handles_missing_doc_fields(): | |
| """Test that extraction node validates required document fields.""" | |
| state = ExtractionGraphState( | |
| batch_id="test-batch", | |
| documents=[ | |
| { | |
| "doc_id": "doc-1", | |
| # Missing 'pages' field | |
| "storage_path": "s3://bucket/doc.pdf" | |
| } | |
| ] | |
| ) | |
| result = await llm_extraction_node(state) | |
| # Should mark document as error instead of crashing | |
| assert len(result["documents"]) == 1 | |
| assert "error" in result["documents"][0] | |
| assert result["documents"][0].get("fallback_required") is True | |
| async def test_extraction_node_handles_empty_documents(): | |
| """Test that extraction node handles empty document list.""" | |
| state = ExtractionGraphState( | |
| batch_id="test-batch", | |
| documents=[] | |
| ) | |
| result = await llm_extraction_node(state) | |
| # Should return gracefully with no documents | |
| assert result["documents"] == [] | |
| assert result["combined_data"] == {} | |
| async def test_extraction_node_specific_exception_handling(): | |
| """Test that extraction node catches only specific exceptions.""" | |
| state = ExtractionGraphState( | |
| batch_id="test-batch", | |
| documents=[ | |
| { | |
| "doc_id": "doc-1", | |
| "pages": ["base64-encoded-image"], | |
| "storage_path": "s3://bucket/doc.pdf" | |
| } | |
| ] | |
| ) | |
| # Mock LLM to raise ValueError (expected) | |
| with patch("src.ai.nodes.extract.ChatGoogleGenerativeAI") as mock_llm: | |
| mock_instance = MagicMock() | |
| structured_llm = AsyncMock() | |
| structured_llm.ainvoke = AsyncMock(side_effect=ValueError("Malformed input")) | |
| mock_instance.with_structured_output.return_value = structured_llm | |
| mock_llm.return_value = mock_instance | |
| result = await llm_extraction_node(state) | |
| # Should handle ValueError gracefully | |
| assert len(result["documents"]) == 1 | |
| assert "error" in result["documents"][0] | |
| async def test_extraction_node_reraises_unknown_exceptions(): | |
| """Test that extraction node re-raises unexpected exceptions.""" | |
| state = ExtractionGraphState( | |
| batch_id="test-batch", | |
| documents=[ | |
| { | |
| "doc_id": "doc-1", | |
| "pages": ["base64-encoded-image"], | |
| "storage_path": "s3://bucket/doc.pdf" | |
| } | |
| ] | |
| ) | |
| # Mock LLM to raise unexpected exception | |
| with patch("src.ai.nodes.extract.ChatGoogleGenerativeAI") as mock_llm: | |
| mock_instance = MagicMock() | |
| structured_llm = AsyncMock() | |
| structured_llm.ainvoke = AsyncMock(side_effect=RuntimeError("Unexpected API error")) | |
| mock_instance.with_structured_output.return_value = structured_llm | |
| mock_llm.return_value = mock_instance | |
| # Should re-raise the unexpected exception | |
| with pytest.raises(RuntimeError, match="Unexpected API error"): | |
| await llm_extraction_node(state) | |
| async def test_extraction_node_combines_data_correctly(): | |
| """Test that extraction node correctly combines data from multiple docs.""" | |
| state = ExtractionGraphState( | |
| batch_id="test-batch", | |
| documents=[ | |
| { | |
| "doc_id": "doc-1", | |
| "pages": ["page1"], | |
| "storage_path": "s3://bucket/doc1.pdf" | |
| }, | |
| { | |
| "doc_id": "doc-2", | |
| "pages": ["page2"], | |
| "storage_path": "s3://bucket/doc2.pdf" | |
| } | |
| ] | |
| ) | |
| # Mock LLM responses | |
| with patch("src.ai.nodes.extract.ChatGoogleGenerativeAI") as mock_llm: | |
| mock_instance = MagicMock() | |
| structured_llm = AsyncMock() | |
| # Return different data for each document | |
| responses = [ | |
| MagicMock(model_dump=MagicMock(return_value={"importer_name": "Company A", "cif_value": 1000})), | |
| MagicMock(model_dump=MagicMock(return_value={"importer_name": "Company B", "cif_value": 2000})) | |
| ] | |
| structured_llm.ainvoke = AsyncMock(side_effect=responses) | |
| mock_instance.with_structured_output.return_value = structured_llm | |
| mock_llm.return_value = mock_instance | |
| result = await llm_extraction_node(state) | |
| # Combined data should have the last writer's value | |
| assert result["combined_data"]["importer_name"] == "Company B" | |
| assert result["combined_data"]["cif_value"] == 2000 | |