generated from john/python-template
V4.2 complete
This commit is contained in:
+12
-3
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user