V4.2 complete

This commit is contained in:
Jim Lancaster
2026-08-14 15:59:38 -05:00
parent 6bd4cbb0a7
commit c9f5dca064
12 changed files with 282 additions and 21 deletions
+12 -3
View File
@@ -4,6 +4,8 @@ Every test gets a fresh in-memory SQLite database so tests are
isolated, fast, and leave no artifacts on disk.
"""
from pathlib import Path
import pytest
import pytest_asyncio
from sqlmodel import Session
@@ -12,7 +14,7 @@ from sqlmodel import create_engine
from sqlmodel.pool import StaticPool
from transcription.config import Settings
from transcription.config import get_settings
from transcription.config import SqliteSettings
from transcription.db.engine import get_database_url
from transcription.db.engine import get_engine
from transcription.db.session import dispose_session_factory
@@ -37,10 +39,17 @@ def session():
@pytest_asyncio.fixture
async def default_settings():
async def default_settings(tmp_path):
"""Provide default settings for tests."""
settings = get_settings(database_url="sqlite:///:memory:")
database_path = tmp_path / "tests.db"
settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(database_path)),
environment="test",
)
db_url = get_database_url(settings)
if Path(str(get_engine(database_url=db_url).url.database)).resolve() != database_path.resolve():
raise RuntimeError(f"Refusing to initialize destructive test fixtures against {db_url}")
await dispose_session_factory(db_url)
engine = get_engine(database_url=db_url)
+36
View File
@@ -17,6 +17,7 @@ from transcription.services.jobs import JobDeleteBlockedError
from transcription.services.jobs import JobNotFoundError
from transcription.services.jobs import JobResubmitBlockedError
from transcription.services.jobs import JobService
from transcription.services.sources import SourceService
class TestJobService:
@@ -232,6 +233,41 @@ class TestJobService:
with pytest.raises(JobNotFoundError):
await job_service.read_job(job_id=job.id)
@pytest.mark.asyncio
async def test_delete_job_and_evidence_removes_attempts_but_preserves_source(
self,
job_service: JobService,
document_service: DocumentService,
):
source_service = SourceService(session_factory=job_service.session_factory)
document = await document_service.create_document(Document(name="evidence-delete-doc"))
job = await job_service.create_job(Job(document_id=document.id, status=JobStatus.FAILED))
source = await source_service.create_source(
Source(
document_id=document.id,
page_number=1,
upload_name="evidence.jpg",
filename="evidence.jpg",
file_path="/uploads/evidence.jpg",
file_hash="d" * 64,
file_size_bytes=1,
)
)
await source_service.create_job_source(JobSource(job_id=job.id, source_id=source.id))
await source_service.update_job_source_transcription(
job_id=job.id,
source_id=source.id,
text=None,
error_detail="fixture failure",
)
await job_service.delete_job_and_evidence(job_id=job.id)
with pytest.raises(JobNotFoundError):
await job_service.read_job(job_id=job.id)
assert await source_service.list_execution_attempts(job_id=job.id) == []
assert (await source_service.read_source(source.id)).id == source.id
@pytest.mark.asyncio
async def test_cancel_job_marks_non_transcribed_sources_failed(
self,
+22
View File
@@ -62,6 +62,28 @@ async def test_get_session_yields_async_session(tmp_path):
await dispose_database_runtime()
@pytest.mark.asyncio
async def test_runtime_rejects_reinitialization_for_different_database(tmp_path):
"""An existing process runtime cannot silently switch database targets."""
first_settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "first.db")),
environment="test",
)
second_settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "second.db")),
environment="test",
)
initialize_database_runtime(settings=first_settings)
try:
with pytest.raises(RuntimeError, match="already initialized for a different database"):
initialize_database_runtime(settings=second_settings)
finally:
await dispose_database_runtime()
@pytest.mark.asyncio
async def test_create_all_seeds_default_registry_rows(tmp_path):
settings = Settings(
+46
View File
@@ -33,6 +33,18 @@ from transcription.services.sources import TranscriptionError
from transcription.services.sources import transcribe_document_image
class _ChunkedAsyncStream(httpx.AsyncByteStream):
def __init__(self, chunks: list[bytes]):
self._chunks = chunks
async def __aiter__(self):
for chunk in self._chunks:
yield chunk
async def aclose(self):
return
@pytest.mark.asyncio
async def test_openrouter_captures_exact_transport_and_secret_safe_manifest():
response_body = (
@@ -87,6 +99,39 @@ async def test_openrouter_captures_exact_transport_and_secret_safe_manifest():
assert result.request_manifest.omitted_optional_parameters == ("temperature", "top_p")
@pytest.mark.asyncio
async def test_openrouter_captures_body_consumed_as_sdk_stream():
response_body = (
b'{"id":"gen-2","created":1,"model":"vendor/model","object":"chat.completion",'
b'"system_fingerprint":null,"choices":[{"index":0,"finish_reason":"stop",'
b'"message":{"role":"assistant","content":"Transcript"}}],'
b'"unknown_streamed_field":true}'
)
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
stream=_ChunkedAsyncStream([response_body[:23], response_body[23:61], response_body[61:]]),
headers={"Content-Type": "application/json"},
request=request,
)
provider = OpenRouterTranscriptionProvider(
settings=Settings(openrouter_api_key="test-key"),
async_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
)
result = await provider.transcribe(
prompt_text="Literal prompt",
image_bytes=b"source-bytes",
mime_type="image/png",
)
assert result.transport_evidence is not None
assert result.transport_evidence.body == response_body
assert b'"unknown_streamed_field":true' in result.transport_evidence.body
await provider.aclose()
@pytest.mark.asyncio
async def test_openrouter_failure_retains_safe_response_evidence():
async def handler(request: httpx.Request) -> httpx.Response:
@@ -113,6 +158,7 @@ async def test_openrouter_failure_retains_safe_response_evidence():
assert evidence.status_code == 500
assert evidence.body == b'{"error":{"message":"provider unavailable"}}'
assert evidence.safe_headers == {"content-type": "application/json", "retry-after": "2"}
assert str(failure.value) == "OpenRouter request failed with HTTP 500: provider unavailable"
@pytest.mark.asyncio
+7
View File
@@ -47,6 +47,13 @@ def app_client(tmp_path_factory: pytest.TempPathFactory) -> AsyncGenerator[tuple
app = create_app(settings=settings)
with TestClient(app) as client:
runtime_path = Path(str(app.state.runtime.engine.url.database)).resolve()
expected_path = Path(settings.database.path).resolve()
if runtime_path != expected_path:
raise RuntimeError(
"Refusing to initialize destructive UI fixtures against "
f"{runtime_path}; expected {expected_path}"
)
yield app, client
+2 -1
View File
@@ -164,5 +164,6 @@ class TestJobsPageRendering:
assert response.status_code == 200
assert "Delete Processing Job" in response.text
assert "Delete job permanently" in response.text
assert "Delete job and evidence" in response.text
assert "immutable execution evidence" in response.text
assert "Delete is blocked" not in response.text