generated from john/python-template
Step 4 implemented
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""Tests for transcription.services.upload."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from transcription.config import Settings
|
||||
from transcription.models import Document, Job, JobStatus
|
||||
from transcription.services.upload import UploadError, create_upload_job
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestUploadValidation:
|
||||
"""Verify upload validation behavior."""
|
||||
|
||||
def test_rejects_empty_bytes(self, session, tmp_path: Path):
|
||||
"""create_upload_job rejects an empty upload payload."""
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
with pytest.raises(UploadError):
|
||||
create_upload_job(
|
||||
filename="letter.jpg",
|
||||
file_bytes=b"",
|
||||
session=session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
def test_rejects_unsupported_extension(self, session, tmp_path: Path):
|
||||
"""create_upload_job rejects unsupported filename extensions."""
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
with pytest.raises(UploadError):
|
||||
create_upload_job(
|
||||
filename="notes.txt",
|
||||
file_bytes=b"content",
|
||||
session=session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestUploadPersistence:
|
||||
"""Verify upload file and record persistence behavior."""
|
||||
|
||||
def test_writes_file_and_creates_records(self, session, tmp_path: Path):
|
||||
"""create_upload_job writes file and creates document/job records."""
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
|
||||
result = create_upload_job(
|
||||
filename="letter.jpg",
|
||||
file_bytes=b"image-bytes",
|
||||
session=session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
assert result.stored_path.exists()
|
||||
assert result.stored_path.read_bytes() == b"image-bytes"
|
||||
|
||||
document = session.get(Document, result.document_id)
|
||||
job = session.get(Job, result.job_id)
|
||||
assert document is not None
|
||||
assert job is not None
|
||||
assert document.filename == "letter.jpg"
|
||||
assert document.file_path == str(result.stored_path)
|
||||
|
||||
def test_uses_unique_stored_filename(self, session, tmp_path: Path):
|
||||
"""create_upload_job stores uploads with unique filenames."""
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
|
||||
first = create_upload_job(
|
||||
filename="duplicate.jpg",
|
||||
file_bytes=b"first",
|
||||
session=session,
|
||||
settings=settings,
|
||||
)
|
||||
second = create_upload_job(
|
||||
filename="duplicate.jpg",
|
||||
file_bytes=b"second",
|
||||
session=session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
assert first.stored_path != second.stored_path
|
||||
assert first.stored_path.exists()
|
||||
assert second.stored_path.exists()
|
||||
|
||||
def test_sets_job_status_queued(self, session, tmp_path: Path):
|
||||
"""create_upload_job persists a job with queued status."""
|
||||
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||
|
||||
result = create_upload_job(
|
||||
filename="queued.pdf",
|
||||
file_bytes=b"%PDF-1.4",
|
||||
session=session,
|
||||
settings=settings,
|
||||
)
|
||||
|
||||
job = session.get(Job, result.job_id)
|
||||
assert job is not None
|
||||
assert job.status == JobStatus.QUEUED
|
||||
@@ -0,0 +1,141 @@
|
||||
"""Tests for transcription.worker."""
|
||||
|
||||
from threading import Event
|
||||
|
||||
import pytest
|
||||
from sqlmodel import select
|
||||
|
||||
from transcription.models import Document, Job, JobStatus, Transcript
|
||||
from transcription.providers.base import TranscriptionResult
|
||||
from transcription.worker import process_next_queued_job, run_worker_loop
|
||||
|
||||
|
||||
def _create_queued_job(session, *, filename: str = "doc.jpg", file_path: str = "uploads/doc.jpg") -> Job:
|
||||
document = Document(filename=filename, file_path=file_path)
|
||||
session.add(document)
|
||||
session.commit()
|
||||
session.refresh(document)
|
||||
|
||||
job = Job(document_id=document.id, status=JobStatus.QUEUED)
|
||||
session.add(job)
|
||||
session.commit()
|
||||
session.refresh(job)
|
||||
return job
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestWorkerQueueBehavior:
|
||||
"""Verify worker behavior when selecting queued jobs."""
|
||||
|
||||
def test_returns_false_when_queue_empty(self, session):
|
||||
"""process_next_queued_job returns False when there are no queued jobs."""
|
||||
processed = process_next_queued_job(session=session)
|
||||
assert processed is False
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestWorkerSuccessPath:
|
||||
"""Verify worker success-path lifecycle transitions and transcript persistence."""
|
||||
|
||||
def test_transitions_processing_to_transcribed(self, session, monkeypatch):
|
||||
"""process_next_queued_job transitions queued jobs to transcribed on success."""
|
||||
job = _create_queued_job(session)
|
||||
|
||||
def _fake_transcribe(_path):
|
||||
return TranscriptionResult(text="ok", provider="openrouter", model="test-model")
|
||||
|
||||
monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
|
||||
|
||||
processed = process_next_queued_job(session=session)
|
||||
session.refresh(job)
|
||||
|
||||
assert processed is True
|
||||
assert job.status == JobStatus.TRANSCRIBED
|
||||
|
||||
def test_persists_transcript_text_on_success(self, session, monkeypatch):
|
||||
"""process_next_queued_job stores transcript text for successful jobs."""
|
||||
job = _create_queued_job(session)
|
||||
|
||||
def _fake_transcribe(_path):
|
||||
return TranscriptionResult(text="Transcript body", provider="openrouter", model="test-model")
|
||||
|
||||
monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
|
||||
|
||||
process_next_queued_job(session=session)
|
||||
|
||||
transcript = session.exec(
|
||||
select(Transcript).where(Transcript.job_id == job.id)
|
||||
).first()
|
||||
assert transcript is not None
|
||||
assert transcript.text == "Transcript body"
|
||||
assert transcript.error_detail is None
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestWorkerFailurePath:
|
||||
"""Verify worker failure-path lifecycle transitions and error persistence."""
|
||||
|
||||
def test_sets_failed_and_error_detail_on_failure(self, session, monkeypatch):
|
||||
"""process_next_queued_job marks failed and stores error detail on exception."""
|
||||
job = _create_queued_job(session)
|
||||
|
||||
def _fake_transcribe(_path):
|
||||
raise RuntimeError("provider failure")
|
||||
|
||||
monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
|
||||
|
||||
processed = process_next_queued_job(session=session)
|
||||
session.refresh(job)
|
||||
transcript = session.exec(
|
||||
select(Transcript).where(Transcript.job_id == job.id)
|
||||
).first()
|
||||
|
||||
assert processed is True
|
||||
assert job.status == JobStatus.FAILED
|
||||
assert transcript is not None
|
||||
assert transcript.text is None
|
||||
assert "provider failure" in transcript.error_detail
|
||||
|
||||
def test_updates_existing_transcript_if_present(self, session, monkeypatch):
|
||||
"""process_next_queued_job updates existing transcript instead of duplicating."""
|
||||
job = _create_queued_job(session)
|
||||
existing = Transcript(job_id=job.id, text="old", error_detail=None)
|
||||
session.add(existing)
|
||||
session.commit()
|
||||
session.refresh(existing)
|
||||
|
||||
def _fake_transcribe(_path):
|
||||
raise RuntimeError("provider failure")
|
||||
|
||||
monkeypatch.setattr("transcription.worker.transcribe_document_image", _fake_transcribe)
|
||||
|
||||
process_next_queued_job(session=session)
|
||||
|
||||
transcripts = session.exec(
|
||||
select(Transcript).where(Transcript.job_id == job.id)
|
||||
).all()
|
||||
assert len(transcripts) == 1
|
||||
assert transcripts[0].id == existing.id
|
||||
assert transcripts[0].text is None
|
||||
assert "provider failure" in transcripts[0].error_detail
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWorkerLoopControl:
|
||||
"""Verify worker loop start/stop behavior."""
|
||||
|
||||
def test_stops_when_stop_event_is_set(self, monkeypatch):
|
||||
"""run_worker_loop exits when a stop event is set."""
|
||||
stop_event = Event()
|
||||
stop_event.set()
|
||||
|
||||
called = {"value": False}
|
||||
|
||||
def _fake_process_next_queued_job():
|
||||
called["value"] = True
|
||||
return False
|
||||
|
||||
monkeypatch.setattr("transcription.worker.process_next_queued_job", _fake_process_next_queued_job)
|
||||
|
||||
run_worker_loop(stop_event=stop_event, poll_interval_seconds=0.01)
|
||||
assert called["value"] is False
|
||||
Reference in New Issue
Block a user