Files
Meeting_Assistant/backend/tests/test_ai.py
egg 8b6184ecc5 feat: Meeting Assistant MVP - Complete implementation
Enterprise Meeting Knowledge Management System with:

Backend (FastAPI):
- Authentication proxy with JWT (pj-auth-api integration)
- MySQL database with 4 tables (users, meetings, conclusions, actions)
- Meeting CRUD with system code generation (C-YYYYMMDD-XX, A-YYYYMMDD-XX)
- Dify LLM integration for AI summarization
- Excel export with openpyxl
- 20 unit tests (all passing)

Client (Electron):
- Login page with company auth
- Meeting list with create/delete
- Meeting detail with real-time transcription
- Editable transcript textarea (single block, easy editing)
- AI summarization with conclusions/action items
- 5-second segment recording (efficient for long meetings)

Sidecar (Python):
- faster-whisper medium model with int8 quantization
- ONNX Runtime VAD (lightweight, ~20MB vs PyTorch ~2GB)
- Chinese punctuation processing
- OpenCC for Traditional Chinese conversion
- Anti-hallucination parameters
- Auto-cleanup of temp audio files

OpenSpec:
- add-meeting-assistant-mvp (47 tasks, archived)
- add-realtime-transcription (29 tasks, archived)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 20:17:44 +08:00

192 lines
6.4 KiB
Python

"""
Unit tests for AI summarization with mock Dify responses.
"""
import pytest
from unittest.mock import patch, MagicMock, AsyncMock
import json
pytestmark = pytest.mark.asyncio
class TestDifyResponseParsing:
"""Tests for parsing Dify LLM responses."""
def test_parse_json_response(self):
"""Test parsing valid JSON response from Dify."""
from app.routers.ai import parse_dify_response
response = '''Here is the summary:
```json
{
"conclusions": ["Agreed on Q1 budget", "New hire approved"],
"action_items": [
{"content": "Submit budget report", "owner": "John", "due_date": "2025-01-15"},
{"content": "Post job listing", "owner": "", "due_date": null}
]
}
```
'''
result = parse_dify_response(response)
assert len(result["conclusions"]) == 2
assert "Q1 budget" in result["conclusions"][0]
assert len(result["action_items"]) == 2
assert result["action_items"][0]["owner"] == "John"
def test_parse_inline_json_response(self):
"""Test parsing inline JSON without code blocks."""
from app.routers.ai import parse_dify_response
response = '{"conclusions": ["Budget approved"], "action_items": []}'
result = parse_dify_response(response)
assert len(result["conclusions"]) == 1
assert result["conclusions"][0] == "Budget approved"
def test_parse_non_json_response(self):
"""Test fallback when response is not JSON."""
from app.routers.ai import parse_dify_response
response = "The meeting discussed Q1 budget and hiring plans."
result = parse_dify_response(response)
# Should return the raw response as a single conclusion
assert len(result["conclusions"]) == 1
assert "Q1 budget" in result["conclusions"][0]
assert len(result["action_items"]) == 0
def test_parse_empty_response(self):
"""Test handling empty response."""
from app.routers.ai import parse_dify_response
result = parse_dify_response("")
assert result["conclusions"] == []
assert result["action_items"] == []
class TestSummarizeEndpoint:
"""Tests for the AI summarization endpoint."""
@patch("app.routers.ai.httpx.AsyncClient")
@patch("app.routers.ai.settings")
async def test_summarize_success(self, mock_settings, mock_client_class):
"""Test successful summarization."""
mock_settings.DIFY_API_URL = "https://dify.test.com/v1"
mock_settings.DIFY_API_KEY = "test-key"
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"answer": json.dumps({
"conclusions": ["Decision made"],
"action_items": [{"content": "Follow up", "owner": "Alice", "due_date": "2025-01-20"}]
})
}
mock_client = AsyncMock()
mock_client.post.return_value = mock_response
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_client_class.return_value = mock_client
from app.routers.ai import summarize_transcript
from app.models import SummarizeRequest, TokenPayload
mock_user = TokenPayload(email="test@test.com", role="user")
result = await summarize_transcript(
SummarizeRequest(transcript="Test meeting transcript"),
current_user=mock_user
)
assert len(result.conclusions) == 1
assert len(result.action_items) == 1
assert result.action_items[0].owner == "Alice"
@patch("app.routers.ai.httpx.AsyncClient")
@patch("app.routers.ai.settings")
async def test_summarize_handles_timeout(self, mock_settings, mock_client_class):
"""Test handling Dify timeout."""
import httpx
from fastapi import HTTPException
mock_settings.DIFY_API_URL = "https://dify.test.com/v1"
mock_settings.DIFY_API_KEY = "test-key"
mock_client = AsyncMock()
mock_client.post.side_effect = httpx.TimeoutException("Timeout")
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_client_class.return_value = mock_client
from app.routers.ai import summarize_transcript
from app.models import SummarizeRequest, TokenPayload
mock_user = TokenPayload(email="test@test.com", role="user")
with pytest.raises(HTTPException) as exc_info:
await summarize_transcript(
SummarizeRequest(transcript="Test"),
current_user=mock_user
)
assert exc_info.value.status_code == 504
@patch("app.routers.ai.settings")
async def test_summarize_no_api_key(self, mock_settings):
"""Test error when Dify API key is not configured."""
from fastapi import HTTPException
mock_settings.DIFY_API_KEY = ""
from app.routers.ai import summarize_transcript
from app.models import SummarizeRequest, TokenPayload
mock_user = TokenPayload(email="test@test.com", role="user")
with pytest.raises(HTTPException) as exc_info:
await summarize_transcript(
SummarizeRequest(transcript="Test"),
current_user=mock_user
)
assert exc_info.value.status_code == 503
class TestPartialDataHandling:
"""Tests for handling partial data from AI."""
def test_action_item_with_empty_owner(self):
"""Test action items with empty owner are handled."""
from app.routers.ai import parse_dify_response
response = json.dumps({
"conclusions": [],
"action_items": [
{"content": "Task 1", "owner": "", "due_date": None},
{"content": "Task 2", "owner": "Bob", "due_date": "2025-02-01"}
]
})
result = parse_dify_response(response)
assert result["action_items"][0]["owner"] == ""
assert result["action_items"][1]["owner"] == "Bob"
def test_action_item_with_missing_fields(self):
"""Test action items with missing fields."""
from app.routers.ai import parse_dify_response
response = json.dumps({
"conclusions": ["Done"],
"action_items": [
{"content": "Task only"}
]
})
result = parse_dify_response(response)
# Should have content but other fields may be missing
assert result["action_items"][0]["content"] == "Task only"