generated from john/python-template
UI update complete?
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import DocumentPersonRole
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentDeleteBlockedError
|
||||
from transcription.services.documents import DocumentError
|
||||
from transcription.services.documents import PersonDeleteBlockedError
|
||||
from transcription.services.documents import DocumentService
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_document_detail_allows_missing_sources(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
created = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="detail-doc",
|
||||
document_type="letter",
|
||||
)
|
||||
)
|
||||
|
||||
detail = await service.read_document_detail(created.id)
|
||||
|
||||
assert detail.id == created.id
|
||||
assert detail.sources == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_document_refreshes_updated_timestamp(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
created = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="timestamp-doc",
|
||||
document_type="letter",
|
||||
updated_at=datetime(2000, 1, 1, tzinfo=UTC),
|
||||
)
|
||||
)
|
||||
|
||||
original_updated_at = created.updated_at
|
||||
created.notes = "updated"
|
||||
|
||||
updated = await service.update_document(created)
|
||||
|
||||
assert updated.notes == "updated"
|
||||
assert updated.updated_at >= original_updated_at
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_document_blocks_when_dependencies_exist(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
document = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="blocked-delete",
|
||||
document_type="record",
|
||||
)
|
||||
)
|
||||
|
||||
async with service._session_scope() as session:
|
||||
session.add(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="001_page.png",
|
||||
filename="001_page.png",
|
||||
file_path="uploads/001_page.png",
|
||||
)
|
||||
)
|
||||
session.add(Job(document_id=document.id))
|
||||
await session.commit()
|
||||
|
||||
with pytest.raises(DocumentDeleteBlockedError):
|
||||
await service.delete_document(document)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_document_succeeds_when_unlinked(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
document = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="free-delete",
|
||||
document_type="memo",
|
||||
)
|
||||
)
|
||||
|
||||
await service.delete_document(document)
|
||||
|
||||
with pytest.raises(DocumentError):
|
||||
await service.read_document_detail(document.id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_person_detail_loads_document_links(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
document = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="linked-doc",
|
||||
document_type="letter",
|
||||
)
|
||||
)
|
||||
person = await service.create_person(Person(full_name="Linked Person"))
|
||||
await service.create_document_person(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
)
|
||||
)
|
||||
|
||||
detail = await service.read_person_detail(person.id)
|
||||
|
||||
assert detail.id == person.id
|
||||
assert len(detail.document_people) == 1
|
||||
assert detail.document_people[0].document is not None
|
||||
assert detail.document_people[0].document.name == "linked-doc"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_person_refreshes_updated_timestamp(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
created = await service.create_person(
|
||||
Person(
|
||||
full_name="timestamp-person",
|
||||
updated_at=datetime(2000, 1, 1, tzinfo=UTC),
|
||||
)
|
||||
)
|
||||
original_updated_at = created.updated_at
|
||||
created.display_name = "updated"
|
||||
|
||||
updated = await service.update_person(created)
|
||||
|
||||
assert updated.display_name == "updated"
|
||||
assert updated.updated_at >= original_updated_at
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_person_blocks_when_linked_documents_exist(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
document = await service.create_document(
|
||||
Document(
|
||||
id=uuid4(),
|
||||
name="block-person-delete-doc",
|
||||
document_type="record",
|
||||
)
|
||||
)
|
||||
person = await service.create_person(Person(full_name="Blocked Person"))
|
||||
await service.create_document_person(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(PersonDeleteBlockedError):
|
||||
await service.delete_person(person)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_person_succeeds_when_unlinked(default_session_factory):
|
||||
service = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
person = await service.create_person(Person(full_name="Free Person"))
|
||||
|
||||
await service.delete_person(person)
|
||||
|
||||
with pytest.raises(DocumentError):
|
||||
await service.read_person_detail(person.id)
|
||||
@@ -9,6 +9,7 @@ from transcription.db.models import JobSourceStatus
|
||||
from transcription.db.models import JobStatus
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentService
|
||||
from transcription.services.jobs import JobDeleteBlockedError
|
||||
from transcription.services.jobs import JobService
|
||||
|
||||
|
||||
@@ -107,3 +108,111 @@ class TestJobService:
|
||||
next_job = await job_service.read_next_queued_job()
|
||||
assert next_job is not None
|
||||
assert next_job.id == first.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_job_persists_provider_model_prompt(
|
||||
self,
|
||||
job_service: JobService,
|
||||
document_service: DocumentService,
|
||||
):
|
||||
document = Document(id=uuid4(), name="provider-doc")
|
||||
await document_service.create_document(document=document)
|
||||
|
||||
job = Job(
|
||||
document_id=document.id,
|
||||
provider="openrouter",
|
||||
model="google/gemini-2.5-flash",
|
||||
prompt_name="transcribe_document.md",
|
||||
)
|
||||
await job_service.create_job(job=job)
|
||||
|
||||
fetched = await job_service.read_job(job_id=job.id)
|
||||
assert fetched.provider == "openrouter"
|
||||
assert fetched.model == "google/gemini-2.5-flash"
|
||||
assert fetched.prompt_name == "transcribe_document.md"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_job_resolves_filename_from_linked_source(
|
||||
self,
|
||||
job_service: JobService,
|
||||
document_service: DocumentService,
|
||||
):
|
||||
document = Document(id=uuid4(), name="filename-doc")
|
||||
await document_service.create_document(document=document)
|
||||
|
||||
job = Job(document_id=document.id)
|
||||
await job_service.create_job(job=job)
|
||||
|
||||
async with job_service._session_scope() as session:
|
||||
source = Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="page_001.png",
|
||||
filename="stored_page_001.png",
|
||||
file_path="/uploads/stored_page_001.png",
|
||||
)
|
||||
session.add(source)
|
||||
await session.flush()
|
||||
|
||||
session.add(
|
||||
JobSource(
|
||||
job_id=job.id,
|
||||
source_id=source.id,
|
||||
status=JobSourceStatus.PENDING,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
fetched = await job_service.read_job(job_id=job.id)
|
||||
assert fetched.filename == "stored_page_001.png"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_job_with_guardrails_blocks_processing_jobs(
|
||||
self,
|
||||
job_service: JobService,
|
||||
document_service: DocumentService,
|
||||
):
|
||||
document = Document(id=uuid4(), name="processing-delete-doc")
|
||||
await document_service.create_document(document=document)
|
||||
|
||||
job = Job(document_id=document.id, status=JobStatus.PROCESSING)
|
||||
await job_service.create_job(job=job)
|
||||
|
||||
with pytest.raises(JobDeleteBlockedError):
|
||||
await job_service.delete_job_with_guardrails(job_id=job.id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_job_with_guardrails_removes_jobsource_links(
|
||||
self,
|
||||
job_service: JobService,
|
||||
document_service: DocumentService,
|
||||
):
|
||||
document = Document(id=uuid4(), name="delete-job-doc")
|
||||
await document_service.create_document(document=document)
|
||||
|
||||
job = Job(document_id=document.id, status=JobStatus.QUEUED)
|
||||
await job_service.create_job(job=job)
|
||||
|
||||
async with job_service._session_scope() as session:
|
||||
source = Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="delete-job-source.jpg",
|
||||
filename="stored-delete-job-source.jpg",
|
||||
file_path="/uploads/stored-delete-job-source.jpg",
|
||||
)
|
||||
session.add(source)
|
||||
await session.flush()
|
||||
session.add(
|
||||
JobSource(
|
||||
job_id=job.id,
|
||||
source_id=source.id,
|
||||
status=JobSourceStatus.PENDING,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
await job_service.delete_job_with_guardrails(job_id=job.id)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await job_service.read_job(job_id=job.id)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from sqlmodel import select
|
||||
|
||||
from transcription.config import Settings
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobSource
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.store import UploadError
|
||||
from transcription.services.store import create_job_for_document
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_job_for_document_requires_at_least_one_upload(async_session, tmp_path):
|
||||
document = Document(id=uuid4(), name="needs-upload")
|
||||
async_session.add(document)
|
||||
await async_session.commit()
|
||||
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
|
||||
with pytest.raises(UploadError):
|
||||
await create_job_for_document(
|
||||
document_id=document.id,
|
||||
uploads=[],
|
||||
session=async_session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_job_for_document_sorts_uploads_and_creates_links(async_session, tmp_path):
|
||||
document = Document(id=uuid4(), name="ordered-upload-doc")
|
||||
async_session.add(document)
|
||||
await async_session.commit()
|
||||
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
|
||||
result = await create_job_for_document(
|
||||
document_id=document.id,
|
||||
uploads=[
|
||||
("folder/b_page.pdf", b"b"),
|
||||
("folder/A_page.pdf", b"a"),
|
||||
],
|
||||
provider="openrouter",
|
||||
model="test-model",
|
||||
prompt_name="transcribe_document.md",
|
||||
session=async_session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
created_job = await async_session.get(Job, result.job_id)
|
||||
assert created_job is not None
|
||||
assert created_job.provider == "openrouter"
|
||||
assert created_job.model == "test-model"
|
||||
assert created_job.prompt_name == "transcribe_document.md"
|
||||
|
||||
sources = (
|
||||
await async_session.exec(
|
||||
select(Source)
|
||||
.where(Source.document_id == document.id)
|
||||
.order_by(Source.page_number) # pyright: ignore[reportArgumentType]
|
||||
)
|
||||
).all()
|
||||
assert [source.upload_name for source in sources] == ["A_page.pdf", "b_page.pdf"]
|
||||
assert all(source.filename.endswith(".pdf") for source in sources)
|
||||
assert all("A_page" not in source.filename and "b_page" not in source.filename for source in sources)
|
||||
|
||||
job_sources = (await async_session.exec(select(JobSource).where(JobSource.job_id == result.job_id))).all()
|
||||
assert len(job_sources) == 2
|
||||
assert set(result.source_ids) == {job_source.source_id for job_source in job_sources}
|
||||
@@ -12,6 +12,8 @@ from transcription.db.models import JobStatus
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentService
|
||||
from transcription.services.jobs import JobService
|
||||
from transcription.services.transcription import SourceDeleteBlockedError
|
||||
from transcription.services.transcription import TranscriptionNotFoundError
|
||||
from transcription.services.transcription import TranscriptionService
|
||||
|
||||
|
||||
@@ -89,3 +91,66 @@ class TestTranscriptionServiceRevisionUpsert:
|
||||
assert len(revisions) == 1
|
||||
assert revisions[0].id == first.id
|
||||
assert revisions[0].revised_text == "Revision v2"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_source_from_job_context_removes_source_and_single_link(self, default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
jobs = JobService(session_factory=default_session_factory)
|
||||
transcriptions = TranscriptionService(session_factory=default_session_factory)
|
||||
|
||||
document = Document(id=uuid4(), name="delete-source-success")
|
||||
await documents.create_document(document=document)
|
||||
|
||||
job = Job(document_id=document.id, status=JobStatus.QUEUED)
|
||||
await jobs.create_job(job=job)
|
||||
|
||||
source = Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="delete.jpg",
|
||||
filename="delete.jpg",
|
||||
file_path="uploads/delete.jpg",
|
||||
)
|
||||
async with transcriptions._session_scope() as session:
|
||||
session.add(source)
|
||||
await session.flush()
|
||||
session.add(JobSource(job_id=job.id, source_id=source.id, status=JobSourceStatus.PENDING))
|
||||
await session.commit()
|
||||
await session.refresh(source)
|
||||
|
||||
await transcriptions.delete_source_from_job_context(job_id=job.id, source_id=source.id)
|
||||
|
||||
with pytest.raises(TranscriptionNotFoundError):
|
||||
await transcriptions.read_source(source.id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_source_from_job_context_blocks_when_other_job_links_exist(self, default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
jobs = JobService(session_factory=default_session_factory)
|
||||
transcriptions = TranscriptionService(session_factory=default_session_factory)
|
||||
|
||||
document = Document(id=uuid4(), name="delete-source-blocked")
|
||||
await documents.create_document(document=document)
|
||||
|
||||
job_one = Job(document_id=document.id, status=JobStatus.QUEUED)
|
||||
job_two = Job(document_id=document.id, status=JobStatus.QUEUED)
|
||||
await jobs.create_job(job=job_one)
|
||||
await jobs.create_job(job=job_two)
|
||||
|
||||
source = Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="shared.jpg",
|
||||
filename="shared.jpg",
|
||||
file_path="uploads/shared.jpg",
|
||||
)
|
||||
async with transcriptions._session_scope() as session:
|
||||
session.add(source)
|
||||
await session.flush()
|
||||
session.add(JobSource(job_id=job_one.id, source_id=source.id, status=JobSourceStatus.PENDING))
|
||||
session.add(JobSource(job_id=job_two.id, source_id=source.id, status=JobSourceStatus.PENDING))
|
||||
await session.commit()
|
||||
await session.refresh(source)
|
||||
|
||||
with pytest.raises(SourceDeleteBlockedError):
|
||||
await transcriptions.delete_source_from_job_context(job_id=job_one.id, source_id=source.id)
|
||||
|
||||
@@ -10,8 +10,10 @@ from transcription.db.models import JobSource
|
||||
from transcription.db.models import JobSourceStatus
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import Source
|
||||
from transcription.services.documents import DocumentDeleteBlockedError
|
||||
from transcription.services.documents import DocumentService
|
||||
from transcription.services.jobs import JobService
|
||||
from transcription.services.transcription import SourceDeleteBlockedError
|
||||
from transcription.services.transcription import TranscriptionService
|
||||
|
||||
|
||||
@@ -123,3 +125,89 @@ async def test_transcription_service_job_source_crud_uses_caller_session(default
|
||||
await session.commit()
|
||||
|
||||
assert len(await transcriptions.list_job_sources(job_id=job.id)) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_detail_loads_linked_person_relationship(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
|
||||
document = await documents.create_document(Document(id=uuid4(), name="detail-person-doc"))
|
||||
person = await documents.create_person(Person(full_name="Grace Hopper"))
|
||||
await documents.create_document_person(
|
||||
DocumentPerson(
|
||||
document_id=document.id,
|
||||
person_id=person.id,
|
||||
role=DocumentPersonRole.AUTHOR,
|
||||
)
|
||||
)
|
||||
|
||||
detail = await documents.read_document_detail(document.id)
|
||||
|
||||
assert len(detail.document_people) == 1
|
||||
link = detail.document_people[0]
|
||||
assert link.person is not None
|
||||
assert link.person.full_name == "Grace Hopper"
|
||||
assert link.role == DocumentPersonRole.AUTHOR
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_delete_is_blocked_with_source_and_job_dependencies(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
jobs = JobService(session_factory=default_session_factory)
|
||||
transcriptions = TranscriptionService(session_factory=default_session_factory)
|
||||
|
||||
document = await documents.create_document(Document(id=uuid4(), name="blocked-by-deps"))
|
||||
job = await jobs.create_job(Job(document_id=document.id))
|
||||
source = await transcriptions.create_source(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="blocked.jpg",
|
||||
filename="blocked.jpg",
|
||||
file_path="uploads/blocked.jpg",
|
||||
)
|
||||
)
|
||||
await transcriptions.create_job_source(
|
||||
JobSource(
|
||||
job_id=job.id,
|
||||
source_id=source.id,
|
||||
status=JobSourceStatus.PENDING,
|
||||
)
|
||||
)
|
||||
|
||||
with pytest.raises(DocumentDeleteBlockedError) as exc_info:
|
||||
await documents.delete_document(document)
|
||||
|
||||
message = exc_info.value.message
|
||||
assert "Sources" in message
|
||||
assert "Jobs" in message
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_source_delete_blocks_when_linked_to_multiple_jobs(default_session_factory):
|
||||
documents = DocumentService(session_factory=default_session_factory)
|
||||
jobs = JobService(session_factory=default_session_factory)
|
||||
transcriptions = TranscriptionService(session_factory=default_session_factory)
|
||||
|
||||
document = await documents.create_document(Document(id=uuid4(), name="multi-job-source-doc"))
|
||||
job_one = await jobs.create_job(Job(document_id=document.id))
|
||||
job_two = await jobs.create_job(Job(document_id=document.id))
|
||||
|
||||
source = await transcriptions.create_source(
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="shared-page.jpg",
|
||||
filename="shared-page.jpg",
|
||||
file_path="uploads/shared-page.jpg",
|
||||
)
|
||||
)
|
||||
await transcriptions.create_job_source(
|
||||
JobSource(job_id=job_one.id, source_id=source.id, status=JobSourceStatus.PENDING)
|
||||
)
|
||||
await transcriptions.create_job_source(
|
||||
JobSource(job_id=job_two.id, source_id=source.id, status=JobSourceStatus.PENDING)
|
||||
)
|
||||
|
||||
with pytest.raises(SourceDeleteBlockedError):
|
||||
await transcriptions.delete_source_from_job_context(job_id=job_one.id, source_id=source.id)
|
||||
|
||||
Reference in New Issue
Block a user