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>
This commit is contained in:
191
backend/tests/test_ai.py
Normal file
191
backend/tests/test_ai.py
Normal file
@@ -0,0 +1,191 @@
|
||||
"""
|
||||
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"
|
||||
Reference in New Issue
Block a user