"""Tests for transcription.models — Document, Job, Transcript, TranscriptRevision models.""" from uuid import UUID import pytest from sqlalchemy.exc import IntegrityError from transcription.models import Document, Job, JobStatus, Transcript, TranscriptRevision def _make_document(**overrides) -> Document: """Create a Document with sensible defaults.""" defaults = {"filename": "letter.jpg", "file_path": "/uploads/letter.jpg"} defaults.update(overrides) return Document(**defaults) def _persist_document(session) -> Document: """Create, persist, and return a Document.""" doc = _make_document() session.add(doc) session.commit() session.refresh(doc) return doc def _persist_job(session, document: Document) -> Job: """Create, persist, and return a Job linked to a Document.""" job = Job(document_id=document.id) session.add(job) session.commit() session.refresh(job) return job class TestDocumentModel: """Verify Document creation and default field population.""" def test_can_be_persisted(self, session): """A Document round-trips through the database with correct fields.""" doc = _persist_document(session) fetched = session.get(Document, doc.id) assert fetched is not None assert fetched.filename == "letter.jpg" assert fetched.file_path == "/uploads/letter.jpg" def test_defaults_are_populated(self, session): """id is a UUID and uploaded_at is populated on creation.""" doc = _persist_document(session) assert isinstance(doc.id, UUID) assert doc.uploaded_at is not None class TestJobModel: """Verify Job creation, defaults, and status transitions.""" def test_can_be_created_for_document(self, session): """A Job linked to a Document via FK persists correctly.""" doc = _persist_document(session) job = _persist_job(session, doc) fetched = session.get(Job, job.id) assert fetched is not None assert fetched.document_id == doc.id def test_defaults_are_populated(self, session): """Default status is queued; created_at and updated_at are populated.""" doc = _persist_document(session) job = _persist_job(session, doc) assert job.status == JobStatus.QUEUED assert job.retry_count == 0 assert job.created_at is not None assert job.updated_at is not None def test_transitions_to_transcribed(self, session): """Status updates from queued to processing to transcribed.""" doc = _persist_document(session) job = _persist_job(session, doc) assert job.status == JobStatus.QUEUED job.status = JobStatus.PROCESSING session.add(job) session.commit() session.refresh(job) assert job.status == JobStatus.PROCESSING job.status = JobStatus.TRANSCRIBED session.add(job) session.commit() session.refresh(job) assert job.status == JobStatus.TRANSCRIBED def test_transitions_to_failed(self, session): """Status updates from processing to failed.""" doc = _persist_document(session) job = _persist_job(session, doc) job.status = JobStatus.PROCESSING session.add(job) session.commit() session.refresh(job) job.status = JobStatus.FAILED session.add(job) session.commit() session.refresh(job) assert job.status == JobStatus.FAILED class TestTranscriptModel: """Verify Transcript persistence for success and failure cases.""" def test_success_record_persists(self, session): """A Transcript with text set and error_detail None persists correctly.""" doc = _persist_document(session) job = _persist_job(session, doc) transcript = Transcript(job_id=job.id, provider="openrouter", prompt_name="transcribe_document.md", text="Dear Sir, ...") session.add(transcript) session.commit() session.refresh(transcript) fetched = session.get(Transcript, transcript.id) assert fetched is not None assert fetched.text == "Dear Sir, ..." assert fetched.error_detail is None def test_failure_record_persists(self, session): """A Transcript with text None and error_detail set persists correctly.""" doc = _persist_document(session) job = _persist_job(session, doc) transcript = Transcript( job_id=job.id, provider="openrouter", prompt_name="transcribe_document.md", error_detail="Provider timeout", ) session.add(transcript) session.commit() session.refresh(transcript) fetched = session.get(Transcript, transcript.id) assert fetched is not None assert fetched.text is None assert fetched.error_detail == "Provider timeout" def test_job_id_is_unique(self, session): """Inserting two transcripts with the same job_id raises an integrity error.""" doc = _persist_document(session) job = _persist_job(session, doc) t1 = Transcript(job_id=job.id, provider="openrouter", prompt_name="transcribe_document.md", text="First") session.add(t1) session.commit() t2 = Transcript(job_id=job.id, provider="openrouter", prompt_name="transcribe_document.md", text="Duplicate") session.add(t2) with pytest.raises(IntegrityError): session.commit() class TestTranscriptRevisionModel: """Verify TranscriptRevision persistence and version uniqueness constraints.""" def test_revision_record_persists(self, session): """A TranscriptRevision with version metadata persists correctly.""" doc = _persist_document(session) job = _persist_job(session, doc) revision = TranscriptRevision( job_id=job.id, version_number=1, provider="openrouter", prompt_name="transcribe_document.md", model="google/gemini-2.5-flash", source="ai", text="Initial text", ) session.add(revision) session.commit() session.refresh(revision) fetched = session.get(TranscriptRevision, revision.id) assert fetched is not None assert fetched.version_number == 1 assert fetched.text == "Initial text" assert fetched.source == "ai" def test_job_version_pair_is_unique(self, session): """Duplicate version_number for same job raises integrity error.""" doc = _persist_document(session) job = _persist_job(session, doc) first = TranscriptRevision( job_id=job.id, version_number=1, provider="openrouter", prompt_name="transcribe_document.md", source="ai", text="Initial", ) duplicate = TranscriptRevision( job_id=job.id, version_number=1, provider="openrouter", prompt_name="transcribe_document.md", source="user", text="Edited", ) session.add(first) session.commit() session.add(duplicate) with pytest.raises(IntegrityError): session.commit() def test_same_version_number_allowed_for_different_jobs(self, session): """Version numbers are scoped per job, not globally.""" doc1 = _persist_document(session) job1 = _persist_job(session, doc1) doc2 = _make_document(filename="letter2.jpg", file_path="/uploads/letter2.jpg") session.add(doc2) session.commit() session.refresh(doc2) job2 = _persist_job(session, doc2) r1 = TranscriptRevision( job_id=job1.id, version_number=1, provider="openrouter", prompt_name="transcribe_document.md", source="ai", text="Job1 v1", ) r2 = TranscriptRevision( job_id=job2.id, version_number=1, provider="openrouter", prompt_name="transcribe_document.md", source="ai", text="Job2 v1", ) session.add(r1) session.add(r2) session.commit() class TestRelationships: """Verify SQLModel relationship navigation between models.""" def test_document_exposes_jobs(self, session): """document.jobs returns the linked Job list.""" doc = _persist_document(session) _persist_job(session, doc) _persist_job(session, doc) session.refresh(doc) assert len(doc.jobs) == 2 assert all(isinstance(j, Job) for j in doc.jobs) def test_job_exposes_transcript(self, session): """job.transcript returns the linked Transcript.""" doc = _persist_document(session) job = _persist_job(session, doc) transcript = Transcript( job_id=job.id, provider="openrouter", prompt_name="transcribe_document.md", text="Transcribed text", ) session.add(transcript) session.commit() session.refresh(job) assert job.transcript is not None assert isinstance(job.transcript, Transcript) assert job.transcript.text == "Transcribed text" def test_job_exposes_transcript_revisions(self, session): """job.transcript_revisions returns all linked revisions.""" doc = _persist_document(session) job = _persist_job(session, doc) session.add( TranscriptRevision( job_id=job.id, version_number=1, provider="openrouter", prompt_name="transcribe_document.md", source="ai", text="v1", ) ) session.add( TranscriptRevision( job_id=job.id, version_number=2, provider="openrouter", prompt_name="transcribe_document.md", source="user", text="v2", ) ) session.commit() session.refresh(job) assert len(job.transcript_revisions) == 2