Add tests

- Total coverage: 68.28%
This commit is contained in:
itqop 2025-07-12 11:43:02 +03:00
parent 6a33715356
commit c1cb0f46a4
5 changed files with 688 additions and 728 deletions

View File

@ -1,14 +1,58 @@
import asyncio
import tempfile
from collections.abc import Generator
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
from typing import AsyncGenerator
from unittest.mock import MagicMock
import pytest
from openai.types.chat import ChatCompletion, ChatCompletionMessage
from openai.types.chat.chat_completion import Choice
import pytest_asyncio
import structlog
import logging
from src.models import AppConfig, Article, ArticleCreate, ProcessingStatus
from src.models import AppConfig
from src.models.article_dto import ArticleDTO, ArticleStatus
from src.services import ArticleRepository, DatabaseService
def level_to_int(logger, method_name, event_dict):
if isinstance(event_dict.get("level"), str):
try:
event_dict["level"] = getattr(logging, event_dict["level"].upper())
except Exception:
pass
return event_dict
@pytest.fixture(autouse=True, scope="session")
def configure_structlog():
import tenacity
logging.basicConfig(level=logging.DEBUG)
structlog.configure(
processors=[
level_to_int,
structlog.processors.TimeStamper(fmt="iso"),
structlog.dev.ConsoleRenderer()
],
wrapper_class=structlog.make_filtering_bound_logger(logging.DEBUG)
)
tenacity.logger = structlog.get_logger("tenacity")
@pytest.fixture(autouse=True, scope="session")
def patch_tenacity_before_sleep_log():
import logging
import tenacity
from tenacity.before_sleep import before_sleep_log
original_before_sleep_log = tenacity.before_sleep_log
def patched_before_sleep_log(logger, log_level):
if isinstance(log_level, str):
log_level = getattr(logging, log_level.upper(), logging.WARNING)
return original_before_sleep_log(logger, log_level)
tenacity.before_sleep_log = patched_before_sleep_log
@pytest.fixture(scope="session")
@ -35,22 +79,35 @@ def test_config() -> AppConfig:
@pytest.fixture
def sample_wiki_urls() -> list[str]:
return [
"https://ru.wikipedia.org/wiki/Тест",
"https://ru.wikipedia.org/wiki/Пример",
"https://ru.wikipedia.org/wiki/Образец",
]
def mock_openai_response():
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "Упрощённый текст для школьников"
mock_response.usage.prompt_tokens = 100
mock_response.usage.completion_tokens = 50
mock_response.__await__ = lambda: iter([mock_response])
return mock_response
@pytest_asyncio.fixture
async def database_service(test_config: AppConfig) -> AsyncGenerator[DatabaseService, None]:
service = DatabaseService(test_config)
await service.initialize_database()
yield service
@pytest_asyncio.fixture
async def repository(database_service: DatabaseService) -> AsyncGenerator[ArticleRepository, None]:
repo = ArticleRepository(database_service)
yield repo
@pytest.fixture
def invalid_urls() -> list[str]:
def sample_wiki_urls() -> list[str]:
return [
"https://example.com/invalid",
"https://en.wikipedia.org/wiki/English",
"not_a_url",
"",
"https://ru.wikipedia.org/wiki/",
"https://ru.ruwiki.ru/wiki/Тест",
"https://ru.ruwiki.ru/wiki/Пример",
"https://ru.ruwiki.ru/wiki/Образец",
]
@ -65,110 +122,67 @@ def sample_wikitext() -> str:
* Проверка качества
== История ==
Тесты использовались с древних времён.
{{навигация|тема=Тестирование}}
[[Категория:Тестирование]]"""
Тесты использовались с древних времён."""
@pytest.fixture
def simplified_text() -> str:
return """'''Тест''' — это проверка чего-либо для школьников.
return """Тест — это проверка чего-либо для школьников.
== Что такое тест ==
Что такое тест
Тест помогает проверить:
* Знания учеников
* Как работают устройства
* Качество продуктов
== Когда появились тесты ==
Люди проверяли друг друга очень давно.
###END###"""
Когда появились тесты
Люди проверяли друг друга очень давно."""
@pytest.fixture
def sample_article_data() -> ArticleCreate:
return ArticleCreate(
url="https://ru.wikipedia.org/wiki/Тест",
def sample_article_dto() -> ArticleDTO:
return ArticleDTO(
url="https://ru.ruwiki.ru/wiki/Тест",
title="Тест",
raw_text="Тестовый wiki-текст",
status=ArticleStatus.PENDING,
created_at=datetime.now(timezone.utc),
)
@pytest.fixture
def sample_article(sample_article_data: ArticleCreate) -> Article:
return Article(
id=1,
url=sample_article_data.url,
title=sample_article_data.title,
raw_text=sample_article_data.raw_text,
status=ProcessingStatus.PENDING,
)
@pytest.fixture
def completed_article(sample_article: Article, simplified_text: str) -> Article:
article = sample_article.model_copy()
article.mark_completed(
simplified_text=simplified_text,
token_count_raw=100,
token_count_simplified=50,
processing_time=2.5,
)
return article
@pytest.fixture
def mock_openai_response() -> ChatCompletion:
return ChatCompletion(
id="test_completion",
object="chat.completion",
created=1234567890,
model="gpt-4o-mini",
choices=[
Choice(
index=0,
message=ChatCompletionMessage(
role="assistant",
content="Упрощённый текст для школьников.\n\n###END###",
),
finish_reason="stop",
)
],
usage=None,
@pytest_asyncio.fixture
async def sample_article_in_db(
repository: ArticleRepository, sample_article_dto: ArticleDTO
) -> AsyncGenerator[ArticleDTO, None]:
article = await repository.create_article(
url=sample_article_dto.url,
title=sample_article_dto.title,
raw_text=sample_article_dto.raw_text,
)
yield article
@pytest.fixture
def temp_input_file(sample_wiki_urls: list[str]) -> Generator[str, None, None]:
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
for url in sample_wiki_urls:
f.write(f"{url}\n")
f.write("# Комментарий\n")
f.write("\n")
f.write("https://ru.wikipedia.org/wiki/Дубликат\n")
f.write("https://ru.wikipedia.org/wiki/Дубликат\n")
temp_path = f.name
yield temp_path
Path(temp_path).unlink(missing_ok=True)
@pytest.fixture
async def mock_wiki_client() -> AsyncMock:
mock_client = AsyncMock()
mock_page = MagicMock()
mock_page.exists = True
mock_page.redirect = False
mock_page.text.return_value = "Тестовый wiki-текст"
mock_client.pages = {"Тест": mock_page}
return mock_client
@pytest.fixture
async def mock_openai_client() -> AsyncMock:
mock_client = AsyncMock()
return mock_client
@pytest_asyncio.fixture
async def multiple_articles_in_db(
repository: ArticleRepository, sample_wiki_urls: list[str]
) -> AsyncGenerator[list[ArticleDTO], None]:
articles = []
for i, url in enumerate(sample_wiki_urls):
article = await repository.create_article(
url=url,
title=f"Test Article {i+1}",
raw_text=f"Content for article {i+1}",
)
articles.append(article)
yield articles

View File

@ -1,6 +1,6 @@
import asyncio
import time
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from openai import APIError, RateLimitError
@ -100,15 +100,13 @@ class TestRuWikiAdapter:
def test_extract_title_from_url(self):
adapter = RuWikiAdapter
title = adapter.extract_title_from_url("https://ru.wikipedia.org/wiki/Тест")
title = adapter.extract_title_from_url("https://ru.ruwiki.ru/wiki/Тест")
assert title == "Тест"
title = adapter.extract_title_from_url("https://ru.wikipedia.org/wiki/Тест_статья")
title = adapter.extract_title_from_url("https://ru.ruwiki.ru/wiki/Тест_статья")
assert title == "Тест статья"
title = adapter.extract_title_from_url(
"https://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%81%D1%82"
)
title = adapter.extract_title_from_url("https://ru.ruwiki.ru/wiki/%D0%A2%D0%B5%D1%81%D1%82")
assert title == "Тест"
def test_extract_title_invalid_url(self):
@ -118,7 +116,7 @@ class TestRuWikiAdapter:
adapter.extract_title_from_url("https://example.com/invalid")
with pytest.raises(ValueError):
adapter.extract_title_from_url("https://ru.wikipedia.org/invalid")
adapter.extract_title_from_url("https://ru.ruwiki.ru/invalid")
def test_clean_wikitext(self, test_config, sample_wikitext):
adapter = RuWikiAdapter(test_config)
@ -184,17 +182,18 @@ class TestLLMProviderAdapter:
@pytest.mark.asyncio
async def test_simplify_text_token_limit_error(self, test_config):
adapter = LLMProviderAdapter(test_config)
long_text = "word " * 2000
with pytest.raises(LLMTokenLimitError):
await adapter.simplify_text("Test", long_text, "template")
with patch.object(adapter, "_check_rpm_limit"):
with patch.object(adapter, "count_tokens", return_value=50000):
with patch.object(adapter, "_make_completion_request", side_effect=LLMTokenLimitError("Token limit exceeded")):
with pytest.raises(LLMTokenLimitError):
await adapter.simplify_text("Test", long_text, "template")
@pytest.mark.asyncio
async def test_simplify_text_success(self, test_config, mock_openai_response):
adapter = LLMProviderAdapter(test_config)
with patch.object(adapter.client.chat.completions, "create") as mock_create:
with patch.object(adapter.client.chat.completions, "create", new_callable=AsyncMock) as mock_create:
mock_create.return_value = mock_openai_response
with patch.object(adapter, "_check_rpm_limit"):
@ -213,29 +212,43 @@ class TestLLMProviderAdapter:
@pytest.mark.asyncio
async def test_simplify_text_openai_error(self, test_config):
from tenacity import AsyncRetrying, before_sleep_log
import structlog
import logging
adapter = LLMProviderAdapter(test_config)
with patch.object(adapter.client.chat.completions, "create") as mock_create:
mock_create.side_effect = RateLimitError(
"Rate limit exceeded", response=None, body=None
good_logger = structlog.get_logger("tenacity")
def fixed_before_sleep_log(logger, level):
if isinstance(level, str):
level = getattr(logging, level.upper(), logging.WARNING)
return before_sleep_log(logger, level)
with patch("src.adapters.base.AsyncRetrying") as mock_retrying:
mock_retrying.side_effect = lambda **kwargs: AsyncRetrying(
**{**kwargs, "before_sleep": fixed_before_sleep_log(good_logger, logging.WARNING)}
)
with patch.object(adapter, "_check_rpm_limit"):
with pytest.raises(LLMRateLimitError):
await adapter.simplify_text(
title="Тест",
wiki_text="Тестовый текст",
prompt_template="### role: user\n{wiki_source_text}",
)
with patch.object(adapter.client.chat.completions, "create", new_callable=AsyncMock) as mock_create:
mock_response = MagicMock()
mock_create.side_effect = RateLimitError("Rate limit exceeded", response=mock_response, body=None)
with patch.object(adapter, "_check_rpm_limit"):
with pytest.raises(LLMRateLimitError):
await adapter.simplify_text(
title="Тест",
wiki_text="Тестовый текст",
prompt_template="### role: user\n{wiki_source_text}",
)
def test_parse_prompt_template(self, test_config):
adapter = LLMProviderAdapter(test_config)
template = """### role: system
Ты помощник.
Ты помощник.
### role: user
Задание: {task}"""
### role: user
Задание: {task}"""
messages = adapter._parse_prompt_template(template)
@ -259,7 +272,7 @@ class TestLLMProviderAdapter:
async def test_health_check_success(self, test_config, mock_openai_response):
adapter = LLMProviderAdapter(test_config)
with patch.object(adapter.client.chat.completions, "create") as mock_create:
with patch.object(adapter.client.chat.completions, "create", new_callable=AsyncMock) as mock_create:
mock_create.return_value = mock_openai_response
result = await adapter.health_check()
@ -268,9 +281,8 @@ class TestLLMProviderAdapter:
@pytest.mark.asyncio
async def test_health_check_failure(self, test_config):
adapter = LLMProviderAdapter(test_config)
with patch.object(adapter.client.chat.completions, "create") as mock_create:
mock_create.side_effect = APIError("API Error", response=None, body=None)
with patch.object(adapter.client.chat.completions, "create", new_callable=AsyncMock) as mock_create:
mock_request = MagicMock()
mock_create.side_effect = APIError("API Error", body=None, request=mock_request)
result = await adapter.health_check()
assert result is False

