/
Watashicuvu
/
agentic-tools
Обзор
Документация
Войти
/
Watashicuvu
/
agentic-tools
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/tests/test_documentation_features.py
235 строк
10 KB
Якуб
mts update
12 апр 2026, 21:19
12 апр 2026, 21:19
bff6dcd
Код
Авторство
О чём код?
""" Тесты для новых функций документации: 1. file_mtime в метаданных 2. Комбинированная сортировка (score + recency) 3. Дедупликация по function_references (Jaccard similarity) 4. Валидация ссылок в документации """ import asyncio import os import tempfile from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest class TestFileMtimeMetadata: """Тест 1: file_mtime сохраняется в метаданных функций и документов.""" def test_documentation_index_stores_file_mtime(self): """file_mtime из os.path.getmtime() сохраняется в метаданные документа.""" from src.services.documentation_index import DocumentationSemanticIndex # Создаём временный файл с известным mtime with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: f.write("# Test Doc\n") f.write("Some content about `my_function()`.") temp_path = f.name try: # Получаем реальный mtime expected_mtime = str(os.path.getmtime(temp_path)) # Мокаем клиент mock_client = AsyncMock() mock_client.create_embedding = AsyncMock(return_value=[0.1] * 10) # Мокаем ChromaDB mock_collection = MagicMock() mock_collection.add = MagicMock() mock_collection.query = MagicMock(return_value={ "documents": [[]], "metadatas": [[]], "distances": [[]] }) with patch.object(DocumentationSemanticIndex, '__init__', lambda self, *a, **kw: None): doc_index = DocumentationSemanticIndex() doc_index.client = mock_client doc_index.docs_collection = mock_collection doc_index.doc_hashes = {} doc_index.project_path = str(Path(temp_path).parent) doc_index.heuristics = { "documentation_indexing": { "max_embedding_text_chars": 2000, "function_reference_patterns": [ r"`([a-zA-Z_][a-zA-Z0-9_]*)\(\)`" ] } } # Индексируем asyncio.get_event_loop().run_until_complete( doc_index.index_document( file_path=Path(temp_path).name, content="# Test Doc\nSome content about `my_function()`.", force=True ) ) # Проверяем, что add был вызван с file_mtime call_args = mock_collection.add.call_args assert call_args is not None, "ChromaDB add() was not called" metadata = call_args[1]['metadatas'][0] if 'metadatas' in call_args[1] else call_args[0][3][0] assert "file_mtime" in metadata, f"file_mtime not in metadata: {metadata}" assert metadata["file_mtime"] == expected_mtime, \ f"Expected mtime {expected_mtime}, got {metadata['file_mtime']}" finally: os.unlink(temp_path) class TestRecencySorting: """Тест 2: Комбинированная сортировка учитывает mtime.""" def test_recency_sorting_combines_score_and_mtime(self): """final_score = semantic * 0.7 + recency * 0.3, старые файлы понижаются.""" from src.services.documentation_index import DocumentationSemanticIndex with patch.object(DocumentationSemanticIndex, '__init__', lambda self, *a, **kw: None): doc_index = DocumentationSemanticIndex() doc_index.heuristics = {"documentation_indexing": {}} # Входные данные: одинаковый score, разный mtime matches = [ {"file_path": "old.md", "score": 0.8, "file_mtime": "1000.0"}, {"file_path": "new.md", "score": 0.8, "file_mtime": "2000.0"}, ] result = doc_index._apply_recency_sorting(matches) # new.md должен иметь более высокий combined_score new_item = next(m for m in result if m["file_path"] == "new.md") old_item = next(m for m in result if m["file_path"] == "old.md") assert "combined_score" in new_item assert "recency_score" in new_item assert new_item["recency_score"] > old_item["recency_score"] assert new_item["combined_score"] > old_item["combined_score"] # new.md должен быть первым assert result[0]["file_path"] == "new.md" def test_recency_sorting_empty_list(self): """Пустой список возвращается без изменений.""" from src.services.documentation_index import DocumentationSemanticIndex with patch.object(DocumentationSemanticIndex, '__init__', lambda self, *a, **kw: None): doc_index = DocumentationSemanticIndex() result = doc_index._apply_recency_sorting([]) assert result == [] class TestDuplicateDetection: """Тест 3: Дедупликация по function_references (Jaccard similarity).""" def test_find_duplicate_documents_jaccard_similarity(self): """Документы с пересечением function_references > 0.6 помечаются как дубликаты.""" from src.services.documentation_index import DocumentationSemanticIndex with patch.object(DocumentationSemanticIndex, '__init__', lambda self, *a, **kw: None): doc_index = DocumentationSemanticIndex() # Мокаем коллекцию с перекрывающимися references mock_collection = MagicMock() mock_collection.get = MagicMock(return_value={ "ids": ["doc1.md", "doc2.md", "doc3.md"], "metadatas": [ { "file_path": "doc1.md", "title": "Doc 1", "function_references": "func_a, func_b, func_c, func_d" }, { "file_path": "doc2.md", "title": "Doc 2", "function_references": "func_a, func_b, func_c, func_e" }, { "file_path": "doc3.md", "title": "Doc 3", "function_references": "func_x, func_y, func_z" }, ] }) doc_index.docs_collection = mock_collection duplicates = doc_index.find_duplicate_documents(min_jaccard_similarity=0.5) # doc1 и doc2 должны быть дубликатами (3 общих из 5 = 0.6 Jaccard) assert len(duplicates) >= 1 assert duplicates[0]["shared_count"] == 3 assert duplicates[0]["jaccard_similarity"] == pytest.approx(0.6, abs=0.01) assert "func_a" in duplicates[0]["shared_references"] def test_find_duplicate_documents_no_overlaps(self): """Документы без пересечений не считаются дубликатами.""" from src.services.documentation_index import DocumentationSemanticIndex with patch.object(DocumentationSemanticIndex, '__init__', lambda self, *a, **kw: None): doc_index = DocumentationSemanticIndex() mock_collection = MagicMock() mock_collection.get = MagicMock(return_value={ "ids": ["doc1.md", "doc2.md"], "metadatas": [ {"file_path": "doc1.md", "title": "Doc 1", "function_references": "func_a, func_b"}, {"file_path": "doc2.md", "title": "Doc 2", "function_references": "func_x, func_y"}, ] }) doc_index.docs_collection = mock_collection duplicates = doc_index.find_duplicate_documents(min_jaccard_similarity=0.6) assert len(duplicates) == 0 class TestCodeBlockReferenceExtraction: """Тест: Извлечение ссылок из code blocks.""" def test_extract_code_block_references(self): """Извлекает функции/классы/вызовы из ```python ... ``` блоков.""" from src.services.documentation_index import DocumentationSemanticIndex content = """ # API Documentation Example usage: ```python class PaymentService: def process_payment(self, amount): validator = PaymentValidator() if validator.validate(amount): return self.charge(amount) ``` """ with patch.object(DocumentationSemanticIndex, '__init__', lambda self, *a, **kw: None): doc_index = DocumentationSemanticIndex() refs = doc_index.extract_code_block_references(content) ref_names = [r['name'] for r in refs] assert 'PaymentService' in ref_names assert 'process_payment' in ref_names assert 'PaymentValidator' in ref_names class TestValidationIntegration: """Тест 4: Валидация ссылок интегрирована в index_repository_async.""" def test_index_documentation_async_includes_validation(self): """index_documentation_async запускает валидацию и логирует предупреждения.""" # Это интеграционный тест — проверяем, что код не падает # и возвращает stats с validation_warnings from src.services.repository_context import RepositoryContext # Мокаем минимальный контекст with tempfile.TemporaryDirectory() as tmpdir: repo_root = Path(tmpdir) (repo_root / "docs").mkdir() (repo_root / "docs" / "test.md").write_text("# Test\n\nSee `nonexistent_function()`.") # Проверка: файл документации существует md_files = list((repo_root / "docs").glob("*.md")) assert len(md_files) == 1 assert md_files[0].name == "test.md" if __name__ == "__main__": pytest.main([__file__, "-v"])