"""Tests for transcription.models — Document, Job, Transcript persistence and relationships.""" from uuid import UUID import pytest from sqlalchemy.exc import IntegrityError from transcription.models import Document from transcription.models import Job from transcription.models import JobStatus from transcription.models import Transcript 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, revision=0, provider="openrouter", model="google/gemini-2.5-flash", 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, revision=0, provider="openrouter", model="google/gemini-2.5-flash", 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_revision_pair_is_unique(self, session): """Duplicate job_id and revision combinations raise an integrity error.""" doc = _persist_document(session) job = _persist_job(session, doc) session.add( Transcript( job_id=job.id, revision=0, provider="openrouter", model="google/gemini-2.5-flash", prompt_name="transcribe_document.md", text="First", ) ) session.add( Transcript( job_id=job.id, revision=1, provider="openrouter", model="google/gemini-2.5-flash", prompt_name="transcribe_document.md", text="Second", ) ) session.commit() session.add( Transcript( job_id=job.id, revision=1, provider="openrouter", model="google/gemini-2.5-flash", prompt_name="transcribe_document.md", text="Duplicate", ) ) with pytest.raises(IntegrityError): session.commit() session.rollback() 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_transcripts(self, session): """job.transcripts returns linked Transcript history.""" doc = _persist_document(session) job = _persist_job(session, doc) session.add( Transcript( job_id=job.id, revision=0, provider="openrouter", model="google/gemini-2.5-flash", prompt_name="transcribe_document.md", text="Transcribed text", ) ) session.add( Transcript( job_id=job.id, revision=1, provider="openrouter", model="google/gemini-2.5-flash", prompt_name="transcribe_document.md", text="Transcribed text v2", ) ) session.commit() session.refresh(job) assert len(job.transcripts) == 2 assert all(isinstance(t, Transcript) for t in job.transcripts)