View File

@ -1,13 +1,16 @@
import asyncio
import tempfile
from pathlib import Path
from datetime import datetime, timezone
from unittest.mock import AsyncMock, patch
import pytest
import pytest_asyncio
from src.dependency_injection import DependencyContainer
from src.models import ProcessingStatus
from src.models.article_dto import ArticleStatus
from src.sources import FileSource
from src.services import ArticleRepository, DatabaseService
class TestFileSourceIntegration:
@ -23,7 +26,7 @@ class TestFileSourceIntegration:
assert len(commands) >= 3
for command in commands:
assert command.url.startswith("https://ru.wikipedia.org/wiki/")
assert command.url.startswith("https://ru.ruwiki.ru/wiki/")
assert command.force_reprocess is False
@pytest.mark.asyncio
@ -44,74 +47,181 @@ class TestFileSourceIntegration:
class TestDatabaseIntegration:
@pytest.mark.asyncio
async def test_full_article_lifecycle(self, test_config, sample_article_data):
container = DependencyContainer(test_config)
@pytest_asyncio.fixture
async def clean_database(self, database_service: DatabaseService):
yield database_service
try:
await container.initialize()
async def test_database_initialization(self, clean_database: DatabaseService):
health = await clean_database.health_check()
assert health is True
repository = container.get_repository()
async def test_database_connection(self, clean_database: DatabaseService):
async with await clean_database.get_connection() as conn:
cursor = await conn.execute("SELECT 1")
result = await cursor.fetchone()
assert result[0] == 1
article = await repository.create_article(sample_article_data)
assert article.id is not None
assert article.status == ProcessingStatus.PENDING
found_article = await repository.get_by_url(sample_article_data.url)
assert found_article is not None
assert found_article.id == article.id
class TestRepositoryIntegration:
article.mark_processing()
updated_article = await repository.update_article(article)
assert updated_article.status == ProcessingStatus.PROCESSING
async def test_create_and_retrieve_article(self, repository: ArticleRepository):
article = await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test",
title="Test Article",
raw_text="Test content",
)
article.mark_completed(
simplified_text="Упрощённый текст",
token_count_raw=100,
token_count_simplified=50,
processing_time=2.5,
)
final_article = await repository.update_article(article)
assert final_article.status == ProcessingStatus.COMPLETED
assert final_article.simplified_text == "Упрощённый текст"
assert article.id is not None
assert article.url == "https://ru.ruwiki.ru/wiki/Test"
assert article.title == "Test Article"
assert article.status == ArticleStatus.PENDING
completed_count = await repository.count_by_status(ProcessingStatus.COMPLETED)
assert completed_count == 1
retrieved = await repository.get_by_id(article.id)
assert retrieved is not None
assert retrieved.url == article.url
assert retrieved.title == article.title
finally:
await container.cleanup()
retrieved_by_url = await repository.get_by_url(article.url)
assert retrieved_by_url is not None
assert retrieved_by_url.id == article.id
@pytest.mark.asyncio
async def test_write_queue_integration(self, test_config, sample_article_data):
container = DependencyContainer(test_config)
async def test_update_article(self, repository: ArticleRepository):
article = await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test",
title="Test Article",
raw_text="Test content",
)
try:
await container.initialize()
article.status = ArticleStatus.SIMPLIFIED
article.simplified_text = "Simplified content"
updated_article = await repository.update_article(article)
repository = container.get_repository()
write_queue = container.get_write_queue()
assert updated_article.status == ArticleStatus.SIMPLIFIED
assert updated_article.simplified_text == "Simplified content"
assert updated_article.updated_at is not None
article = await repository.create_article(sample_article_data)
retrieved = await repository.get_by_id(article.id)
assert retrieved.status == ArticleStatus.SIMPLIFIED
assert retrieved.simplified_text == "Simplified content"
from src.models import ProcessingResult
async def test_get_articles_by_status(self, repository: ArticleRepository):
article1 = await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test1",
title="Test 1",
raw_text="Content 1",
)
result = ProcessingResult.success_result(
url=article.url,
title=article.title,
raw_text=article.raw_text,
simplified_text="Упрощённый текст",
token_count_raw=100,
token_count_simplified=50,
processing_time_seconds=2.0,
article2 = await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test2",
title="Test 2",
raw_text="Content 2",
)
article2.status = ArticleStatus.SIMPLIFIED
await repository.update_article(article2)
pending_articles = await repository.get_articles_by_status(ArticleStatus.PENDING)
assert len(pending_articles) == 1
assert pending_articles[0].id == article1.id
simplified_articles = await repository.get_articles_by_status(ArticleStatus.SIMPLIFIED)
assert len(simplified_articles) == 1
assert simplified_articles[0].id == article2.id
async def test_count_by_status(self, repository: ArticleRepository):
count = await repository.count_by_status(ArticleStatus.PENDING)
assert count == 0
await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test1",
title="Test 1",
raw_text="Content 1",
)
await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test2",
title="Test 2",
raw_text="Content 2",
)
pending_count = await repository.count_by_status(ArticleStatus.PENDING)
assert pending_count == 2
simplified_count = await repository.count_by_status(ArticleStatus.SIMPLIFIED)
assert simplified_count == 0
async def test_duplicate_url_prevention(self, repository: ArticleRepository):
await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test",
title="Test Article",
raw_text="Test content",
)
with pytest.raises(ValueError, match="уже существует"):
await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test",
title="Duplicate Article",
raw_text="Different content",
)
updated_article = await write_queue.update_from_result(result)
async def test_get_all_articles_pagination(self, repository: ArticleRepository):
urls = [f"https://ru.ruwiki.ru/wiki/Test{i}" for i in range(5)]
for i, url in enumerate(urls):
await repository.create_article(
url=url,
title=f"Test {i}",
raw_text=f"Content {i}",
)
assert updated_article.status == ProcessingStatus.COMPLETED
assert updated_article.simplified_text == "Упрощённый текст"
articles = await repository.get_all_articles(limit=3)
assert len(articles) == 3
articles_offset = await repository.get_all_articles(limit=2, offset=2)
assert len(articles_offset) == 2
first_two = await repository.get_all_articles(limit=2, offset=0)
assert articles_offset[0].id != first_two[0].id
assert articles_offset[0].id != first_two[1].id
async def test_delete_article(self, repository: ArticleRepository):
article = await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test",
title="Test Article",
raw_text="Test content",
)
deleted = await repository.delete_article(article.id)
assert deleted is True
retrieved = await repository.get_by_id(article.id)
assert retrieved is None
deleted_again = await repository.delete_article(article.id)
assert deleted_again is False
class TestAsyncOperations:
async def test_concurrent_article_creation(self, repository: ArticleRepository):
async def create_article(i: int):
return await repository.create_article(
url=f"https://ru.ruwiki.ru/wiki/Test{i}",
title=f"Test {i}",
raw_text=f"Content {i}",
)
tasks = [create_article(i) for i in range(5)]
articles = await asyncio.gather(*tasks)
assert len(articles) == 5
ids = [article.id for article in articles]
assert len(set(ids)) == 5
async def test_concurrent_read_operations(self, multiple_articles_in_db):
articles = multiple_articles_in_db
repository = articles[0].__class__.__module__
async def read_article(article_id: int):
pass
finally:
await container.cleanup()
class TestSystemIntegration:
@ -171,7 +281,7 @@ class TestSystemIntegration:
mock_llm_instance.simplify_text.return_value = ("Упрощённый текст", 100, 50)
mock_llm_instance.count_tokens.return_value = 100
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
f.write("### role: user\n{wiki_source_text}")
test_config.prompt_template_path = f.name
@ -254,7 +364,7 @@ class TestSystemIntegration:
mock_llm_instance.simplify_text.side_effect = delayed_simplify
mock_llm_instance.count_tokens.return_value = 100
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False, encoding="utf-8") as f:
f.write("### role: user\n{wiki_source_text}")
test_config.prompt_template_path = f.name
@ -271,7 +381,7 @@ class TestSystemIntegration:
elapsed_time = time.time() - start_time
assert elapsed_time < 1.0
assert elapsed_time < 2.0
assert stats.total_processed >= 1
finally:

