generated from john/python-template
Continue GC code review and cleanup
This commit is contained in:
@@ -120,6 +120,7 @@ class TestOpenRouterProviderTranscribe:
|
||||
|
||||
assert result.text == "Line 1\nLine 2"
|
||||
assert result.provider == "openrouter"
|
||||
assert result.prompt_name is None
|
||||
assert result.model == "vendor/model-b"
|
||||
assert result.ai_metadata == {
|
||||
"finish_reason": "stop",
|
||||
@@ -142,6 +143,27 @@ class TestOpenRouterProviderTranscribe:
|
||||
mime_type="image/png",
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sends_pdf_as_file_content(self):
|
||||
"""PDF payloads use OpenRouter's file content contract."""
|
||||
response = {"model": "vendor/model-a", "choices": [{"message": {"content": "Transcript text"}}]}
|
||||
client = _FakeClient(response=response)
|
||||
provider = OpenRouterTranscriptionProvider(
|
||||
settings=Settings(openrouter_api_key="test-key"),
|
||||
client=client,
|
||||
)
|
||||
|
||||
await provider.transcribe(
|
||||
prompt_text="Prompt body",
|
||||
image_bytes=b"pdf-bytes",
|
||||
mime_type="application/pdf",
|
||||
)
|
||||
|
||||
content = client.chat.calls[0]["messages"][0]["content"]
|
||||
assert content[1]["type"] == "file"
|
||||
assert content[1]["file"]["filename"] == "source.pdf"
|
||||
assert content[1]["file"]["file_data"].startswith("data:application/pdf;base64,")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_raises_on_empty_or_invalid_response(self):
|
||||
"""Transcribe raises ProviderResponseError for missing completion text."""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from uuid import uuid4
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -12,8 +12,9 @@ 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 JobCancelBlockedError
|
||||
from transcription.services.jobs import JobDeleteBlockedError
|
||||
from transcription.services.jobs import JobNotFoundError
|
||||
from transcription.services.jobs import JobResubmitBlockedError
|
||||
from transcription.services.jobs import JobService
|
||||
|
||||
@@ -228,7 +229,7 @@ class TestJobService:
|
||||
|
||||
await job_service.delete_job_with_guardrails(job_id=job.id)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(JobNotFoundError):
|
||||
await job_service.read_job(job_id=job.id)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -355,8 +356,12 @@ class TestJobService:
|
||||
refreshed = await job_service.read_job(job_id=job.id)
|
||||
assert refreshed.status == JobStatus.QUEUED
|
||||
|
||||
failed_entry = next(item for item in refreshed.job_sources if item.source is not None and item.source.page_number == 1)
|
||||
transcribed_entry = next(item for item in refreshed.job_sources if item.source is not None and item.source.page_number == 2)
|
||||
failed_entry = next(
|
||||
item for item in refreshed.job_sources if item.source is not None and item.source.page_number == 1
|
||||
)
|
||||
transcribed_entry = next(
|
||||
item for item in refreshed.job_sources if item.source is not None and item.source.page_number == 2
|
||||
)
|
||||
assert failed_entry.status == JobSourceStatus.PENDING
|
||||
assert failed_entry.error_detail is None
|
||||
assert failed_entry.source is not None
|
||||
|
||||
+23
-16
@@ -7,18 +7,19 @@ from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from transcription.app import create_app
|
||||
from transcription.config import Settings
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestAppFactory:
|
||||
"""Verify FastAPI app factory wiring."""
|
||||
|
||||
def test_create_app_returns_fastapi_instance(self):
|
||||
def test_create_app_returns_fastapi_instance(self, monkeypatch):
|
||||
"""create_app returns a FastAPI application instance."""
|
||||
monkeypatch.setattr("transcription.app.register_pages", lambda _app: None)
|
||||
app = create_app()
|
||||
assert isinstance(app, FastAPI)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestAppLifespan:
|
||||
"""Verify startup and shutdown lifecycle behavior."""
|
||||
@@ -28,6 +29,7 @@ class TestAppLifespan:
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr("transcription.app.configure_logging", lambda _settings: calls.append("logging"))
|
||||
monkeypatch.setattr("transcription.app.register_pages", lambda _app: None)
|
||||
|
||||
async def _create_all(**_kwargs):
|
||||
calls.append("schema")
|
||||
@@ -56,12 +58,14 @@ class TestAppLifespan:
|
||||
|
||||
monkeypatch.setattr("transcription.app.worker_consumer_lifespan", _worker_lifespan)
|
||||
|
||||
class _Settings:
|
||||
should_bootstrap_schema = True
|
||||
upload_dir = tmp_path / "uploads"
|
||||
prompt_dir = tmp_path / "prompts"
|
||||
|
||||
monkeypatch.setattr("transcription.app.get_settings", lambda: _Settings())
|
||||
settings = Settings(
|
||||
openrouter_api_key="test-key",
|
||||
environment="test",
|
||||
bootstrap_schema_on_startup=True,
|
||||
upload_dir=tmp_path / "uploads",
|
||||
prompt_dir=tmp_path / "prompts",
|
||||
)
|
||||
monkeypatch.setattr("transcription.app.get_settings", lambda: settings)
|
||||
|
||||
app = create_app()
|
||||
with TestClient(app):
|
||||
@@ -73,14 +77,15 @@ class TestAppLifespan:
|
||||
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()
|
||||
assert settings.upload_dir.exists()
|
||||
assert settings.prompt_dir.exists()
|
||||
|
||||
def test_shutdown_stops_worker_resources(self, monkeypatch, tmp_path):
|
||||
"""Shutdown signals and stops worker resources cleanly."""
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr("transcription.app.configure_logging", lambda _settings: calls.append("logging"))
|
||||
monkeypatch.setattr("transcription.app.register_pages", lambda _app: None)
|
||||
|
||||
async def _create_all(**_kwargs):
|
||||
calls.append("schema")
|
||||
@@ -109,12 +114,14 @@ class TestAppLifespan:
|
||||
|
||||
monkeypatch.setattr("transcription.app.worker_consumer_lifespan", _worker_lifespan)
|
||||
|
||||
class _Settings:
|
||||
should_bootstrap_schema = True
|
||||
upload_dir = tmp_path / "uploads"
|
||||
prompt_dir = tmp_path / "prompts"
|
||||
|
||||
monkeypatch.setattr("transcription.app.get_settings", lambda: _Settings())
|
||||
settings = Settings(
|
||||
openrouter_api_key="test-key",
|
||||
environment="test",
|
||||
bootstrap_schema_on_startup=True,
|
||||
upload_dir=tmp_path / "uploads",
|
||||
prompt_dir=tmp_path / "prompts",
|
||||
)
|
||||
monkeypatch.setattr("transcription.app.get_settings", lambda: settings)
|
||||
|
||||
app = create_app()
|
||||
with TestClient(app):
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tools import run_destructive_tests
|
||||
|
||||
|
||||
def test_reuses_initial_backup_across_test_attempts(tmp_path: Path) -> None:
|
||||
data_path = tmp_path / "data"
|
||||
backup_root = tmp_path / ".test-backups"
|
||||
data_path.mkdir()
|
||||
backup_root.mkdir()
|
||||
(data_path / "transcription.db").write_text("original", encoding="utf-8")
|
||||
|
||||
initial_backup = run_destructive_tests.create_or_reuse_backup(data_path, backup_root)
|
||||
(data_path / "transcription.db").write_text("overwritten", encoding="utf-8")
|
||||
reused_backup = run_destructive_tests.create_or_reuse_backup(data_path, backup_root)
|
||||
|
||||
assert reused_backup == initial_backup
|
||||
assert (reused_backup / "transcription.db").read_text(encoding="utf-8") == "original"
|
||||
assert [path for path in backup_root.iterdir() if path.is_dir()] == [initial_backup]
|
||||
|
||||
|
||||
def test_missing_active_backup_stops_instead_of_replacing_it(tmp_path: Path) -> None:
|
||||
data_path = tmp_path / "data"
|
||||
backup_root = tmp_path / ".test-backups"
|
||||
data_path.mkdir()
|
||||
backup_root.mkdir()
|
||||
(data_path / "transcription.db").write_text("post-test", encoding="utf-8")
|
||||
(backup_root / run_destructive_tests.ACTIVE_BACKUP_FILENAME).write_text("data-backup-missing", encoding="utf-8")
|
||||
|
||||
with pytest.raises(FileNotFoundError, match="Active backup is missing"):
|
||||
run_destructive_tests.create_or_reuse_backup(data_path, backup_root)
|
||||
|
||||
assert not any(path.is_dir() for path in backup_root.iterdir())
|
||||
|
||||
|
||||
def test_closing_cycle_preserves_backup(tmp_path: Path) -> None:
|
||||
data_path = tmp_path / "data"
|
||||
backup_root = tmp_path / ".test-backups"
|
||||
data_path.mkdir()
|
||||
backup_root.mkdir()
|
||||
(data_path / "transcription.db").write_text("original", encoding="utf-8")
|
||||
backup_path = run_destructive_tests.create_or_reuse_backup(data_path, backup_root)
|
||||
|
||||
closed_backup = run_destructive_tests.close_active_backup_cycle(backup_root)
|
||||
|
||||
assert closed_backup == backup_path
|
||||
assert backup_path.is_dir()
|
||||
assert not (backup_root / run_destructive_tests.ACTIVE_BACKUP_FILENAME).exists()
|
||||
+33
-27
@@ -2,11 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from datetime import UTC, datetime
|
||||
from collections.abc import AsyncGenerator
|
||||
from collections.abc import Awaitable
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Awaitable
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
@@ -16,45 +17,52 @@ from fastapi.testclient import TestClient
|
||||
from sqlmodel import delete
|
||||
|
||||
from transcription.app import create_app
|
||||
from transcription.config import Settings, SqliteSettings
|
||||
from transcription.db import create_all, initialize_database_runtime, session_scope
|
||||
from transcription.db.models import (
|
||||
Document,
|
||||
DocumentPerson,
|
||||
Job,
|
||||
JobSource,
|
||||
JobSourceStatus,
|
||||
JobStatus,
|
||||
Person,
|
||||
Source,
|
||||
)
|
||||
from transcription.config import Settings
|
||||
from transcription.config import SqliteSettings
|
||||
from transcription.db import session as db_session_module
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobSource
|
||||
from transcription.db.models import JobSourceStatus
|
||||
from transcription.db.models import JobStatus
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import Source
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_client(tmp_path_factory: pytest.TempPathFactory) -> AsyncGenerator[tuple[FastAPI, TestClient], None]:
|
||||
def app_client(tmp_path_factory: pytest.TempPathFactory) -> AsyncGenerator[tuple[FastAPI, TestClient]]:
|
||||
"""Provide a real application and test client backed by in-memory SQLite."""
|
||||
tmp_path = tmp_path_factory.mktemp("ui")
|
||||
settings = Settings(
|
||||
openrouter_api_key="test-key",
|
||||
database=SqliteSettings(path=":memory:"),
|
||||
database=SqliteSettings(path=str(tmp_path / "ui-tests.db")),
|
||||
environment="test",
|
||||
bootstrap_schema_on_startup=True,
|
||||
upload_dir=tmp_path / "uploads",
|
||||
prompt_dir=tmp_path / "prompts",
|
||||
)
|
||||
|
||||
app = create_app()
|
||||
app.state.runtime = initialize_database_runtime(settings=settings)
|
||||
asyncio.run(create_all(engine=app.state.runtime.engine))
|
||||
app = create_app(settings=settings)
|
||||
|
||||
with TestClient(app) as client:
|
||||
yield app, client
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def clear_ui_database(app_client: tuple[FastAPI, TestClient]) -> None:
|
||||
async def clear_ui_database(
|
||||
app_client: tuple[FastAPI, TestClient],
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Reset UI-facing tables asynchronously before each test for isolation."""
|
||||
async with session_scope() as session:
|
||||
app, _ = app_client
|
||||
monkeypatch.setattr(
|
||||
db_session_module,
|
||||
"resolve_session_factory",
|
||||
lambda *_args, **_kwargs: app.state.runtime.session_factory,
|
||||
)
|
||||
async with session_scope(session_factory=app.state.runtime.session_factory) as session:
|
||||
await session.exec(delete(JobSource))
|
||||
await session.exec(delete(DocumentPerson))
|
||||
await session.exec(delete(Source))
|
||||
@@ -118,9 +126,7 @@ async def seed_job(app_client: tuple[FastAPI, TestClient]) -> Callable[..., Awai
|
||||
job_id=job.id,
|
||||
source_id=source.id,
|
||||
status=(
|
||||
JobSourceStatus.TRANSCRIBED
|
||||
if transcription_text is not None
|
||||
else JobSourceStatus.FAILED
|
||||
JobSourceStatus.TRANSCRIBED if transcription_text is not None else JobSourceStatus.FAILED
|
||||
),
|
||||
raw_transcription=transcription_text,
|
||||
error_detail=error_detail,
|
||||
@@ -135,4 +141,4 @@ async def seed_job(app_client: tuple[FastAPI, TestClient]) -> Callable[..., Awai
|
||||
await session.commit()
|
||||
return job.id
|
||||
|
||||
return _seed
|
||||
return _seed
|
||||
|
||||
Reference in New Issue
Block a user