Continue GC code review and cleanup

This commit is contained in:
Jim Lancaster
2026-08-11 16:42:09 -05:00
parent 8d5aec4301
commit b8be27f0c9
16 changed files with 363 additions and 161 deletions
+22
View File
@@ -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."""
+10 -5
View File
@@ -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
View File
@@ -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):
+50
View File
@@ -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
View File
@@ -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