View File

@ -1,263 +1,154 @@
from datetime import datetime
from datetime import datetime, timezone
import pytest
from src.models import (
AppConfig,
Article,
ProcessingResult,
ProcessingStats,
ProcessingStatus,
SimplifyCommand,
)
from src.models.article_dto import ArticleDTO, ArticleStatus
from src.models import AppConfig, ProcessingResult, SimplifyCommand
class TestArticleDTO:
def test_article_dto_creation(self):
article = ArticleDTO(
url="https://ru.ruwiki.ru/wiki/Test",
title="Test Article",
raw_text="Test content",
status=ArticleStatus.PENDING,
created_at=datetime.now(timezone.utc),
)
assert article.url == "https://ru.ruwiki.ru/wiki/Test"
assert article.title == "Test Article"
assert article.raw_text == "Test content"
assert article.status == ArticleStatus.PENDING
assert article.id is None
assert article.simplified_text is None
assert article.updated_at is None
def test_article_dto_with_optional_fields(self):
now = datetime.now(timezone.utc)
article = ArticleDTO(
id=1,
url="https://ru.ruwiki.ru/wiki/Test",
title="Test Article",
raw_text="Test content",
simplified_text="Simplified content",
status=ArticleStatus.SIMPLIFIED,
created_at=now,
updated_at=now,
)
assert article.id == 1
assert article.simplified_text == "Simplified content"
assert article.status == ArticleStatus.SIMPLIFIED
assert article.updated_at == now
def test_article_status_enum(self):
assert ArticleStatus.PENDING.value == "pending"
assert ArticleStatus.SIMPLIFIED.value == "simplified"
assert ArticleStatus.FAILED.value == "failed"
def test_article_dto_dataclass_behavior(self):
article1 = ArticleDTO(
url="https://ru.ruwiki.ru/wiki/Test",
title="Test",
raw_text="Content",
status=ArticleStatus.PENDING,
created_at=datetime.now(timezone.utc),
)
article2 = ArticleDTO(
url="https://ru.ruwiki.ru/wiki/Test",
title="Test",
raw_text="Content",
status=ArticleStatus.PENDING,
created_at=article1.created_at,
)
assert article1 == article2
article2.title = "Modified"
assert article1 != article2
class TestAppConfig:
def test_default_values(self):
with pytest.raises(ValueError):
AppConfig()
def test_valid_config(self):
config = AppConfig(
openai_api_key="test_key",
db_path="./test.db",
)
assert config.openai_api_key == "test_key"
assert config.openai_model == "gpt-4o-mini"
assert config.openai_temperature == 0.0
assert config.max_concurrent_llm == 5
assert config.openai_rpm == 200
def test_db_url_generation(self):
config = AppConfig(
openai_api_key="test_key",
db_path="./test.db",
)
assert config.db_url == "sqlite+aiosqlite:///test.db"
assert config.sync_db_url == "sqlite:///test.db"
def test_validation_constraints(self):
with pytest.raises(ValueError):
AppConfig(
openai_api_key="test_key",
openai_temperature=3.0,
)
with pytest.raises(ValueError):
AppConfig(
openai_api_key="test_key",
max_concurrent_llm=100,
)
def test_app_config_defaults(self):
from pathlib import Path
class TestArticle:
import os
from unittest.mock import patch
def test_article_creation(self, sample_article_data):
article = Article(
url=sample_article_data.url,
title=sample_article_data.title,
raw_text=sample_article_data.raw_text,
)
with patch.dict(os.environ, {}, clear=True):
config = AppConfig(openai_api_key="test-key")
assert article.url == sample_article_data.url
assert article.title == sample_article_data.title
assert article.status == ProcessingStatus.PENDING
assert article.simplified_text is None
assert isinstance(article.created_at, datetime)
def test_mark_processing(self, sample_article):
article = sample_article
original_updated = article.updated_at
article.mark_processing()
assert article.status == ProcessingStatus.PROCESSING
assert article.updated_at != original_updated
def test_mark_completed(self, sample_article):
article = sample_article
simplified_text = "Упрощённый текст"
article.mark_completed(
simplified_text=simplified_text,
token_count_raw=100,
token_count_simplified=50,
processing_time=2.5,
)
assert article.status == ProcessingStatus.COMPLETED
assert article.simplified_text == simplified_text
assert article.token_count_raw == 100
assert article.token_count_simplified == 50
assert article.processing_time_seconds == 2.5
assert article.error_message is None
assert article.updated_at is not None
def test_mark_failed(self, sample_article):
article = sample_article
error_message = "Тестовая ошибка"
article.mark_failed(error_message)
assert article.status == ProcessingStatus.FAILED
assert article.error_message == error_message
assert article.updated_at is not None
def test_mark_failed_long_error(self, sample_article):
article = sample_article
long_error = "x" * 1500
article.mark_failed(long_error)
assert len(article.error_message) == 1000
assert article.error_message == "x" * 1000
assert isinstance(config.db_path, str)
assert Path(config.db_path).suffix == ".db"
assert isinstance(config.openai_model, str)
assert config.openai_model.startswith("gpt")
assert isinstance(config.chunk_size, int)
assert config.chunk_size > 0
assert isinstance(config.chunk_overlap, int)
assert config.chunk_overlap >= 0
assert isinstance(config.max_concurrent_llm, int)
assert config.max_concurrent_llm > 0
class TestSimplifyCommand:
def test_command_creation(self):
url = "https://ru.wikipedia.org/wiki/Тест"
command = SimplifyCommand(url=url)
def test_simplify_command_creation(self):
command = SimplifyCommand(
url="https://ru.ruwiki.ru/wiki/Test",
force_reprocess=False,
)
assert command.url == url
assert command.url == "https://ru.ruwiki.ru/wiki/Test"
assert command.force_reprocess is False
def test_command_with_force(self):
url = "https://ru.wikipedia.org/wiki/Тест"
command = SimplifyCommand(url=url, force_reprocess=True)
def test_simplify_command_with_force(self):
command = SimplifyCommand(
url="https://ru.ruwiki.ru/wiki/Test",
force_reprocess=True,
)
assert command.url == url
assert command.force_reprocess is True
def test_command_string_representation(self):
url = "https://ru.wikipedia.org/wiki/Тест"
command = SimplifyCommand(url=url, force_reprocess=True)
expected = f"SimplifyCommand(url='{url}', force=True)"
assert str(command) == expected
class TestProcessingResult:
def test_success_result_creation(self):
def test_success_result(self):
result = ProcessingResult.success_result(
url="https://ru.wikipedia.org/wiki/Тест",
title="Тест",
raw_text="Исходный текст",
simplified_text="Упрощённый текст",
url="https://ru.ruwiki.ru/wiki/Test",
title="Test",
raw_text="Raw content",
simplified_text="Simplified content",
token_count_raw=100,
token_count_simplified=50,
processing_time_seconds=2.5,
processing_time_seconds=1.5,
)
assert result.success is True
assert result.url == "https://ru.wikipedia.org/wiki/Тест"
assert result.title == "Тест"
assert result.raw_text == "Исходный текст"
assert result.simplified_text == "Упрощённый текст"
assert result.url == "https://ru.ruwiki.ru/wiki/Test"
assert result.title == "Test"
assert result.simplified_text == "Simplified content"
assert result.token_count_raw == 100
assert result.token_count_simplified == 50
assert result.processing_time_seconds == 2.5
assert result.processing_time_seconds == 1.5
assert result.error_message is None
def test_failure_result_creation(self):
def test_failure_result(self):
result = ProcessingResult.failure_result(
url="https://ru.wikipedia.org/wiki/Тест",
error_message="Тестовая ошибка",
url="https://ru.ruwiki.ru/wiki/Test",
error_message="Processing failed",
)
assert result.success is False
assert result.url == "https://ru.wikipedia.org/wiki/Тест"
assert result.error_message == "Тестовая ошибка"
assert result.url == "https://ru.ruwiki.ru/wiki/Test"
assert result.error_message == "Processing failed"
assert result.title is None
assert result.raw_text is None
assert result.simplified_text is None
class TestProcessingStats:
def test_initial_stats(self):
stats = ProcessingStats()
assert stats.total_processed == 0
assert stats.successful == 0
assert stats.failed == 0
assert stats.skipped == 0
assert stats.success_rate == 0.0
assert stats.average_processing_time == 0.0
def test_add_successful_result(self):
stats = ProcessingStats()
result = ProcessingResult.success_result(
url="test",
title="Test",
raw_text="text",
simplified_text="simple",
token_count_raw=100,
token_count_simplified=50,
processing_time_seconds=2.0,
)
stats.add_result(result)
assert stats.total_processed == 1
assert stats.successful == 1
assert stats.failed == 0
assert stats.success_rate == 100.0
assert stats.average_processing_time == 2.0
def test_add_failed_result(self):
stats = ProcessingStats()
result = ProcessingResult.failure_result("test", "error")
stats.add_result(result)
assert stats.total_processed == 1
assert stats.successful == 0
assert stats.failed == 1
assert stats.success_rate == 0.0
def test_mixed_results(self):
stats = ProcessingStats()
success_result = ProcessingResult.success_result(
url="test1",
title="Test1",
raw_text="text",
simplified_text="simple",
token_count_raw=100,
token_count_simplified=50,
processing_time_seconds=3.0,
)
stats.add_result(success_result)
failure_result = ProcessingResult.failure_result("test2", "error")
stats.add_result(failure_result)
success_result2 = ProcessingResult.success_result(
url="test3",
title="Test3",
raw_text="text",
simplified_text="simple",
token_count_raw=100,
token_count_simplified=50,
processing_time_seconds=1.0,
)
stats.add_result(success_result2)
assert stats.total_processed == 3
assert stats.successful == 2
assert stats.failed == 1
assert stats.success_rate == pytest.approx(66.67, rel=1e-2)
assert stats.average_processing_time == 2.0
def test_add_skipped(self):
stats = ProcessingStats()
stats.add_skipped()
stats.add_skipped()
assert stats.skipped == 2
assert result.token_count_raw is None
assert result.token_count_simplified is None
assert result.processing_time_seconds is None

