"""Integration tests for AI report generation Tests the report generation flow: 1. Data collection from room 2. DIFY prompt construction 3. Document assembly """ import pytest from datetime import datetime from unittest.mock import Mock, patch, AsyncMock import json # Test data fixtures @pytest.fixture def sample_room_data(): """Sample room metadata for testing""" return { "room_id": "test-room-123", "title": "設備故障測試", "incident_type": "equipment_failure", "severity": "high", "status": "active", "location": "A棟生產線", "description": "測試用事件描述", "resolution_notes": None, "created_at": datetime(2024, 1, 15, 9, 0), "resolved_at": None, "created_by": "test@example.com", } @pytest.fixture def sample_messages(): """Sample messages for testing""" return [ { "message_id": "msg-1", "sender_id": "user1@example.com", "sender_name": "張三", "content": "發現設備異常", "message_type": "text", "created_at": datetime(2024, 1, 15, 9, 5), "file_name": None, }, { "message_id": "msg-2", "sender_id": "user2@example.com", "sender_name": "李四", "content": "收到,立即前往查看", "message_type": "text", "created_at": datetime(2024, 1, 15, 9, 10), "file_name": None, }, ] @pytest.fixture def sample_members(): """Sample members for testing""" return [ { "user_id": "user1@example.com", "display_name": "張三", "role": "owner", }, { "user_id": "user2@example.com", "display_name": "李四", "role": "editor", }, ] @pytest.fixture def sample_files(): """Sample files for testing""" return [ { "file_id": "file-1", "filename": "故障照片.jpg", "file_type": "image", "mime_type": "image/jpeg", "uploaded_at": datetime(2024, 1, 15, 9, 15), "uploader_id": "user1@example.com", "uploader_name": "張三", "minio_object_path": "rooms/test-room-123/file-1.jpg", }, ] @pytest.fixture def sample_ai_response(): """Sample AI response JSON for testing""" return { "summary": { "content": "A棟生產線設備於2024年1月15日發生異常,維修人員已前往處理。" }, "timeline": { "events": [ {"time": "09:05", "description": "張三發現設備異常並通報"}, {"time": "09:10", "description": "李四收到通報前往查看"}, ] }, "participants": { "members": [ {"name": "張三", "role": "事件發起人"}, {"name": "李四", "role": "維修負責人"}, ] }, "resolution_process": { "content": "維修人員接獲通報後立即前往現場查看設備狀況。" }, "current_status": { "status": "active", "description": "維修人員正在現場處理中" }, "final_resolution": { "has_resolution": False, "content": "" } } class TestPromptConstruction: """Tests for prompt construction""" def test_build_report_prompt_includes_room_info(self, sample_room_data, sample_messages, sample_members, sample_files): """Test that prompt includes room information""" from app.modules.report_generation.prompts import build_report_prompt prompt = build_report_prompt( room_data=sample_room_data, messages=sample_messages, members=sample_members, files=sample_files, ) assert "設備故障測試" in prompt assert "設備故障" in prompt # Translated incident type assert "高" in prompt # Translated severity assert "A棟生產線" in prompt def test_build_report_prompt_includes_messages(self, sample_room_data, sample_messages, sample_members, sample_files): """Test that prompt includes message content""" from app.modules.report_generation.prompts import build_report_prompt prompt = build_report_prompt( room_data=sample_room_data, messages=sample_messages, members=sample_members, files=sample_files, ) assert "發現設備異常" in prompt assert "張三" in prompt assert "李四" in prompt def test_build_report_prompt_includes_files(self, sample_room_data, sample_messages, sample_members, sample_files): """Test that prompt includes file information""" from app.modules.report_generation.prompts import build_report_prompt prompt = build_report_prompt( room_data=sample_room_data, messages=sample_messages, members=sample_members, files=sample_files, ) assert "故障照片.jpg" in prompt assert "圖片" in prompt # File type label class TestDifyJsonParsing: """Tests for DIFY response JSON parsing""" def test_extract_json_from_pure_json(self, sample_ai_response): """Test parsing pure JSON response""" from app.modules.report_generation.services.dify_client import DifyService service = DifyService() text = json.dumps(sample_ai_response) result = service._extract_json(text) assert result["summary"]["content"] == sample_ai_response["summary"]["content"] def test_extract_json_from_markdown_code_block(self, sample_ai_response): """Test parsing JSON from markdown code block""" from app.modules.report_generation.services.dify_client import DifyService service = DifyService() text = f"```json\n{json.dumps(sample_ai_response)}\n```" result = service._extract_json(text) assert result["summary"]["content"] == sample_ai_response["summary"]["content"] def test_extract_json_from_mixed_text(self, sample_ai_response): """Test parsing JSON embedded in other text""" from app.modules.report_generation.services.dify_client import DifyService service = DifyService() text = f"Here is the report:\n{json.dumps(sample_ai_response)}\nEnd of report." result = service._extract_json(text) assert result["summary"]["content"] == sample_ai_response["summary"]["content"] def test_validate_schema_with_valid_data(self, sample_ai_response): """Test schema validation with valid data""" from app.modules.report_generation.services.dify_client import DifyService service = DifyService() # Should not raise any exception service._validate_schema(sample_ai_response) def test_validate_schema_with_missing_section(self, sample_ai_response): """Test schema validation with missing section""" from app.modules.report_generation.services.dify_client import DifyService, DifyValidationError service = DifyService() del sample_ai_response["summary"] with pytest.raises(DifyValidationError) as exc_info: service._validate_schema(sample_ai_response) assert "summary" in str(exc_info.value) class TestDocxGeneration: """Tests for docx document generation""" def test_create_report_returns_bytesio(self, sample_room_data, sample_ai_response, sample_files): """Test that document assembly returns BytesIO""" from app.modules.report_generation.services.docx_service import DocxAssemblyService import io service = DocxAssemblyService() result = service.create_report( room_data=sample_room_data, ai_content=sample_ai_response, files=sample_files, include_images=False, # Skip image download for test include_file_list=True, ) assert isinstance(result, io.BytesIO) assert result.getvalue() # Should have content def test_create_report_is_valid_docx(self, sample_room_data, sample_ai_response, sample_files): """Test that generated document is valid DOCX""" from app.modules.report_generation.services.docx_service import DocxAssemblyService from docx import Document service = DocxAssemblyService() result = service.create_report( room_data=sample_room_data, ai_content=sample_ai_response, files=sample_files, include_images=False, include_file_list=True, ) # Should be able to parse as DOCX doc = Document(result) assert len(doc.paragraphs) > 0 class TestReportDataService: """Tests for report data collection service""" def test_to_prompt_dict_format(self, sample_room_data, sample_messages, sample_members, sample_files): """Test data conversion to prompt dictionary format""" from app.modules.report_generation.services.report_data_service import ( ReportDataService, RoomReportData, MessageData, MemberData, FileData, ) # Create RoomReportData manually for testing room_data = RoomReportData( room_id=sample_room_data["room_id"], title=sample_room_data["title"], incident_type=sample_room_data["incident_type"], severity=sample_room_data["severity"], status=sample_room_data["status"], location=sample_room_data["location"], description=sample_room_data["description"], resolution_notes=sample_room_data["resolution_notes"], created_at=sample_room_data["created_at"], resolved_at=sample_room_data["resolved_at"], created_by=sample_room_data["created_by"], messages=[ MessageData( message_id=m["message_id"], sender_id=m["sender_id"], sender_name=m["sender_name"], content=m["content"], message_type=m["message_type"], created_at=m["created_at"], file_name=m.get("file_name"), ) for m in sample_messages ], members=[ MemberData( user_id=m["user_id"], display_name=m["display_name"], role=m["role"], ) for m in sample_members ], files=[ FileData( file_id=f["file_id"], filename=f["filename"], file_type=f["file_type"], mime_type=f["mime_type"], uploaded_at=f["uploaded_at"], uploader_id=f["uploader_id"], uploader_name=f["uploader_name"], minio_object_path=f["minio_object_path"], ) for f in sample_files ], ) # Test conversion using class method directly (not from db session) # Create mock db session mock_db = Mock() service = ReportDataService(mock_db) result = service.to_prompt_dict(room_data) assert "room_data" in result assert "messages" in result assert "members" in result assert "files" in result assert result["room_data"]["title"] == sample_room_data["title"] assert len(result["messages"]) == 2 assert len(result["members"]) == 2 assert len(result["files"]) == 1 if __name__ == "__main__": pytest.main([__file__, "-v"])