Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

771 lines
23 KiB
Python

"""
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