Files
Task_Reporter/tests/test_report_generation.py
egg 3927441103 feat: Add AI report generation with DIFY integration
- Add Users table for display name resolution from AD authentication
- Integrate DIFY AI service for report content generation
- Create docx assembly service with image embedding from MinIO
- Add REST API endpoints for report generation and download
- Add WebSocket notifications for generation progress
- Add frontend UI with progress modal and download functionality
- Add integration tests for report generation flow

Report sections (Traditional Chinese):
- 事件摘要 (Summary)
- 時間軸 (Timeline)
- 參與人員 (Participants)
- 處理過程 (Resolution Process)
- 目前狀態 (Current Status)
- 最終處置結果 (Final Resolution)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 18:32:40 +08:00

345 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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"])