| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- """
- Unit tests for the Novel Translator MCP Server.
- This test suite validates all MCP tools and server functionality.
- """
- import asyncio
- import json
- import tempfile
- from pathlib import Path
- from unittest.mock import AsyncMock, MagicMock, Mock, patch
- import pytest
- # Mock the heavy ML imports before importing server modules
- sys_modules_mock = MagicMock()
- sys_modules_mock.transformers = MagicMock()
- sys_modules_mock.torch = MagicMock()
- with patch.dict('sys.modules', {
- 'transformers': sys_modules_mock.transformers,
- 'torch': sys_modules_mock.torch
- }):
- from src.mcp_server.server import (
- mcp,
- get_pipeline,
- get_glossary,
- get_cleaning_pipeline,
- get_fingerprint_service,
- create_task,
- update_progress,
- complete_task,
- notify_glossary_updated,
- )
- from src.glossary.models import Glossary, GlossaryEntry, TermCategory
- # ============================================================================
- # Fixtures
- # ============================================================================
- @pytest.fixture
- def temp_file():
- """Create a temporary file for testing."""
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt', encoding='utf-8') as f:
- f.write("## Chapter 1\n\nThis is test content.\n\n## Chapter 2\n\nMore content here.")
- temp_path = f.name
- yield temp_path
- Path(temp_path).unlink(missing_ok=True)
- @pytest.fixture
- def sample_glossary():
- """Create a sample glossary for testing."""
- glossary = Glossary()
- glossary.add(GlossaryEntry(
- source="林风",
- target="Lin Feng",
- category=TermCategory.CHARACTER,
- context="Main protagonist"
- ))
- glossary.add(GlossaryEntry(
- source="火球术",
- target="Fireball",
- category=TermCategory.SKILL,
- context="Magic spell"
- ))
- return glossary
- @pytest.fixture
- def mock_pipeline():
- """Create a mock translation pipeline."""
- pipeline = MagicMock()
- pipeline.translate = MagicMock(return_value=MagicMock(
- translated="Translated text",
- terms_used=["林风"]
- ))
- pipeline.translate_batch = MagicMock(return_value=[
- MagicMock(translated="Translation 1", terms_used=["term1"]),
- MagicMock(translated="Translation 2", terms_used=["term2"])
- ])
- pipeline.set_languages = MagicMock()
- pipeline.update_glossary = MagicMock()
- pipeline.src_lang = "zh"
- pipeline.tgt_lang = "en"
- return pipeline
- @pytest.fixture
- def mock_cleaning_pipeline():
- """Create a mock cleaning pipeline."""
- from src.cleaning.models import Chapter
- pipeline = MagicMock()
- pipeline.enable_cleaning = True
- pipeline.enable_splitting = True
- pipeline.process = MagicMock(return_value=[
- Chapter(index=0, title="Chapter 1", content="Content 1", char_count=100),
- Chapter(index=1, title="Chapter 2", content="Content 2", char_count=150),
- ])
- return pipeline
- @pytest.fixture
- def mock_fingerprint_service():
- """Create a mock fingerprint service."""
- service = MagicMock()
- service.check_before_import = MagicMock(return_value=(False, None))
- service.get_fingerprint = MagicMock(return_value="abc123def456")
- service.get_file_info = MagicMock(return_value={
- "fingerprint": "abc123",
- "metadata": {"size": 1000},
- "is_duplicate": False,
- "existing_work_id": None
- })
- return service
- # ============================================================================
- # Server State Tests
- # ============================================================================
- class TestServerState:
- """Tests for server state management."""
- def test_get_glossary_returns_singleton(self):
- """Test that get_glossary returns the same instance."""
- g1 = get_glossary()
- g2 = get_glossary()
- assert g1 is g2
- def test_create_task_generates_unique_id(self):
- """Test that create_task generates unique task IDs."""
- task1 = create_task("test_type", 10)
- task2 = create_task("test_type", 10)
- assert task1 != task2
- # ============================================================================
- # Translation Tool Tests
- # ============================================================================
- class TestTranslationTools:
- """Tests for translation tools."""
- @pytest.mark.asyncio
- async def test_translate_text_with_mock(self, mock_pipeline):
- """Test translate_text with mocked pipeline."""
- from src.mcp_server.server import translate_text
- with patch('src.mcp_server.server.get_pipeline', return_value=mock_pipeline):
- result = await translate_text(
- text="你好世界",
- src_lang="zh",
- tgt_lang="en"
- )
- assert result["success"] is True
- assert result["translated"] == "Translated text"
- assert result["terms_used"] == ["林风"]
- @pytest.mark.asyncio
- async def test_translate_text_empty_input(self):
- """Test translate_text with empty input."""
- from src.mcp_server.server import translate_text
- result = await translate_text(text="", src_lang="zh", tgt_lang="en")
- assert result["success"] is False
- assert "empty" in result["error"].lower()
- @pytest.mark.asyncio
- async def test_translate_batch_with_mock(self, mock_pipeline):
- """Test translate_batch with mocked pipeline."""
- from src.mcp_server.server import translate_batch
- with patch('src.mcp_server.server.get_pipeline', return_value=mock_pipeline):
- result = await translate_batch(
- texts=["Text 1", "Text 2"],
- src_lang="zh",
- tgt_lang="en"
- )
- assert result["success"] is True
- assert result["translations"] == ["Translation 1", "Translation 2"]
- assert len(result["terms_used"]) == 2
- @pytest.mark.asyncio
- async def test_translate_batch_empty_list(self):
- """Test translate_batch with empty list."""
- from src.mcp_server.server import translate_batch
- result = await translate_batch(texts=[], src_lang="zh", tgt_lang="en")
- assert result["success"] is False
- assert "empty" in result["error"].lower()
- @pytest.mark.asyncio
- async def test_translate_file_with_mock(
- self,
- temp_file,
- mock_pipeline,
- mock_cleaning_pipeline
- ):
- """Test translate_file with mocked dependencies."""
- from src.mcp_server.server import translate_file
- with patch('src.mcp_server.server.get_pipeline', return_value=mock_pipeline), \
- patch('src.mcp_server.server.get_cleaning_pipeline', return_value=mock_cleaning_pipeline), \
- patch('src.mcp_server.server.create_task', return_value="test-task-id"), \
- patch('src.mcp_server.server.update_progress', new_callable=AsyncMock), \
- patch('src.mcp_server.server.complete_task', new_callable=AsyncMock):
- # Create a temp output file
- with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='_en.txt') as f:
- output_path = f.name
- try:
- result = await translate_file(
- file_path=temp_file,
- output_path=output_path
- )
- assert result["success"] is True
- assert "task_id" in result
- assert result["chapters_translated"] == 2
- finally:
- Path(output_path).unlink(missing_ok=True)
- @pytest.mark.asyncio
- async def test_translate_file_not_found(self):
- """Test translate_file with non-existent file."""
- from src.mcp_server.server import translate_file
- result = await translate_file(file_path="/nonexistent/file.txt")
- assert result["success"] is False
- assert "not found" in result["error"].lower()
- # ============================================================================
- # Cleaning Tool Tests
- # ============================================================================
- class TestCleaningTools:
- """Tests for cleaning tools."""
- @pytest.mark.asyncio
- async def test_clean_file_with_mock(self, temp_file, mock_cleaning_pipeline):
- """Test clean_file with mocked pipeline."""
- from src.mcp_server.server import clean_file
- with patch('src.mcp_server.server.get_cleaning_pipeline', return_value=mock_cleaning_pipeline):
- result = await clean_file(file_path=temp_file)
- assert result["success"] is True
- assert result["chapter_count"] == 2
- assert result["total_chars"] == 250
- assert len(result["chapters"]) == 2
- @pytest.mark.asyncio
- async def test_clean_file_not_found(self):
- """Test clean_file with non-existent file."""
- from src.mcp_server.server import clean_file
- result = await clean_file(file_path="/nonexistent/file.txt")
- assert result["success"] is False
- assert "not found" in result["error"].lower()
- @pytest.mark.asyncio
- async def test_split_chapters_with_mock(self, mock_cleaning_pipeline):
- """Test split_chapters with mocked splitter."""
- from src.mcp_server.server import split_chapters
- from src.cleaning.splitter import ChapterSplitter
- mock_splitter = MagicMock()
- mock_splitter.split = MagicMock(return_value=[
- MagicMock(index=0, title="Chapter 1", char_count=100, content="Content 1"),
- MagicMock(index=1, title="Chapter 2", char_count=150, content="Content 2"),
- ])
- with patch('src.cleaning.splitter.ChapterSplitter', return_value=mock_splitter):
- result = await split_chapters(text="## Chapter 1\n\nContent\n\n## Chapter 2\n\nMore content")
- assert result["success"] is True
- assert result["chapter_count"] == 2
- @pytest.mark.asyncio
- async def test_split_chapters_empty_text(self):
- """Test split_chapters with empty text."""
- from src.mcp_server.server import split_chapters
- result = await split_chapters(text="")
- assert result["success"] is False
- assert "empty" in result["error"].lower()
- # ============================================================================
- # Glossary Tool Tests
- # ============================================================================
- class TestGlossaryTools:
- """Tests for glossary tools."""
- @pytest.mark.asyncio
- async def test_glossary_add(self):
- """Test adding a term to the glossary."""
- from src.mcp_server.server import glossary_add, get_glossary
- # Clear the glossary first
- get_glossary()._terms.clear()
- result = await glossary_add(
- source="林风",
- target="Lin Feng",
- category="character",
- context="Main protagonist"
- )
- assert result["success"] is True
- assert "entry" in result
- assert result["entry"]["source"] == "林风"
- assert result["entry"]["target"] == "Lin Feng"
- @pytest.mark.asyncio
- async def test_glossary_add_empty_source(self):
- """Test adding a term with empty source."""
- from src.mcp_server.server import glossary_add
- result = await glossary_add(source="", target="Lin Feng")
- assert result["success"] is False
- assert "empty" in result["error"].lower()
- @pytest.mark.asyncio
- async def test_glossary_add_empty_target(self):
- """Test adding a term with empty target."""
- from src.mcp_server.server import glossary_add
- result = await glossary_add(source="林风", target="")
- assert result["success"] is False
- assert "empty" in result["error"].lower()
- @pytest.mark.asyncio
- async def test_glossary_list(self, sample_glossary):
- """Test listing glossary entries."""
- from src.mcp_server.server import glossary_list, get_glossary
- # Replace with sample glossary
- with patch('src.mcp_server.server.get_glossary', return_value=sample_glossary):
- result = await glossary_list()
- assert result["success"] is True
- assert result["count"] == 2
- assert len(result["entries"]) == 2
- # Check entries
- sources = [e["source"] for e in result["entries"]]
- assert "林风" in sources
- assert "火球术" in sources
- @pytest.mark.asyncio
- async def test_glossary_clear(self):
- """Test clearing the glossary."""
- from src.mcp_server.server import glossary_clear, get_glossary
- # Add some entries first
- get_glossary().add(GlossaryEntry(
- source="Test",
- target="Test EN",
- category=TermCategory.OTHER
- ))
- result = await glossary_clear()
- assert result["success"] is True
- assert len(get_glossary()._terms) == 0
- # ============================================================================
- # Fingerprint Tool Tests
- # ============================================================================
- class TestFingerprintTools:
- """Tests for fingerprint tools."""
- @pytest.mark.asyncio
- async def test_check_duplicate(self, temp_file, mock_fingerprint_service):
- """Test checking for duplicate files."""
- from src.mcp_server.server import check_duplicate
- with patch('src.mcp_server.server.get_fingerprint_service', return_value=mock_fingerprint_service):
- result = await check_duplicate(file_path=temp_file)
- assert result["success"] is True
- assert result["is_duplicate"] is False
- assert result["fingerprint"] == "abc123def456"
- @pytest.mark.asyncio
- async def test_check_duplicate_not_found(self):
- """Test check_duplicate with non-existent file."""
- from src.mcp_server.server import check_duplicate
- result = await check_duplicate(file_path="/nonexistent/file.txt")
- assert result["success"] is False
- assert "not found" in result["error"].lower()
- @pytest.mark.asyncio
- async def test_get_fingerprint(self, temp_file, mock_fingerprint_service):
- """Test getting file fingerprint."""
- from src.mcp_server.server import get_fingerprint
- with patch('src.mcp_server.server.get_fingerprint_service', return_value=mock_fingerprint_service):
- result = await get_fingerprint(file_path=temp_file)
- assert result["success"] is True
- assert result["fingerprint"] == "abc123def456"
- assert "file_name" in result
- @pytest.mark.asyncio
- async def test_get_fingerprint_not_found(self):
- """Test get_fingerprint with non-existent file."""
- from src.mcp_server.server import get_fingerprint
- result = await get_fingerprint(file_path="/nonexistent/file.txt")
- assert result["success"] is False
- assert "not found" in result["error"].lower()
- # ============================================================================
- # Progress Resource Tests
- # ============================================================================
- class TestProgressResources:
- """Tests for progress resources."""
- @pytest.mark.asyncio
- async def test_progress_resource_flow(self):
- """Test the full progress resource flow."""
- from src.mcp_server.server import get_progress_resource, list_all_progress
- # Create a task
- task_id = create_task("test_type", 10)
- # Update progress
- await update_progress(task_id, {"current": 5, "percent": 50.0})
- # Get progress
- progress_json = await get_progress_resource(task_id)
- progress = json.loads(progress_json)
- assert progress["task_id"] == task_id
- assert progress["current"] == 5
- assert progress["percent"] == 50.0
- # List all progress
- list_json = await list_all_progress()
- task_list = json.loads(list_json)
- assert task_list["count"] >= 1
- # Complete task
- await complete_task(task_id, success=True)
- # Verify completion
- final_progress = json.loads(await get_progress_resource(task_id))
- assert final_progress["status"] == "completed"
- @pytest.mark.asyncio
- async def test_progress_resource_not_found(self):
- """Test getting progress for non-existent task."""
- from src.mcp_server.server import get_progress_resource
- progress_json = await get_progress_resource("non-existent-task-id")
- progress = json.loads(progress_json)
- assert "error" in progress
- assert "not found" in progress["error"].lower()
- # ============================================================================
- # Integration Tests
- # ============================================================================
- class TestIntegration:
- """Integration tests for complete workflows."""
- @pytest.mark.asyncio
- async def test_glossary_translation_workflow(self):
- """Test adding terms and using them in translation."""
- from src.mcp_server.server import glossary_add, glossary_list
- # Clear glossary
- get_glossary()._terms.clear()
- # Add terms
- await glossary_add(source="林风", target="Lin Feng", category="character")
- await glossary_add(source="青云宗", target="Qingyun Sect", category="organization")
- # List terms
- result = await glossary_list()
- assert result["success"] is True
- assert result["count"] == 2
- # Verify terms were added
- sources = {e["source"] for e in result["entries"]}
- assert "林风" in sources
- assert "青云宗" in sources
- if __name__ == "__main__":
- pytest.main([__file__, "-v", "--cov=src/mcp_server", "--cov-report=term"])
|