Updated test suite

This commit is contained in:
Jim Lancaster
2026-07-29 16:20:46 -05:00
parent 0973311d9f
commit bc21a97019
17 changed files with 447 additions and 236 deletions
+31 -4
View File
@@ -33,7 +33,25 @@ class TestPipelineSuccessFlow:
_ = (prompt_text, image_bytes, mime_type)
return TranscriptionResult(text="Pipeline transcript", provider="openrouter", model="test-model", prompt_name="transcribe_document.md")
monkeypatch.setattr("transcription.services.transcription.OpenRouterTranscriptionProvider.transcribe", _fake_transcribe)
async def _fake_transcribe_document_image(
image_path,
*,
prompt_name="transcribe_document.md",
settings=None,
provider=None,
) -> TranscriptionResult:
_ = (image_path, prompt_name, settings, provider)
return TranscriptionResult(
text="Pipeline transcript",
provider="openrouter",
model="test-model",
prompt_name="transcribe_document.md",
)
monkeypatch.setattr(
"transcription.services.workflows.transcribe_document_image",
_fake_transcribe_document_image,
)
processed = await process_next_queued_job(session=async_session)
job = await async_session.get(Job, upload_result.job_id)
@@ -60,11 +78,20 @@ class TestPipelineFailureFlow:
settings=settings,
)
async def _fake_transcribe(*, prompt_text: str, image_bytes: bytes, mime_type: str) -> TranscriptionResult:
_ = (prompt_text, image_bytes, mime_type)
async def _fake_transcribe_document_image(
image_path,
*,
prompt_name="transcribe_document.md",
settings=None,
provider=None,
) -> TranscriptionResult:
_ = (image_path, prompt_name, settings, provider)
raise RuntimeError("pipeline provider failure")
monkeypatch.setattr("transcription.services.transcription.OpenRouterTranscriptionProvider.transcribe", _fake_transcribe)
monkeypatch.setattr(
"transcription.services.workflows.transcribe_document_image",
_fake_transcribe_document_image,
)
processed = await process_next_queued_job(session=async_session)
job = await async_session.get(Job, upload_result.job_id)
+13 -9
View File
@@ -15,7 +15,7 @@ class _FakeChat:
self._error = error
self.calls = []
def send(self, **kwargs):
async def send_async(self, **kwargs):
self.calls.append(kwargs)
if self._error:
raise self._error
@@ -48,7 +48,8 @@ class TestOpenRouterProviderInit:
class TestOpenRouterProviderTranscribe:
"""Verify OpenRouter request construction and response parsing."""
def test_includes_optional_referer_and_title_when_set(self):
@pytest.mark.asyncio
async def test_includes_optional_referer_and_title_when_set(self):
"""Transcribe sends app attribution fields when configured."""
response = {"model": "vendor/model-a", "choices": [{"message": {"content": "Transcript text"}}]}
client = _FakeClient(response=response)
@@ -59,7 +60,7 @@ class TestOpenRouterProviderTranscribe:
)
provider = OpenRouterTranscriptionProvider(settings=settings, client=client)
result = provider.transcribe(
result = await provider.transcribe(
prompt_text="Prompt body",
image_bytes=b"img-bytes",
mime_type="image/png",
@@ -70,7 +71,8 @@ class TestOpenRouterProviderTranscribe:
assert send_call["x_open_router_title"] == "Transcription App"
assert result.text == "Transcript text"
def test_parses_successful_response_text(self):
@pytest.mark.asyncio
async def test_parses_successful_response_text(self):
"""Transcribe returns normalized text from a valid response payload."""
response = {
"model": "vendor/model-b",
@@ -81,7 +83,7 @@ class TestOpenRouterProviderTranscribe:
client=_FakeClient(response=response),
)
result = provider.transcribe(
result = await provider.transcribe(
prompt_text="Prompt body",
image_bytes=b"img-bytes",
mime_type="image/jpeg",
@@ -91,7 +93,8 @@ class TestOpenRouterProviderTranscribe:
assert result.provider == "openrouter"
assert result.model == "vendor/model-b"
def test_maps_sdk_exception_to_provider_error(self):
@pytest.mark.asyncio
async def test_maps_sdk_exception_to_provider_error(self):
"""Transcribe converts SDK failures to ProviderError."""
provider = OpenRouterTranscriptionProvider(
settings=Settings(openrouter_api_key="test-key"),
@@ -99,13 +102,14 @@ class TestOpenRouterProviderTranscribe:
)
with pytest.raises(ProviderError):
provider.transcribe(
await provider.transcribe(
prompt_text="Prompt body",
image_bytes=b"img-bytes",
mime_type="image/png",
)
def test_raises_on_empty_or_invalid_response(self):
@pytest.mark.asyncio
async def test_raises_on_empty_or_invalid_response(self):
"""Transcribe raises ProviderResponseError for missing completion text."""
provider = OpenRouterTranscriptionProvider(
settings=Settings(openrouter_api_key="test-key"),
@@ -113,7 +117,7 @@ class TestOpenRouterProviderTranscribe:
)
with pytest.raises(ProviderResponseError):
provider.transcribe(
await provider.transcribe(
prompt_text="Prompt body",
image_bytes=b"img-bytes",
mime_type="image/png",
+76 -68
View File
@@ -4,85 +4,93 @@ import pytest
from transcription.models import Document
from transcription.models import Job
from transcription.models import JobStatus
from transcription.models import Source
from transcription.services.documents import DocumentService
from transcription.services.jobs import JobService
from transcription.services.jobs import JobStatus
class TestJobService:
class TestBasicCRUD:
@pytest.mark.asyncio
async def test_create_job(self, job_service: JobService):
"""Test creating a job."""
@pytest.mark.asyncio
async def test_create_and_read_job(self, job_service: JobService, document_service: DocumentService):
document = Document(id=uuid4(), name="test-bundle")
await document_service.create_document(document=document)
def fake_job_factory():
return Job(document_id=uuid4())
job = Job(document_id=document.id)
await job_service.create_job(job=job)
await job_service.create_job(job=fake_job_factory())
fetched = await job_service.read_job(job_id=job.id)
assert fetched.id == job.id
assert fetched.document is not None
assert fetched.document.id == document.id
async with job_service._session_scope() as session:
for _ in range(10):
await job_service.create_job(job=fake_job_factory(), session=session)
@pytest.mark.asyncio
async def test_update_job_state_updates_status_and_retry(self, job_service: JobService, document_service: DocumentService):
document = Document(id=uuid4(), name="test-bundle")
await document_service.create_document(document=document)
@pytest.mark.asyncio
async def test_backpropagation(self, job_service: JobService, document_service: DocumentService):
"""Test that creating a job backpropagates to the related document."""
doc_id = uuid4()
document = Document(
id=doc_id,
filename="test.txt",
file_path="/path/to/test.txt",
job = Job(document_id=document.id)
await job_service.create_job(job=job)
updated = await job_service.update_job_state(
job_id=job.id,
status=JobStatus.PROCESSING,
retry_count_increment=1,
)
assert updated.status == JobStatus.PROCESSING
assert updated.retry_count == 1
@pytest.mark.asyncio
async def test_query_jobs_by_status(self, job_service: JobService, document_service: DocumentService):
document = Document(id=uuid4(), name="query-doc")
await document_service.create_document(document=document)
await job_service.create_job(job=Job(document_id=document.id, status=JobStatus.PROCESSING))
await job_service.create_job(job=Job(document_id=document.id, status=JobStatus.QUEUED))
result = await job_service.query_jobs(status=JobStatus.PROCESSING)
assert len(result) == 1
assert result[0].status == JobStatus.PROCESSING
@pytest.mark.asyncio
async def test_query_jobs_by_source_filename(self, job_service: JobService, document_service: DocumentService):
document = Document(id=uuid4(), name="source-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:
session.add(
Source(
document_id=document.id,
job_id=job.id,
upload_name="letter.jpg",
filename="stored-letter.jpg",
file_path="/uploads/stored-letter.jpg",
)
)
await document_service.create_document(document=document)
job = Job(document_id=doc_id)
await job_service.create_job(job=job)
await session.commit()
read_job = await job_service.read_job(job_id=job.id)
assert isinstance(read_job.document, Document)
assert read_job.document.id == document.id
result = await job_service.query_jobs(filename="stored-letter.jpg")
assert len(result) == 1
assert result[0].id == job.id
@pytest.mark.asyncio
async def test_reading_job(self, job_service: JobService):
"""Test reading a job."""
uuid = uuid4()
await job_service.create_job(job=Job(id=uuid, document_id=uuid4()))
job = await job_service.read_job(job_id=uuid)
assert job.id == uuid
@pytest.mark.asyncio
async def test_read_next_queued_job_orders_by_created_date(
self,
job_service: JobService,
document_service: DocumentService,
):
document = Document(id=uuid4(), name="ordered-doc")
await document_service.create_document(document=document)
@pytest.mark.asyncio
async def test_updating_job(self, job_service: JobService):
"""Test updating a job."""
uuid = uuid4()
job = Job(id=uuid, document_id=uuid4())
async with job_service._session_scope() as session:
await job_service.create_job(job=job, session=session)
job.status = JobStatus.PROCESSING
await job_service.update_job(job=job, session=session)
read_job = await job_service.read_job(job_id=uuid, session=session)
assert read_job == job
first = Job(document_id=document.id, status=JobStatus.QUEUED)
second = Job(document_id=document.id, status=JobStatus.QUEUED)
await job_service.create_job(job=first)
await job_service.create_job(job=second)
@pytest.mark.asyncio
async def test_deleting_job(self, job_service: JobService):
"""Test deleting a job."""
class TestServiceMethods:
@pytest.mark.asyncio
async def test_query_jobs(self, job_service: JobService):
"""Test querying jobs."""
await job_service.create_job(job=Job(document_id=uuid4(), status=JobStatus.PROCESSING))
result = await job_service.query_jobs(status=JobStatus.PROCESSING)
jobs = {str(job.id).split("-")[0]: job.status for job in result}
assert len(jobs) == 1
@pytest.mark.asyncio
async def test_list_jobs(self, job_service: JobService):
"""Test listing jobs."""
n = 5
for _ in range(n):
await job_service.create_job(job=Job(document_id=uuid4()))
jobs = await job_service.list_jobs()
assert len(jobs) == n
@pytest.mark.asyncio
async def test_mark_job_status(self, job_service: JobService):
"""Test marking a job with a new status."""
next_job = await job_service.read_next_queued_job()
assert next_job is not None
assert next_job.id == first.id
@@ -49,10 +49,11 @@ class TestRealImageExternalTranscription:
assert REAL_IMAGES_DIR.exists()
assert _real_image_paths()
@pytest.mark.asyncio
@pytest.mark.parametrize("image_path", _real_image_paths(), ids=lambda p: p.name)
def test_transcribes_real_image_fixture(self, image_path: Path):
async def test_transcribes_real_image_fixture(self, image_path: Path):
"""Real fixture image produces a non-empty transcription result."""
result = transcribe_document_image(image_path)
result = await transcribe_document_image(image_path)
assert result.provider == "openrouter"
assert isinstance(result.model, str) and result.model.strip()
assert isinstance(result.text, str) and result.text.strip()
@@ -0,0 +1,63 @@
"""Reliability tests for worker workflow timeout behavior."""
from pathlib import Path
from uuid import uuid4
import pytest
from transcription.config import Settings
from transcription.models import Document
from transcription.models import Job
from transcription.models import JobStatus
from transcription.models import Source
from transcription.services import ServiceBundle
from transcription.services.workflows import process_queued_job
@pytest.mark.integration
class TestWorkflowReliability:
"""Verify timeout and terminal-state reliability behavior."""
@pytest.mark.asyncio
async def test_process_queued_job_timeout_marks_job_failed(self, default_session_factory, monkeypatch):
"""Provider timeout transitions a queued job to failed with error detail."""
services = ServiceBundle()
object.__setattr__(services, "documents", services.documents.__class__(session_factory=default_session_factory))
object.__setattr__(services, "jobs", services.jobs.__class__(session_factory=default_session_factory))
object.__setattr__(services, "transcriptions", services.transcriptions.__class__(session_factory=default_session_factory))
async with services.jobs._session_scope() as session:
document = Document(id=uuid4(), name="timeout-doc")
session.add(document)
await session.flush()
job = Job(document_id=document.id, status=JobStatus.QUEUED)
session.add(job)
await session.flush()
source = Source(
document_id=document.id,
job_id=job.id,
upload_name="timeout.jpg",
filename="timeout.jpg",
file_path=str(Path("tests/fixtures/images/real/Book Two - page 02.jpg")),
)
session.add(source)
await session.commit()
loaded = await services.jobs.read_job(job_id=job.id, session=session)
async def _never_returns(image_path, *, prompt_name="transcribe_document.md", settings=None, provider=None):
_ = (image_path, prompt_name, settings, provider)
raise TimeoutError("simulated provider timeout")
monkeypatch.setattr("transcription.services.workflows.transcribe_document_image", _never_returns)
timeout_settings = Settings(openrouter_api_key="test-key", worker_provider_timeout_seconds=20.0)
result = await process_queued_job(job=loaded, services=services, settings=timeout_settings)
assert result is not None
assert result.status == JobStatus.FAILED
assert result.error_detail is not None
assert "timed out" in result.error_detail.lower()
assert "20.0s" in result.error_detail
+64 -29
View File
@@ -1,5 +1,7 @@
"""Tests for transcription.app."""
from contextlib import asynccontextmanager
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
@@ -21,28 +23,43 @@ class TestAppFactory:
class TestAppLifespan:
"""Verify startup and shutdown lifecycle behavior."""
def test_startup_initializes_runtime_dependencies(self, monkeypatch):
def test_startup_initializes_runtime_dependencies(self, monkeypatch, tmp_path):
"""Startup initializes logging, schema, directories, and worker resources."""
calls = []
monkeypatch.setattr("transcription.app.setup_logging", lambda: calls.append("logging"))
monkeypatch.setattr("transcription.app.create_all", lambda **_kwargs: calls.append("schema"))
monkeypatch.setattr("transcription.app.configure_logging", lambda: calls.append("logging"))
async def _create_all(**_kwargs):
calls.append("schema")
monkeypatch.setattr("transcription.app.create_all", _create_all)
monkeypatch.setattr(
"transcription.app.initialize_database_runtime",
lambda **_kwargs: type("_Runtime", (), {"engine": object()})(),
lambda **_kwargs: type("_Runtime", (), {"engine": object(), "session_factory": object()})(),
)
monkeypatch.setattr("transcription.app.dispose_database_runtime", lambda: calls.append("dispose_db"))
monkeypatch.setattr("transcription.app.should_bootstrap_schema", lambda _settings: True)
monkeypatch.setattr("transcription.app._start_worker", lambda _app: calls.append("start_worker"))
monkeypatch.setattr("transcription.app._stop_worker", lambda _app: calls.append("stop_worker"))
class _Dir:
def mkdir(self, parents: bool, exist_ok: bool):
calls.append("mkdir")
async def _dispose_runtime():
calls.append("dispose_db")
monkeypatch.setattr("transcription.app.dispose_database_runtime", _dispose_runtime)
async def _recover_stale(_app):
calls.append("recover")
monkeypatch.setattr("transcription.app._recover_stale_processing_jobs", _recover_stale)
@asynccontextmanager
async def _worker_lifespan(**_kwargs):
calls.append("worker_start")
yield object(), object()
calls.append("worker_stop")
monkeypatch.setattr("transcription.app.worker_consumer_lifespan", _worker_lifespan)
class _Settings:
upload_dir = _Dir()
prompt_dir = _Dir()
should_bootstrap_schema = True
upload_dir = tmp_path / "uploads"
prompt_dir = tmp_path / "prompts"
monkeypatch.setattr("transcription.app.get_settings", lambda: _Settings())
@@ -52,32 +69,50 @@ class TestAppLifespan:
assert "logging" in calls
assert "schema" in calls
assert "mkdir" in calls
assert "start_worker" in calls
assert "recover" in calls
assert "worker_start" in calls
assert "worker_stop" in calls
assert "dispose_db" in calls
assert _Settings.upload_dir.exists()
assert _Settings.prompt_dir.exists()
def test_shutdown_stops_worker_resources(self, monkeypatch):
def test_shutdown_stops_worker_resources(self, monkeypatch, tmp_path):
"""Shutdown signals and stops worker resources cleanly."""
calls = []
monkeypatch.setattr("transcription.app.setup_logging", lambda: None)
monkeypatch.setattr("transcription.app.create_all", lambda **_kwargs: None)
monkeypatch.setattr("transcription.app.configure_logging", lambda: calls.append("logging"))
async def _create_all(**_kwargs):
calls.append("schema")
monkeypatch.setattr("transcription.app.create_all", _create_all)
monkeypatch.setattr(
"transcription.app.initialize_database_runtime",
lambda **_kwargs: type("_Runtime", (), {"engine": object()})(),
lambda **_kwargs: type("_Runtime", (), {"engine": object(), "session_factory": object()})(),
)
monkeypatch.setattr("transcription.app.dispose_database_runtime", lambda: calls.append("dispose_db"))
monkeypatch.setattr("transcription.app.should_bootstrap_schema", lambda _settings: True)
monkeypatch.setattr("transcription.app._start_worker", lambda _app: calls.append("start_worker"))
monkeypatch.setattr("transcription.app._stop_worker", lambda _app: calls.append("stop_worker"))
class _Dir:
def mkdir(self, parents: bool, exist_ok: bool):
return None
async def _dispose_runtime():
calls.append("dispose_db")
monkeypatch.setattr("transcription.app.dispose_database_runtime", _dispose_runtime)
async def _recover_stale(_app):
calls.append("recover")
monkeypatch.setattr("transcription.app._recover_stale_processing_jobs", _recover_stale)
@asynccontextmanager
async def _worker_lifespan(**_kwargs):
calls.append("worker_start")
yield object(), object()
calls.append("worker_stop")
monkeypatch.setattr("transcription.app.worker_consumer_lifespan", _worker_lifespan)
class _Settings:
upload_dir = _Dir()
prompt_dir = _Dir()
should_bootstrap_schema = True
upload_dir = tmp_path / "uploads"
prompt_dir = tmp_path / "prompts"
monkeypatch.setattr("transcription.app.get_settings", lambda: _Settings())
@@ -85,4 +120,4 @@ class TestAppLifespan:
with TestClient(app):
pass
assert calls == ["start_worker", "stop_worker", "dispose_db"]
assert calls == ["logging", "schema", "recover", "worker_start", "worker_stop", "dispose_db"]
+48 -78
View File
@@ -1,97 +1,67 @@
"""Tests for transcription.db schema bootstrap and session factory."""
"""Tests for transcription.db runtime and schema bootstrap behavior."""
from sqlalchemy import inspect, text
from sqlmodel import Session, SQLModel, create_engine
from sqlmodel.pool import StaticPool
import pytest
from sqlalchemy import inspect
from transcription.config import Settings
from transcription.db import create_all
from transcription.db import dispose_database_runtime
from transcription.db import get_session
from transcription.db import initialize_database_runtime
def _in_memory_engine():
"""Create a fresh in-memory SQLite engine for isolated db tests."""
return create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
@pytest.mark.asyncio
async def test_create_all_creates_expected_tables(tmp_path):
settings = Settings(
openrouter_api_key="test-key",
database_url=f"sqlite:///{tmp_path / 'schema.db'}",
environment="test",
)
runtime = initialize_database_runtime(settings=settings)
try:
await create_all(engine=runtime.engine)
async with runtime.engine.connect() as conn:
table_names = set(await conn.run_sync(lambda c: inspect(c).get_table_names()))
class TestSchemaBootstrap:
"""Verify create_all produces the expected table set."""
def test_create_all_creates_expected_tables(self):
"""After create_all(), document, source, job, and revision tables exist."""
engine = _in_memory_engine()
# Ensure models are imported so metadata is populated
from transcription.models import Document, Job, Revision, Source # noqa: F401
import transcription.db as db_module
db_module.create_all(engine=engine)
inspector = inspect(engine)
table_names = set(inspector.get_table_names())
assert "document" in table_names
assert "job" in table_names
assert "source" in table_names
assert "revision" in table_names
finally:
await dispose_database_runtime()
class TestSessionFactory:
"""Verify get_session yields and cleans up sessions."""
@pytest.mark.asyncio
async def test_get_session_yields_async_session(tmp_path):
settings = Settings(
openrouter_api_key="test-key",
database_url=f"sqlite:///{tmp_path / 'session.db'}",
environment="test",
)
initialize_database_runtime(settings=settings)
def test_get_session_yields_session(self):
"""get_session() yields a usable Session object."""
engine = _in_memory_engine()
SQLModel.metadata.create_all(engine)
import transcription.db as db_module
with db_module.get_session(engine=engine) as session:
assert isinstance(session, Session)
def test_session_is_closed_after_generator_exit(self):
"""After the context manager exits, the session is closed."""
engine = _in_memory_engine()
SQLModel.metadata.create_all(engine)
import transcription.db as db_module
with db_module.get_session(engine=engine) as session:
# Session is usable inside the context
session.execute(text("SELECT 1"))
captured = session
# After exiting, the session's internal connection is released
# (no active transaction bound to the session)
assert captured._transaction is None
try:
async with get_session(settings=settings) as session:
assert session is not None
finally:
await dispose_database_runtime()
class TestBootstrapPolicy:
"""Verify schema bootstrap policy defaults and overrides."""
def test_bootstrap_policy_production_defaults_false():
settings = Settings(openrouter_api_key="test-key", environment="production")
assert settings.should_bootstrap_schema is False
def test_production_defaults_to_no_bootstrap(self):
"""Production defaults to explicit non-bootstrap startup behavior."""
from transcription.config import Settings
from transcription.db import should_bootstrap_schema
settings = Settings(openrouter_api_key="test-key", environment="production")
assert should_bootstrap_schema(settings) is False
def test_bootstrap_policy_development_defaults_true():
settings = Settings(openrouter_api_key="test-key", environment="development")
assert settings.should_bootstrap_schema is True
def test_development_defaults_to_bootstrap(self):
"""Development defaults to schema bootstrap for local workflows."""
from transcription.config import Settings
from transcription.db import should_bootstrap_schema
settings = Settings(openrouter_api_key="test-key", environment="development")
assert should_bootstrap_schema(settings) is True
def test_explicit_override_wins(self):
"""Explicit bootstrap_schema_on_startup overrides environment default."""
from transcription.config import Settings
from transcription.db import should_bootstrap_schema
settings = Settings(
openrouter_api_key="test-key",
environment="production",
bootstrap_schema_on_startup=True,
)
assert should_bootstrap_schema(settings) is True
def test_bootstrap_policy_explicit_override_true():
settings = Settings(
openrouter_api_key="test-key",
environment="production",
bootstrap_schema_on_startup=True,
)
assert settings.should_bootstrap_schema is True
+6 -6
View File
@@ -9,19 +9,19 @@ MVP_REQUIREMENT_TEST_MAP: dict[str, list[str]] = {
"tests/integration/test_pipeline_flow.py",
],
"REQ-1": [
"tests/services/test_upload.py",
"tests/integration/test_pipeline_flow.py",
"tests/ui/test_upload_page.py",
],
"REQ-2": [
"tests/services/test_worker.py",
"tests/services/test_workflows_reliability.py",
"tests/integration/test_pipeline_flow.py",
],
"REQ-3": [
"tests/services/test_worker.py",
"tests/services/test_job_service.py",
"tests/ui/test_jobs_page.py",
],
"REQ-4": [
"tests/services/test_worker.py",
"tests/services/test_workflows_reliability.py",
"tests/integration/test_pipeline_flow.py",
],
"REQ-5": [
@@ -30,7 +30,7 @@ MVP_REQUIREMENT_TEST_MAP: dict[str, list[str]] = {
],
"REQ-6": [
"tests/test_app.py",
"tests/services/test_worker.py",
"tests/services/test_workflows_reliability.py",
],
"REQ-8": [
"tests/test_app.py",
@@ -38,7 +38,7 @@ MVP_REQUIREMENT_TEST_MAP: dict[str, list[str]] = {
],
"REQ-12": [
"tests/test_prompts.py",
"tests/services/test_transcription.py",
"tests/services/test_transcription_external.py",
],
}
+34 -23
View File
@@ -21,9 +21,10 @@ from transcription.db import initialize_database_runtime
from transcription.models import Document
from transcription.models import Job
from transcription.models import JobStatus
from transcription.models import Transcript
from transcription.models import Revision
from transcription.models import Source
TranscriptSeed = tuple[int, str | None, str | None]
RevisionSeed = str
@pytest.fixture(scope="session")
@@ -54,7 +55,8 @@ def clear_ui_database(app_client: tuple[FastAPI, TestClient]) -> None:
async def _clear() -> None:
async with get_session(session_factory=app.state.runtime.session_factory) as session:
await session.exec(delete(Transcript))
await session.exec(delete(Revision))
await session.exec(delete(Source))
await session.exec(delete(Job))
await session.exec(delete(Document))
await session.commit()
@@ -64,7 +66,7 @@ def clear_ui_database(app_client: tuple[FastAPI, TestClient]) -> None:
@pytest.fixture
def seed_job(app_client: tuple[FastAPI, TestClient]) -> Callable[..., UUID]:
"""Return a helper for inserting a document/job/transcript trio."""
"""Return a helper for inserting a document/job/source/(optional revision) tuple."""
app, _ = app_client
fixtures_dir = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "valid"
@@ -72,9 +74,9 @@ def seed_job(app_client: tuple[FastAPI, TestClient]) -> Callable[..., UUID]:
*,
filename: str = "sample.pdf",
status: JobStatus = JobStatus.TRANSCRIBED,
transcript_text: str | None = "Sample transcript text",
transcription_text: str | None = "Sample transcript text",
error_detail: str | None = None,
transcript_revisions: list[TranscriptSeed] | None = None,
revision_text: RevisionSeed | None = None,
source_file: Path | None = None,
) -> UUID:
async def _insert() -> UUID:
@@ -84,31 +86,40 @@ def seed_job(app_client: tuple[FastAPI, TestClient]) -> Callable[..., UUID]:
source_path = source_file or fixtures_dir / "small_png.png"
stored_path.write_bytes(source_path.read_bytes())
document = Document(filename=filename, file_path=str(stored_path))
document = Document(name=filename)
session.add(document)
await session.flush()
job = Job(document_id=document.id, status=status, retry_count=0)
job = Job(
document_id=document.id,
status=status,
retry_count=0,
text=transcription_text,
error_detail=error_detail,
provider="openrouter",
model="google/gemini-2.5-flash",
prompt_name="transcribe_document.md",
)
session.add(job)
await session.flush()
revisions = transcript_revisions
if revisions is None and (transcript_text is not None or error_detail is not None):
revisions = [(0, transcript_text, error_detail)]
source = Source(
document_id=document.id,
job_id=job.id,
upload_name=filename,
filename=filename,
file_path=str(stored_path),
)
session.add(source)
await session.flush()
if revisions is not None:
for revision, revision_text, revision_error in revisions:
session.add(
Transcript(
job_id=job.id,
revision=revision,
provider="openrouter",
model="google/gemini-2.5-flash",
prompt_name="transcribe_document",
text=revision_text,
error_detail=revision_error,
)
if revision_text is not None:
session.add(
Revision(
source_id=source.id,
text=revision_text,
)
)
await session.commit()
return job.id
+6 -10
View File
@@ -18,13 +18,12 @@ class TestPageRendering:
response = client.get("/ui/jobs")
assert response.status_code == 200
assert "Transcription Jobs" in response.text
assert "No jobs yet." in response.text
def test_jobs_page_lists_seeded_jobs(self, app_client, seed_job):
"""GET /ui/jobs lists seeded jobs from the in-memory database."""
_, client = app_client
seed_job(filename="sample.pdf", status=JobStatus.TRANSCRIBED, transcript_text="done")
seed_job(filename="sample.pdf", status=JobStatus.TRANSCRIBED, transcription_text="done")
response = client.get("/ui/jobs")
@@ -39,23 +38,20 @@ class TestPageRendering:
job_id = seed_job(
filename="detail.pdf",
status=JobStatus.TRANSCRIBED,
transcript_revisions=[
(0, None, "first attempt failed"),
(1, "hello", None),
],
transcription_text="original text",
revision_text="hello",
source_file=fixture_path,
)
response = client.get(f"/ui/jobs/{job_id}")
assert response.status_code == 200
assert "Job Detail" in response.text
assert "Job overview" in response.text
assert "Original Transcription" in response.text
assert "detail.pdf" in response.text
assert "Transcripts" in response.text
assert "Revision" in response.text
assert "first attempt failed" in response.text
assert "Revision" in response.text
assert "hello" in response.text
assert "original text" in response.text
assert "Document preview" in response.text
assert "/uploads/detail.pdf" in response.text