131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
"""PostgreSQL integration tests for atomic document publication."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
|
|
|
from ragcore.application.documents import (
|
|
ArtifactRecord,
|
|
ChunkRecord,
|
|
DocumentPublicationService,
|
|
DocumentRecord,
|
|
DocumentVersionRecord,
|
|
DocumentVersionState,
|
|
)
|
|
from ragcore.application.sources import implicit_source_record
|
|
from ragcore.config import Settings
|
|
from ragcore.infrastructure.database import (
|
|
create_database_engine,
|
|
create_session_factory,
|
|
)
|
|
from ragcore.infrastructure.database.content import PostgresDocumentRepository
|
|
from ragcore.infrastructure.database.sources import PostgresSourceRegistry
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def publication_context() -> tuple[
|
|
PostgresDocumentRepository, async_sessionmaker[AsyncSession]
|
|
]:
|
|
engine = create_database_engine(Settings(env="test"))
|
|
factory = create_session_factory(engine)
|
|
try:
|
|
yield PostgresDocumentRepository(factory), factory
|
|
finally:
|
|
await engine.dispose()
|
|
|
|
|
|
async def _register_source(factory, document) -> None: # type: ignore[no-untyped-def]
|
|
"""The content schema requires a registry row for every document's source."""
|
|
|
|
await PostgresSourceRegistry(factory).ensure_source(
|
|
implicit_source_record(
|
|
source_id=document.source_id,
|
|
workspace_id=document.workspace_id,
|
|
space_id=document.space_id,
|
|
)
|
|
)
|
|
|
|
|
|
def version(document_id, state: DocumentVersionState, *, fingerprint: str) -> DocumentVersionRecord:
|
|
return DocumentVersionRecord(
|
|
id=uuid4(),
|
|
document_id=document_id,
|
|
source_fingerprint=fingerprint,
|
|
content_hash=fingerprint,
|
|
parser_version="parser-v1",
|
|
chunk_profile_version="prose-v1",
|
|
state=state,
|
|
)
|
|
|
|
|
|
@pytest.mark.integration
|
|
async def test_publication_supersedes_previous_and_is_idempotent(
|
|
publication_context: tuple[PostgresDocumentRepository, async_sessionmaker[AsyncSession]],
|
|
) -> None:
|
|
repository, factory = publication_context
|
|
document = DocumentRecord(uuid4(), uuid4(), uuid4(), "source-1", f"doc-{uuid4()}", "Runbook")
|
|
await _register_source(factory, document)
|
|
await repository.create_document(document)
|
|
first = version(document.id, DocumentVersionState.VALIDATED, fingerprint="1" * 64)
|
|
second = version(document.id, DocumentVersionState.VALIDATED, fingerprint="2" * 64)
|
|
await repository.create_version(first)
|
|
await repository.create_version(second)
|
|
service = DocumentPublicationService(repository)
|
|
artifact = ArtifactRecord(uuid4(), second.id, "canonical", "2" * 64, "parsed/key", 10)
|
|
chunk = ChunkRecord(uuid4(), second.id, "/blocks/0000", "3" * 64, "prose-v1", "safe", 1)
|
|
|
|
await service.publish_validated(first.id, artifacts=(), chunks=())
|
|
await service.publish_validated(second.id, artifacts=(artifact,), chunks=(chunk,))
|
|
await service.publish_validated(second.id, artifacts=(artifact,), chunks=(chunk,))
|
|
|
|
assert (await repository.list_searchable_versions(document.id)) == (
|
|
await repository.get_version(second.id),
|
|
)
|
|
assert (await repository.get_version(first.id)).state is DocumentVersionState.SUPERSEDED
|
|
|
|
|
|
@pytest.mark.integration
|
|
async def test_failed_version_does_not_replace_previous_active_version(
|
|
publication_context: tuple[PostgresDocumentRepository, async_sessionmaker[AsyncSession]],
|
|
) -> None:
|
|
repository, factory = publication_context
|
|
document = DocumentRecord(uuid4(), uuid4(), uuid4(), "source-2", f"doc-{uuid4()}", "Stable")
|
|
await _register_source(factory, document)
|
|
await repository.create_document(document)
|
|
active = version(document.id, DocumentVersionState.VALIDATED, fingerprint="4" * 64)
|
|
failed = version(document.id, DocumentVersionState.FAILED, fingerprint="5" * 64)
|
|
await repository.create_version(active)
|
|
await repository.create_version(failed)
|
|
service = DocumentPublicationService(repository)
|
|
await service.publish_validated(active.id, artifacts=(), chunks=())
|
|
|
|
with pytest.raises(RuntimeError):
|
|
await service.publish_validated(failed.id, artifacts=(), chunks=())
|
|
|
|
assert len(await repository.list_searchable_versions(document.id)) == 1
|
|
assert (await repository.list_searchable_versions(document.id))[0].id == active.id
|
|
|
|
|
|
@pytest.mark.integration
|
|
async def test_source_deletion_retires_active_version_immediately(
|
|
publication_context: tuple[PostgresDocumentRepository, async_sessionmaker[AsyncSession]],
|
|
) -> None:
|
|
repository, factory = publication_context
|
|
document = DocumentRecord(uuid4(), uuid4(), uuid4(), "source-3", f"doc-{uuid4()}", "Deleted")
|
|
await _register_source(factory, document)
|
|
await repository.create_document(document)
|
|
active = version(document.id, DocumentVersionState.VALIDATED, fingerprint="6" * 64)
|
|
await repository.create_version(active)
|
|
await DocumentPublicationService(repository).publish_validated(
|
|
active.id, artifacts=(), chunks=()
|
|
)
|
|
|
|
await DocumentPublicationService(repository).mark_source_deleted(document.id)
|
|
|
|
assert await repository.list_searchable_versions(document.id) == ()
|
|
assert (await repository.get_version(active.id)).state is DocumentVersionState.RETIRED
|