""" Extended tests for library models - Comprehensive coverage of unified document architecture. Tests cover: - Document model operations - Collection model operations - DocumentCollection (many-to-many) operations - DocumentChunk model operations - DownloadQueue model operations - RAGIndex model operations - LibraryStatistics model operations - CollectionFolder and CollectionFolderFile models """ import uuid import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from local_deep_research.database.models import Base from local_deep_research.database.models.library import ( Document, DocumentBlob, Collection, DocumentCollection, DocumentChunk, DownloadQueue, LibraryStatistics, RAGIndex, CollectionFolder, CollectionFolderFile, SourceType, UploadBatch, DocumentStatus, RAGIndexStatus, EmbeddingProvider, ) @pytest.fixture def engine(): """Create in-memory SQLite engine.""" engine = create_engine("sqlite:///:memory:", echo=False) Base.metadata.create_all(engine) yield engine engine.dispose() @pytest.fixture def session(engine): """Create database session.""" Session = sessionmaker(bind=engine) session = Session() yield session session.close() @pytest.fixture def source_type(session): """Create a source type for testing.""" st = SourceType( id=str(uuid.uuid4()), name="research_download", display_name="Research Download", description="Downloaded from research sources", ) session.add(st) session.commit() return st @pytest.fixture def collection(session): """Create a collection for testing.""" coll = Collection( id=str(uuid.uuid4()), name="Test Collection", description="A test collection", collection_type="user_collection", is_default=False, ) session.add(coll) session.commit() return coll class TestDocumentModel: """Tests for Document model.""" def test_create_document(self, session, source_type): """Should create a document.""" doc = Document( id=str(uuid.uuid4()), source_type_id=source_type.id, document_hash="abc123def456" * 5 + "ab", # 64 chars file_size=1024, file_type="pdf", status=DocumentStatus.COMPLETED, ) session.add(doc) session.commit() assert doc.id is not None assert doc.file_size == 1024 def test_document_with_all_fields(self, session, source_type): """Should create document with all optional fields.""" doc = Document( id=str(uuid.uuid4()), source_type_id=source_type.id, document_hash="xyz789" * 10 + "abcd", file_size=2048, file_type="pdf", status=DocumentStatus.COMPLETED, title="Test Paper", description="A test paper description", authors=["Author One", "Author Two"], doi="10.1234/test.doi", arxiv_id="2301.00001", text_content="This is the paper content...", extraction_method="pdf_extraction", extraction_source="pdfplumber", extraction_quality="high", tags=["machine-learning", "nlp"], ) session.add(doc) session.commit() retrieved = session.query(Document).filter_by(id=doc.id).first() assert retrieved.title == "Test Paper" assert retrieved.authors == ["Author One", "Author Two"] def test_document_status_enum(self, session, source_type): """Document status should use enum values.""" doc = Document( id=str(uuid.uuid4()), source_type_id=source_type.id, document_hash="status123" * 7 + "a", file_size=512, file_type="txt", status=DocumentStatus.PENDING, ) session.add(doc) session.commit() assert doc.status == DocumentStatus.PENDING def test_document_unique_hash(self, session, source_type): """Document hash should be unique.""" hash_value = "unique_hash" * 5 + "abcd" doc1 = Document( id=str(uuid.uuid4()), source_type_id=source_type.id, document_hash=hash_value, file_size=100, file_type="pdf", status=DocumentStatus.COMPLETED, ) session.add(doc1) session.commit() doc2 = Document( id=str(uuid.uuid4()), source_type_id=source_type.id, document_hash=hash_value, # Same hash file_size=200, file_type="pdf", status=DocumentStatus.COMPLETED, ) session.add(doc2) with pytest.raises(Exception): # IntegrityError session.commit() def test_document_repr(self, session, source_type): """Document __repr__ should work.""" doc = Document( id=str(uuid.uuid4()), source_type_id=source_type.id, document_hash="repr_test" * 8, file_size=100, file_type="pdf", title="Repr Test", status=DocumentStatus.COMPLETED, ) repr_str = repr(doc) assert "Document" in repr_str class TestCollectionModel: """Tests for Collection model.""" def test_create_collection(self, session): """Should create a collection.""" coll = Collection( id=str(uuid.uuid4()), name="My Collection", description="Test description", ) session.add(coll) session.commit() assert coll.id is not None assert coll.name == "My Collection" def test_collection_default_values(self, session): """Collection should have correct defaults.""" coll = Collection( id=str(uuid.uuid4()), name="Default Test", ) session.add(coll) session.commit() assert coll.is_default is False assert coll.collection_type == "user_collection" def test_collection_with_embedding_config(self, session): """Collection can store embedding configuration.""" coll = Collection( id=str(uuid.uuid4()), name="Embedding Collection", embedding_model="all-MiniLM-L6-v2", embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS, embedding_dimension=384, chunk_size=512, chunk_overlap=50, ) session.add(coll) session.commit() retrieved = session.query(Collection).filter_by(id=coll.id).first() assert retrieved.embedding_model == "all-MiniLM-L6-v2" assert retrieved.embedding_dimension == 384 def test_collection_repr(self, session): """Collection __repr__ should work.""" coll = Collection( id="test-id", name="Repr Test", collection_type="user_collection", ) repr_str = repr(coll) assert "Collection" in repr_str class TestDocumentCollectionModel: """Tests for DocumentCollection many-to-many model.""" def test_link_document_to_collection( self, session, source_type, collection ): """Should link document to collection.""" doc = Document( id=str(uuid.uuid4()), source_type_id=source_type.id, document_hash="link_test" * 8, file_size=100, file_type="pdf", status=DocumentStatus.COMPLETED, ) session.add(doc) session.commit() link = DocumentCollection( document_id=doc.id, collection_id=collection.id, indexed=False, chunk_count=0, ) session.add(link) session.commit() assert link.id is not None def test_document_collection_unique_pair( self, session, source_type, collection ): """Document-collection pair should be unique.""" doc = Document( id=str(uuid.uuid4()), source_type_id=source_type.id, document_hash="unique_pair" * 6 + "ab", file_size=100, file_type="pdf", status=DocumentStatus.COMPLETED, ) session.add(doc) session.commit() link1 = DocumentCollection( document_id=doc.id, collection_id=collection.id, ) session.add(link1) session.commit() link2 = DocumentCollection( document_id=doc.id, collection_id=collection.id, # Same pair ) session.add(link2) with pytest.raises(Exception): # IntegrityError session.commit() def test_document_collection_indexing_status( self, session, source_type, collection ): """Should track indexing status per collection.""" doc = Document( id=str(uuid.uuid4()), source_type_id=source_type.id, document_hash="index_status" * 6 + "12", file_size=100, file_type="pdf", status=DocumentStatus.COMPLETED, ) session.add(doc) session.commit() link = DocumentCollection( document_id=doc.id, collection_id=collection.id, indexed=True, chunk_count=25, ) session.add(link) session.commit() retrieved = ( session.query(DocumentCollection) .filter_by(document_id=doc.id) .first() ) assert retrieved.indexed is True assert retrieved.chunk_count == 25 class TestDocumentChunkModel: """Tests for DocumentChunk model.""" def test_create_document_chunk(self, session): """Should create a document chunk.""" chunk = DocumentChunk( chunk_hash="chunk_hash" * 6 + "ab", source_type="document", source_id=str(uuid.uuid4()), collection_name="collection_abc123", chunk_text="This is the chunk text content.", chunk_index=0, start_char=0, end_char=31, word_count=6, embedding_id=str(uuid.uuid4()), embedding_model="all-MiniLM-L6-v2", embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS, ) session.add(chunk) session.commit() assert chunk.id is not None def test_chunk_unique_per_collection(self, session): """Chunk hash should be unique per collection.""" chunk_hash = "duplicate_hash" * 5 chunk1 = DocumentChunk( chunk_hash=chunk_hash, source_type="document", collection_name="collection_1", chunk_text="Content 1", chunk_index=0, start_char=0, end_char=10, word_count=2, embedding_id=str(uuid.uuid4()), embedding_model="model", embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS, ) session.add(chunk1) session.commit() # Same hash, same collection should fail chunk2 = DocumentChunk( chunk_hash=chunk_hash, source_type="document", collection_name="collection_1", # Same collection chunk_text="Content 2", chunk_index=1, start_char=10, end_char=20, word_count=2, embedding_id=str(uuid.uuid4()), embedding_model="model", embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS, ) session.add(chunk2) with pytest.raises(Exception): session.commit() def test_chunk_repr(self, session): """DocumentChunk __repr__ should work.""" chunk = DocumentChunk( chunk_hash="repr_test" * 8, source_type="document", collection_name="test_collection", chunk_text="Test content", chunk_index=5, start_char=100, end_char=200, word_count=10, embedding_id=str(uuid.uuid4()), embedding_model="model", embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS, ) repr_str = repr(chunk) assert "DocumentChunk" in repr_str class TestRAGIndexModel: """Tests for RAGIndex model.""" def test_create_rag_index(self, session): """Should create a RAG index.""" index = RAGIndex( collection_name="collection_abc", embedding_model="all-MiniLM-L6-v2", embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS, embedding_dimension=384, index_path="/data/indexes/collection_abc.faiss", index_hash="index_hash" * 6 + "ab", chunk_size=512, chunk_overlap=50, status=RAGIndexStatus.ACTIVE, ) session.add(index) session.commit() assert index.id is not None def test_rag_index_status_transitions(self, session): """RAG index status can transition.""" index = RAGIndex( collection_name="status_test", embedding_model="model", embedding_model_type=EmbeddingProvider.OLLAMA, embedding_dimension=768, index_path="/path/to/index.faiss", index_hash="status_hash" * 6 + "ab", chunk_size=256, chunk_overlap=25, status=RAGIndexStatus.ACTIVE, ) session.add(index) session.commit() # Update status index.status = RAGIndexStatus.REBUILDING session.commit() retrieved = session.query(RAGIndex).filter_by(id=index.id).first() assert retrieved.status == RAGIndexStatus.REBUILDING def test_rag_index_repr(self, session): """RAGIndex __repr__ should work.""" index = RAGIndex( collection_name="repr_collection", embedding_model="test-model", embedding_model_type=EmbeddingProvider.SENTENCE_TRANSFORMERS, embedding_dimension=384, index_path="/path/index.faiss", index_hash="repr_hash" * 8, chunk_size=512, chunk_overlap=50, chunk_count=100, ) repr_str = repr(index) assert "RAGIndex" in repr_str class TestLibraryStatisticsModel: """Tests for LibraryStatistics model.""" def test_create_statistics(self, session): """Should create library statistics.""" stats = LibraryStatistics( total_documents=100, total_pdfs=80, total_html=15, total_other=5, total_size_bytes=1024000, average_document_size=10240, ) session.add(stats) session.commit() assert stats.id is not None def test_statistics_download_metrics(self, session): """Statistics should track download metrics.""" stats = LibraryStatistics( total_documents=50, total_download_attempts=100, successful_downloads=45, failed_downloads=5, pending_downloads=50, ) session.add(stats) session.commit() retrieved = ( session.query(LibraryStatistics).filter_by(id=stats.id).first() ) assert retrieved.total_download_attempts == 100 assert retrieved.successful_downloads == 45 def test_statistics_repr(self, session): """LibraryStatistics __repr__ should work.""" stats = LibraryStatistics( total_documents=50, total_size_bytes=500000, ) repr_str = repr(stats) assert "LibraryStatistics" in repr_str class TestDownloadQueueModel: """Tests for DownloadQueue model.""" def test_create_queue_item(self, session, collection): """Should create a download queue item.""" # Note: This requires a ResearchResource to exist # For now, test the model structure queue = DownloadQueue.__table__ columns = {c.name for c in queue.columns} assert "resource_id" in columns assert "research_id" in columns assert "priority" in columns assert "status" in columns assert "attempts" in columns class TestCollectionFolderModel: """Tests for CollectionFolder model.""" def test_create_collection_folder(self, session, collection): """Should create a collection folder link.""" folder = CollectionFolder( collection_id=collection.id, folder_path="/home/user/documents/research", include_patterns=["*.pdf", "*.txt"], recursive=True, ) session.add(folder) session.commit() assert folder.id is not None def test_folder_default_patterns(self, session, collection): """Folder should have default include patterns.""" folder = CollectionFolder( collection_id=collection.id, folder_path="/path/to/folder", ) session.add(folder) session.commit() # Default patterns should include common document types assert folder.include_patterns is not None def test_folder_repr(self, session, collection): """CollectionFolder __repr__ should work.""" folder = CollectionFolder( collection_id=collection.id, folder_path="/test/path", file_count=10, ) repr_str = repr(folder) assert "CollectionFolder" in repr_str class TestCollectionFolderFileModel: """Tests for CollectionFolderFile model.""" def test_create_folder_file(self, session, collection): """Should create a folder file entry.""" folder = CollectionFolder( collection_id=collection.id, folder_path="/test/folder", ) session.add(folder) session.commit() file = CollectionFolderFile( folder_id=folder.id, relative_path="subdir/document.pdf", file_hash="file_hash" * 8, file_size=2048, file_type="pdf", indexed=False, ) session.add(file) session.commit() assert file.id is not None def test_folder_file_unique_path(self, session, collection): """File path should be unique within folder.""" folder = CollectionFolder( collection_id=collection.id, folder_path="/unique/test", ) session.add(folder) session.commit() file1 = CollectionFolderFile( folder_id=folder.id, relative_path="same/path.pdf", ) session.add(file1) session.commit() file2 = CollectionFolderFile( folder_id=folder.id, relative_path="same/path.pdf", # Same path ) session.add(file2) with pytest.raises(Exception): session.commit() def test_folder_file_repr(self): """CollectionFolderFile __repr__ should work.""" file = CollectionFolderFile( relative_path="test/file.pdf", indexed=True, ) repr_str = repr(file) assert "CollectionFolderFile" in repr_str class TestSourceTypeModel: """Tests for SourceType model.""" def test_create_source_type(self, session): """Should create a source type.""" st = SourceType( id=str(uuid.uuid4()), name="user_upload", display_name="User Upload", description="Uploaded by user", icon="upload", ) session.add(st) session.commit() assert st.id is not None def test_source_type_unique_name(self, session): """Source type name should be unique.""" st1 = SourceType( id=str(uuid.uuid4()), name="unique_type", display_name="Unique Type", ) session.add(st1) session.commit() st2 = SourceType( id=str(uuid.uuid4()), name="unique_type", # Same name display_name="Another Unique Type", ) session.add(st2) with pytest.raises(Exception): session.commit() def test_source_type_repr(self): """SourceType __repr__ should work.""" st = SourceType( id="test-id", name="test_type", display_name="Test Type", ) repr_str = repr(st) assert "SourceType" in repr_str class TestDocumentBlobModel: """Tests for DocumentBlob model.""" def test_create_document_blob(self, session, source_type): """Should create a document blob.""" doc = Document( id=str(uuid.uuid4()), source_type_id=source_type.id, document_hash="blob_test" * 8, file_size=1000, file_type="pdf", status=DocumentStatus.COMPLETED, ) session.add(doc) session.commit() blob = DocumentBlob( document_id=doc.id, pdf_binary=b"PDF binary content here", blob_hash="binary_hash" * 6 + "ab", ) session.add(blob) session.commit() retrieved = ( session.query(DocumentBlob).filter_by(document_id=doc.id).first() ) assert retrieved.pdf_binary == b"PDF binary content here" def test_blob_repr(self, session, source_type): """DocumentBlob __repr__ should work.""" doc = Document( id="test-doc-id-" + "x" * 24, source_type_id=source_type.id, document_hash="repr_blob" * 8, file_size=100, file_type="pdf", status=DocumentStatus.COMPLETED, ) blob = DocumentBlob( document_id=doc.id, pdf_binary=b"test", ) repr_str = repr(blob) assert "DocumentBlob" in repr_str @pytest.mark.filterwarnings( "ignore:UploadBatch is deprecated.*:DeprecationWarning" ) class TestUploadBatchModel: """Tests for UploadBatch model (deprecated — see PR 11a).""" def test_create_upload_batch(self, session, collection): """Should create an upload batch.""" batch = UploadBatch( id=str(uuid.uuid4()), collection_id=collection.id, file_count=5, total_size=10240, ) session.add(batch) session.commit() assert batch.id is not None assert batch.file_count == 5 def test_batch_repr(self): """UploadBatch __repr__ should work.""" batch = UploadBatch( id="test-batch-id", file_count=3, total_size=5000, ) repr_str = repr(batch) assert "UploadBatch" in repr_str