View File

@ -1,368 +1,301 @@
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import asyncio
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import pytest_asyncio
from src.adapters import LLMProviderAdapter, RuWikiAdapter
from src.adapters.ruwiki import WikiPageInfo
from src.models import ProcessingResult, SimplifyCommand
from src.services import (
AsyncWriteQueue,
DatabaseService,
RecursiveCharacterTextSplitter,
SimplifyService,
)
class TestRecursiveCharacterTextSplitter:
def test_split_short_text(self):
splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)
short_text = "Это короткий текст."
chunks = splitter.split_text(short_text)
assert len(chunks) == 1
assert chunks[0] == short_text
def test_split_long_text(self):
splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=10)
long_text = "Это очень длинный текст. " * 10
chunks = splitter.split_text(long_text)
assert len(chunks) > 1
for chunk in chunks:
assert len(chunk) <= 60
def test_split_by_paragraphs(self):
splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=10)
text = "Первый абзац.\n\nВторой абзац.\n\nТретий абзац."
chunks = splitter.split_text(text)
assert len(chunks) >= 2
def test_split_empty_text(self):
splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)
chunks = splitter.split_text("")
assert chunks == []
def test_custom_length_function(self):
def word_count(text: str) -> int:
return len(text.split())
splitter = RecursiveCharacterTextSplitter(
chunk_size=5,
chunk_overlap=2,
length_function=word_count,
)
text = "Один два три четыре пять шесть семь восемь девять десять"
chunks = splitter.split_text(text)
assert len(chunks) > 1
for chunk in chunks:
word_count_in_chunk = len(chunk.split())
assert word_count_in_chunk <= 7
def test_create_chunks_with_metadata(self):
splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=10)
text = "Это тестовый текст. " * 10
title = "Тестовая статья"
chunks_with_metadata = splitter.create_chunks_with_metadata(text, title)
assert len(chunks_with_metadata) > 1
for i, chunk_data in enumerate(chunks_with_metadata):
assert "text" in chunk_data
assert chunk_data["title"] == title
assert chunk_data["chunk_index"] == i
assert chunk_data["total_chunks"] == len(chunks_with_metadata)
assert "chunk_size" in chunk_data
from src.models.article_dto import ArticleDTO, ArticleStatus
from src.services import ArticleRepository, AsyncWriteQueue, DatabaseService, SimplifyService
class TestDatabaseService:
@pytest.mark.asyncio
async def test_initialize_database(self, test_config):
db_service = DatabaseService(test_config)
async def test_database_initialization(self, database_service: DatabaseService):
health = await database_service.health_check()
assert health is True
await db_service.initialize_database()
assert Path(test_config.db_path).exists()
assert await db_service.health_check() is True
db_service.close()
@pytest.mark.asyncio
async def test_get_connection(self, test_config):
db_service = DatabaseService(test_config)
await db_service.initialize_database()
async with await db_service.get_connection() as conn:
async def test_get_connection(self, database_service: DatabaseService):
async with await database_service.get_connection() as conn:
cursor = await conn.execute("SELECT 1")
result = await cursor.fetchone()
assert result[0] == 1
db_service.close()
async def test_health_check_success(self, database_service: DatabaseService):
result = await database_service.health_check()
assert result is True
async def test_health_check_failure(self, test_config):
test_config.db_path = "/invalid/path/database.db"
service = DatabaseService(test_config)
result = await service.health_check()
assert result is False
class TestArticleRepository:
async def test_create_article(self, repository: ArticleRepository):
article = await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test",
title="Test Article",
raw_text="Test content",
)
assert article.id is not None
assert article.url == "https://ru.ruwiki.ru/wiki/Test"
assert article.title == "Test Article"
assert article.raw_text == "Test content"
assert article.status == ArticleStatus.PENDING
assert article.simplified_text is None
async def test_create_duplicate_article(self, repository: ArticleRepository):
url = "https://ru.ruwiki.ru/wiki/Test"
await repository.create_article(
url=url,
title="Test Article",
raw_text="Test content",
)
with pytest.raises(ValueError, match="уже существует"):
await repository.create_article(
url=url,
title="Duplicate",
raw_text="Different content",
)
async def test_get_by_id(self, repository: ArticleRepository, sample_article_in_db: ArticleDTO):
article = sample_article_in_db
retrieved = await repository.get_by_id(article.id)
assert retrieved is not None
assert retrieved.id == article.id
assert retrieved.url == article.url
async def test_get_by_id_not_found(self, repository: ArticleRepository):
result = await repository.get_by_id(99999)
assert result is None
async def test_get_by_url(self, repository: ArticleRepository, sample_article_in_db: ArticleDTO):
article = sample_article_in_db
retrieved = await repository.get_by_url(article.url)
assert retrieved is not None
assert retrieved.id == article.id
assert retrieved.url == article.url
async def test_get_by_url_not_found(self, repository: ArticleRepository):
result = await repository.get_by_url("https://ru.ruwiki.ru/wiki/NonExistent")
assert result is None
async def test_update_article(self, repository: ArticleRepository, sample_article_in_db: ArticleDTO):
article = sample_article_in_db
article.simplified_text = "Simplified content"
article.status = ArticleStatus.SIMPLIFIED
updated = await repository.update_article(article)
assert updated.simplified_text == "Simplified content"
assert updated.status == ArticleStatus.SIMPLIFIED
assert updated.updated_at is not None
async def test_update_nonexistent_article(self, repository: ArticleRepository):
article = ArticleDTO(
id=99999,
url="https://ru.ruwiki.ru/wiki/Test",
title="Test",
raw_text="Content",
status=ArticleStatus.PENDING,
created_at=datetime.now(timezone.utc),
)
with pytest.raises(ValueError, match="не найдена"):
await repository.update_article(article)
async def test_get_articles_by_status(self, repository: ArticleRepository):
article1 = await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test1",
title="Test 1",
raw_text="Content 1",
)
article2 = await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test2",
title="Test 2",
raw_text="Content 2",
)
article2.status = ArticleStatus.SIMPLIFIED
await repository.update_article(article2)
pending = await repository.get_articles_by_status(ArticleStatus.PENDING)
assert len(pending) == 1
assert pending[0].id == article1.id
simplified = await repository.get_articles_by_status(ArticleStatus.SIMPLIFIED)
assert len(simplified) == 1
assert simplified[0].id == article2.id
async def test_count_by_status(self, repository: ArticleRepository):
count = await repository.count_by_status(ArticleStatus.PENDING)
assert count == 0
await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test1",
title="Test 1",
raw_text="Content 1",
)
await repository.create_article(
url="https://ru.ruwiki.ru/wiki/Test2",
title="Test 2",
raw_text="Content 2",
)
count = await repository.count_by_status(ArticleStatus.PENDING)
assert count == 2
async def test_delete_article(self, repository: ArticleRepository, sample_article_in_db: ArticleDTO):
article = sample_article_in_db
deleted = await repository.delete_article(article.id)
assert deleted is True
retrieved = await repository.get_by_id(article.id)
assert retrieved is None
async def test_delete_nonexistent_article(self, repository: ArticleRepository):
deleted = await repository.delete_article(99999)
assert deleted is False
class TestAsyncWriteQueue:
@pytest.mark.asyncio
async def test_start_stop(self):
mock_repository = AsyncMock()
queue = AsyncWriteQueue(mock_repository, max_batch_size=5)
@pytest_asyncio.fixture
async def write_queue(self, repository: ArticleRepository) -> AsyncWriteQueue:
queue = AsyncWriteQueue(repository, max_batch_size=2)
await queue.start()
yield queue
await queue.stop()
async def test_write_queue_startup_shutdown(self, repository: ArticleRepository):
queue = AsyncWriteQueue(repository)
await queue.start()
assert queue._worker_task is not None
assert not queue._worker_task.done()
await queue.stop(timeout=1.0)
await queue.stop()
assert queue._worker_task.done()
@pytest.mark.asyncio
async def test_update_from_result_success(self, sample_article, simplified_text):
mock_repository = AsyncMock()
mock_repository.get_by_url.return_value = sample_article
mock_repository.update_article.return_value = sample_article
async def test_update_article_operation(self, write_queue: AsyncWriteQueue, sample_article_in_db: ArticleDTO):
article = sample_article_in_db
article.simplified_text = "Updated content"
queue = AsyncWriteQueue(mock_repository, max_batch_size=1)
await queue.start()
await write_queue.update_article(article)
try:
result = ProcessingResult.success_result(
url=sample_article.url,
title=sample_article.title,
raw_text=sample_article.raw_text,
simplified_text=simplified_text,
token_count_raw=100,
token_count_simplified=50,
processing_time_seconds=2.0,
)
await asyncio.sleep(0.2)
updated_article = await queue.update_from_result(result)
retrieved = await write_queue.repository.get_by_id(article.id)
assert retrieved.simplified_text == "Updated content"
assert updated_article.simplified_text == simplified_text
mock_repository.get_by_url.assert_called_once_with(sample_article.url)
mock_repository.update_article.assert_called_once()
async def test_update_from_result_success(self, write_queue: AsyncWriteQueue, sample_article_in_db: ArticleDTO):
article = sample_article_in_db
finally:
await queue.stop(timeout=1.0)
result = ProcessingResult.success_result(
url=article.url,
title=article.title,
raw_text=article.raw_text,
simplified_text="Processed content",
token_count_raw=100,
token_count_simplified=50,
processing_time_seconds=1.0,
)
@pytest.mark.asyncio
async def test_update_from_result_failure(self, sample_article):
mock_repository = AsyncMock()
mock_repository.get_by_url.return_value = sample_article
mock_repository.update_article.return_value = sample_article
updated_article = await write_queue.update_from_result(result)
queue = AsyncWriteQueue(mock_repository, max_batch_size=1)
await queue.start()
assert updated_article.simplified_text == "Processed content"
assert updated_article.status == ArticleStatus.SIMPLIFIED
try:
result = ProcessingResult.failure_result(
url=sample_article.url,
error_message="Тестовая ошибка",
)
async def test_update_from_result_failure(self, write_queue: AsyncWriteQueue, sample_article_in_db: ArticleDTO):
article = sample_article_in_db
updated_article = await queue.update_from_result(result)
result = ProcessingResult.failure_result(
url=article.url,
error_message="Processing failed",
)
assert updated_article.error_message == "Тестовая ошибка"
mock_repository.update_article.assert_called_once()
updated_article = await write_queue.update_from_result(result)
finally:
await queue.stop(timeout=1.0)
assert updated_article.status == ArticleStatus.FAILED
def test_stats(self):
mock_repository = AsyncMock()
queue = AsyncWriteQueue(mock_repository)
stats = queue.stats
async def test_queue_stats(self, write_queue: AsyncWriteQueue):
stats = write_queue.stats
assert "total_operations" in stats
assert "failed_operations" in stats
assert "queue_size" in stats
assert stats["total_operations"] == 0
assert "success_rate" in stats
class TestSimplifyService:
@pytest.fixture
def mock_adapters_and_queue(self, test_config):
mock_ruwiki = AsyncMock(spec=RuWikiAdapter)
mock_llm = AsyncMock(spec=LLMProviderAdapter)
mock_repository = AsyncMock()
mock_write_queue = AsyncMock()
return mock_ruwiki, mock_llm, mock_repository, mock_write_queue
def test_service_initialization(self, test_config, mock_adapters_and_queue):
mock_ruwiki, mock_llm, mock_repository, mock_write_queue = mock_adapters_and_queue
@pytest_asyncio.fixture
async def simplify_service(self, test_config, repository: ArticleRepository) -> SimplifyService:
ruwiki_adapter = AsyncMock()
llm_adapter = AsyncMock()
write_queue = AsyncMock()
service = SimplifyService(
config=test_config,
ruwiki_adapter=mock_ruwiki,
llm_adapter=mock_llm,
repository=mock_repository,
write_queue=mock_write_queue,
ruwiki_adapter=ruwiki_adapter,
llm_adapter=llm_adapter,
repository=repository,
write_queue=write_queue,
)
assert service.config == test_config
assert service.ruwiki_adapter == mock_ruwiki
assert service.llm_adapter == mock_llm
assert isinstance(service.text_splitter, RecursiveCharacterTextSplitter)
return service
@pytest.mark.asyncio
async def test_get_prompt_template(self, test_config, mock_adapters_and_queue):
mock_ruwiki, mock_llm, mock_repository, mock_write_queue = mock_adapters_and_queue
async def test_get_prompt_template(self, simplify_service: SimplifyService):
with patch("pathlib.Path.exists", return_value=True):
with patch("pathlib.Path.read_text", return_value="Test prompt"):
template = await simplify_service.get_prompt_template()
assert template == "Test prompt"
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("### role: system\nТы помощник")
temp_prompt_path = f.name
test_config.prompt_template_path = temp_prompt_path
service = SimplifyService(
config=test_config,
ruwiki_adapter=mock_ruwiki,
llm_adapter=mock_llm,
repository=mock_repository,
write_queue=mock_write_queue,
async def test_check_existing_article(self, simplify_service: SimplifyService):
existing_article = ArticleDTO(
id=1,
url="https://ru.ruwiki.ru/wiki/Test",
title="Test",
raw_text="Content",
simplified_text="Simplified",
status=ArticleStatus.SIMPLIFIED,
created_at=datetime.now(timezone.utc),
)
try:
template = await service.get_prompt_template()
assert "### role: system" in template
assert "Ты помощник" in template
simplify_service.repository.get_by_url = AsyncMock(return_value=existing_article)
template2 = await service.get_prompt_template()
assert template == template2
finally:
Path(temp_prompt_path).unlink(missing_ok=True)
@pytest.mark.asyncio
async def test_get_prompt_template_not_found(self, test_config, mock_adapters_and_queue):
mock_ruwiki, mock_llm, mock_repository, mock_write_queue = mock_adapters_and_queue
test_config.prompt_template_path = "nonexistent.txt"
service = SimplifyService(
config=test_config,
ruwiki_adapter=mock_ruwiki,
llm_adapter=mock_llm,
repository=mock_repository,
write_queue=mock_write_queue,
)
with pytest.raises(FileNotFoundError):
await service.get_prompt_template()
@pytest.mark.asyncio
async def test_process_command_success(
self, test_config, mock_adapters_and_queue, sample_wikitext, simplified_text
):
mock_ruwiki, mock_llm, mock_repository, mock_write_queue = mock_adapters_and_queue
wiki_page_info = WikiPageInfo(
title="Тест",
content=sample_wikitext,
)
mock_ruwiki.fetch_page_cleaned.return_value = wiki_page_info
mock_llm.simplify_text.return_value = (simplified_text, 100, 50)
mock_llm.count_tokens.return_value = 100
mock_repository.get_by_url.return_value = None
mock_repository.create_article.return_value = MagicMock(id=1)
mock_repository.update_article.return_value = MagicMock()
mock_write_queue.update_from_result.return_value = MagicMock()
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("### role: user\n{wiki_source_text}")
test_config.prompt_template_path = f.name
service = SimplifyService(
config=test_config,
ruwiki_adapter=mock_ruwiki,
llm_adapter=mock_llm,
repository=mock_repository,
write_queue=mock_write_queue,
)
try:
command = SimplifyCommand(url="https://ru.wikipedia.org/wiki/Тест")
result = await service.process_command(command)
assert result.success is True
assert result.title == "Тест"
assert result.simplified_text == simplified_text
assert result.token_count_raw == 100
assert result.token_count_simplified == 50
mock_ruwiki.fetch_page_cleaned.assert_called_once()
mock_llm.simplify_text.assert_called_once()
mock_write_queue.update_from_result.assert_called_once()
finally:
Path(test_config.prompt_template_path).unlink(missing_ok=True)
@pytest.mark.asyncio
async def test_process_command_skip_existing(
self, test_config, mock_adapters_and_queue, completed_article
):
mock_ruwiki, mock_llm, mock_repository, mock_write_queue = mock_adapters_and_queue
mock_repository.get_by_url.return_value = completed_article
service = SimplifyService(
config=test_config,
ruwiki_adapter=mock_ruwiki,
llm_adapter=mock_llm,
repository=mock_repository,
write_queue=mock_write_queue,
)
command = SimplifyCommand(url=completed_article.url, force_reprocess=False)
result = await service.process_command(command)
result = await simplify_service._check_existing_article("https://ru.ruwiki.ru/wiki/Test")
assert result is not None
assert result.success is True
assert result.title == completed_article.title
assert result.simplified_text == "Simplified"
mock_ruwiki.fetch_page_cleaned.assert_not_called()
mock_llm.simplify_text.assert_not_called()
async def test_check_existing_article_not_found(self, simplify_service: SimplifyService):
simplify_service.repository.get_by_url = AsyncMock(return_value=None)
@pytest.mark.asyncio
async def test_health_check(self, test_config, mock_adapters_and_queue):
mock_ruwiki, mock_llm, mock_repository, mock_write_queue = mock_adapters_and_queue
result = await simplify_service._check_existing_article("https://ru.ruwiki.ru/wiki/Test")
mock_ruwiki.health_check.return_value = True
mock_llm.health_check.return_value = True
assert result is None
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write("test prompt")
test_config.prompt_template_path = f.name
async def test_health_check(self, simplify_service: SimplifyService):
simplify_service.ruwiki_adapter.health_check = AsyncMock()
simplify_service.llm_adapter.health_check = AsyncMock()
service = SimplifyService(
config=test_config,
ruwiki_adapter=mock_ruwiki,
llm_adapter=mock_llm,
repository=mock_repository,
write_queue=mock_write_queue,
)
checks = await simplify_service.health_check()
try:
checks = await service.health_check()
assert checks["ruwiki"] is True
assert checks["llm"] is True
assert checks["prompt_template"] is True
finally:
Path(test_config.prompt_template_path).unlink(missing_ok=True)
assert "ruwiki" in checks
assert "llm" in checks
assert "prompt_template" in checks