V4.2 Updated what ai_raw_response data is being captured. The changes were more extensive than I expected.

This commit is contained in:
Jim Lancaster
2026-08-14 07:21:15 -05:00
parent 28811d79ce
commit 6bd4cbb0a7
29 changed files with 2091 additions and 130 deletions
+16
View File
@@ -1,5 +1,6 @@
"""Tests for transcription.providers.openrouter."""
import asyncio
from types import SimpleNamespace
import pytest
@@ -143,6 +144,21 @@ class TestOpenRouterProviderTranscribe:
mime_type="image/png",
)
@pytest.mark.asyncio
async def test_preserves_caller_cancellation(self):
"""Caller and shutdown cancellation must not be relabeled as a timeout."""
provider = OpenRouterTranscriptionProvider(
settings=Settings(openrouter_api_key="test-key"),
client=_FakeClient(error=asyncio.CancelledError()),
)
with pytest.raises(asyncio.CancelledError):
await provider.transcribe(
prompt_text="Prompt body",
image_bytes=b"img-bytes",
mime_type="image/png",
)
@pytest.mark.asyncio
async def test_sends_pdf_as_file_content(self):
"""PDF payloads use OpenRouter's file content contract."""
+1 -1
View File
@@ -365,7 +365,7 @@ class TestJobService:
assert failed_entry.status == JobSourceStatus.PENDING
assert failed_entry.error_detail is None
assert failed_entry.source is not None
assert failed_entry.source.raw_transcription is None
assert failed_entry.source.raw_transcription == "existing text"
assert transcribed_entry.status == JobSourceStatus.TRANSCRIBED
@pytest.mark.asyncio
@@ -1,5 +1,6 @@
"""Reliability tests for worker workflow timeout behavior."""
import asyncio
from pathlib import Path
from uuid import uuid4
@@ -12,6 +13,7 @@ from transcription.db.models import JobSource
from transcription.db.models import JobSourceStatus
from transcription.db.models import JobStatus
from transcription.db.models import Source
from transcription.providers.base import TranscriptionResult
from transcription.services import ServiceBundle
from transcription.services.workflows import process_queued_job
@@ -88,3 +90,64 @@ class TestWorkflowReliability:
assert result.error_detail is not None
assert "timed out" in result.error_detail.lower()
assert "20.0s" in result.error_detail
@pytest.mark.asyncio
async def test_completed_page_is_committed_before_next_provider_call_finishes(
self,
default_session_factory,
monkeypatch,
):
services = ServiceBundle(
documents=ServiceBundle().documents.__class__(session_factory=default_session_factory),
jobs=ServiceBundle().jobs.__class__(session_factory=default_session_factory),
sources=ServiceBundle().sources.__class__(session_factory=default_session_factory),
people=ServiceBundle().people.__class__(session_factory=default_session_factory),
)
async with services.jobs._session_scope() as session:
document = Document(id=uuid4(), name="durability-doc")
session.add(document)
await session.flush()
job = Job(document_id=document.id, status=JobStatus.QUEUED)
session.add(job)
await session.flush()
for page_number in (1, 2):
source = Source(
document_id=document.id,
page_number=page_number,
upload_name=f"page-{page_number}.jpg",
filename=f"page-{page_number}.jpg",
file_path=str(Path("tests/fixtures/images/real/Book Two - page 02.jpg")),
file_hash=str(page_number) * 64,
file_size_bytes=1,
)
session.add(source)
await session.flush()
session.add(JobSource(job_id=job.id, source_id=source.id))
await session.commit()
loaded = await services.jobs.read_job(job_id=job.id, session=session)
second_started = asyncio.Event()
release_second = asyncio.Event()
call_count = 0
async def _transcribe(image_path, **kwargs):
nonlocal call_count
_ = (image_path, kwargs)
call_count += 1
if call_count == 2:
second_started.set()
await release_second.wait()
return TranscriptionResult(text=f"page {call_count}", provider="fixture", model="model")
monkeypatch.setattr("transcription.services.workflows.transcribe_document_image", _transcribe)
task = asyncio.create_task(process_queued_job(job=loaded, services=services))
await asyncio.wait_for(second_started.wait(), timeout=2)
attempts = await services.sources.list_execution_attempts(job_id=job.id)
assert len(attempts) == 1
assert attempts[0].raw_transcription == "page 1"
release_second.set()
result = await task
assert result is not None
assert result.status == JobStatus.TRANSCRIBED
+52
View File
@@ -12,6 +12,7 @@ from transcription.db import create_all
from transcription.db import dispose_database_runtime
from transcription.db import initialize_database_runtime
from transcription.db import session_scope
from transcription.db import upgrade_schema
from transcription.db.models import DocumentType
from transcription.db.models import PersonRole
@@ -38,6 +39,8 @@ async def test_create_all_creates_expected_tables(tmp_path):
assert "job" in table_names
assert "source" in table_names
assert "job_source" in table_names
assert "execution_attempt" in table_names
assert "processing_artifact" in table_names
assert "revision" not in table_names
finally:
await dispose_database_runtime()
@@ -112,6 +115,55 @@ async def test_create_all_upgrades_existing_person_table_for_family_search(tmp_p
await dispose_database_runtime()
@pytest.mark.asyncio
async def test_v42_upgrade_adds_evidence_tables_without_rewriting_legacy_snapshot(tmp_path):
settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "v42-upgrade.db")),
environment="test",
)
runtime = initialize_database_runtime(settings=settings)
try:
async with runtime.engine.begin() as connection:
await connection.execute(text("CREATE TABLE job (id CHAR(32) PRIMARY KEY NOT NULL)"))
await connection.execute(text("CREATE TABLE source (id CHAR(32) PRIMARY KEY NOT NULL)"))
await connection.execute(
text(
"CREATE TABLE job_source ("
"id CHAR(32) PRIMARY KEY NOT NULL, "
"job_id CHAR(32) NOT NULL, "
"source_id CHAR(32) NOT NULL, "
"raw_api_response JSON"
")"
)
)
await connection.execute(text("INSERT INTO job (id) VALUES ('job-1')"))
await connection.execute(text("INSERT INTO source (id) VALUES ('source-1')"))
await connection.execute(
text(
"INSERT INTO job_source (id, job_id, source_id, raw_api_response) "
"VALUES ('link-1', 'job-1', 'source-1', :snapshot)"
),
{"snapshot": '{"legacy":true}'},
)
await upgrade_schema(engine=runtime.engine)
await upgrade_schema(engine=runtime.engine)
async with runtime.engine.connect() as connection:
table_names = set(await connection.run_sync(lambda c: inspect(c).get_table_names()))
legacy_snapshot = (
await connection.execute(
text("SELECT raw_api_response FROM job_source WHERE id = 'link-1'")
)
).scalar_one()
assert {"execution_attempt", "processing_artifact"}.issubset(table_names)
assert "legacy" in legacy_snapshot
finally:
await dispose_database_runtime()
def test_bootstrap_policy_production_defaults_false():
settings = Settings(openrouter_api_key="test-key", environment="production")
assert settings.should_bootstrap_schema is False
+356
View File
@@ -0,0 +1,356 @@
"""Focused V4.2 evidence, integrity, and benchmark tests."""
from __future__ import annotations
import hashlib
import json
from datetime import UTC
from datetime import datetime
from uuid import uuid4
import httpx
import pytest
from transcription.benchmarking import EditorialAssessment
from transcription.benchmarking import score_transcription
from transcription.config import Settings
from transcription.db.models import Document
from transcription.db.models import Job
from transcription.db.models import JobSource
from transcription.db.models import JobSourceStatus
from transcription.db.models import ProcessingArtifact
from transcription.db.models import Source
from transcription.providers.base import ProviderError
from transcription.providers.base import TranscriptionResult
from transcription.providers.evidence import SourceEvidenceReference
from transcription.providers.openrouter import OpenRouterTranscriptionProvider
from transcription.services.documents import DocumentService
from transcription.services.jobs import JobDeleteBlockedError
from transcription.services.jobs import JobService
from transcription.services.sources import SourceDeleteBlockedError
from transcription.services.sources import SourceService
from transcription.services.sources import TranscriptionError
from transcription.services.sources import transcribe_document_image
@pytest.mark.asyncio
async def test_openrouter_captures_exact_transport_and_secret_safe_manifest():
response_body = (
b'{"id":"gen-1","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_transport_field":{"retained":true}}'
)
async def handler(request: httpx.Request) -> httpx.Response:
assert request.headers["authorization"] == "Bearer test-key"
return httpx.Response(
200,
content=response_body,
headers={
"Content-Type": "application/json",
"X-Request-Id": "req-123",
"Set-Cookie": "must-not-persist=1",
},
request=request,
)
async_client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
provider = OpenRouterTranscriptionProvider(
settings=Settings(openrouter_api_key="test-key"),
async_client=async_client,
)
result = await provider.transcribe(
prompt_text="Literal prompt",
image_bytes=b"source-bytes",
mime_type="image/png",
source_reference=SourceEvidenceReference(
source_id=uuid4(),
digest_sha256=hashlib.sha256(b"source-bytes").hexdigest(),
byte_size=len(b"source-bytes"),
media_type="image/png",
page_number=1,
),
)
assert result.transport_evidence is not None
assert result.transport_evidence.body == response_body
assert result.transport_evidence.safe_headers == {
"content-type": "application/json",
"x-request-id": "req-123",
}
assert b'"unknown_transport_field":{"retained":true}' in result.transport_evidence.body
assert result.request_manifest is not None
serialized_manifest = json.dumps(result.request_manifest.model_dump(mode="json"))
assert "data:image/png;base64" not in serialized_manifest
assert "test-key" not in serialized_manifest
assert result.request_manifest.omitted_optional_parameters == ("temperature", "top_p")
@pytest.mark.asyncio
async def test_openrouter_failure_retains_safe_response_evidence():
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
500,
content=b'{"error":{"message":"provider unavailable"}}',
headers={"Content-Type": "application/json", "Retry-After": "2", "Set-Cookie": "secret=1"},
request=request,
)
provider = OpenRouterTranscriptionProvider(
settings=Settings(openrouter_api_key="test-key"),
async_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
)
with pytest.raises(ProviderError) as failure:
await provider.transcribe(
prompt_text="Literal prompt",
image_bytes=b"source-bytes",
mime_type="image/png",
)
evidence = failure.value.transport_evidence
assert evidence is not None
assert evidence.status_code == 500
assert evidence.body == b'{"error":{"message":"provider unavailable"}}'
assert evidence.safe_headers == {"content-type": "application/json", "retry-after": "2"}
@pytest.mark.asyncio
async def test_openrouter_does_not_reuse_prior_response_on_connection_failure():
call_count = 0
async def handler(request: httpx.Request) -> httpx.Response:
nonlocal call_count
call_count += 1
if call_count == 1:
return httpx.Response(500, content=b'{"error":"first"}', request=request)
raise httpx.ConnectError("connection failed", request=request)
provider = OpenRouterTranscriptionProvider(
settings=Settings(openrouter_api_key="test-key"),
async_client=httpx.AsyncClient(transport=httpx.MockTransport(handler)),
)
with pytest.raises(ProviderError) as first_failure:
await provider.transcribe(prompt_text="First", image_bytes=b"one", mime_type="image/png")
with pytest.raises(ProviderError) as second_failure:
await provider.transcribe(prompt_text="Second", image_bytes=b"two", mime_type="image/png")
assert first_failure.value.transport_evidence is not None
assert second_failure.value.transport_evidence is not None
assert second_failure.value.transport_evidence.response_received is False
assert second_failure.value.transport_evidence.body is None
await provider.aclose()
@pytest.mark.asyncio
async def test_attempts_are_append_only_and_exported_with_integrity(default_session_factory):
documents = DocumentService(session_factory=default_session_factory)
jobs = JobService(session_factory=default_session_factory)
sources = SourceService(session_factory=default_session_factory)
document = await documents.create_document(Document(name="Evidence"))
job = await jobs.create_job(Job(document_id=document.id))
source = await sources.create_source(
Source(
document_id=document.id,
page_number=1,
upload_name="page.png",
filename="page.png",
file_path="page.png",
file_hash="a" * 64,
file_size_bytes=10,
)
)
await sources.create_job_source(JobSource(job_id=job.id, source_id=source.id))
now = datetime.now(UTC)
await sources.update_job_source_transcription(
job_id=job.id,
source_id=source.id,
text=None,
error_detail="first failed",
error_category="external_provider_error",
failure_phase="connection",
started_at=now,
finished_at=now,
)
await sources.update_job_source_transcription(
job_id=job.id,
source_id=source.id,
text="second succeeded",
provider="openrouter",
model="vendor/model",
started_at=now,
finished_at=now,
)
attempts = await sources.list_execution_attempts(source_id=source.id)
assert [attempt.attempt_number for attempt in attempts] == [1, 2]
assert attempts[0].status == JobSourceStatus.FAILED
assert attempts[0].error_detail == "first failed"
assert attempts[1].status == JobSourceStatus.TRANSCRIBED
assert attempts[1].raw_transcription == "second succeeded"
payload = {"words": [{"text": "second", "polygon": [0, 0, 1, 1]}]}
payload_bytes = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
artifact = await sources.create_processing_artifact(
ProcessingArtifact(
source_id=source.id,
execution_attempt_id=attempts[1].id,
artifact_type="ocr.words",
media_type="application/json",
schema_name="example.ocr.words",
schema_version="1",
producer="fixture",
producer_version="1",
inline_payload=payload,
payload_sha256=hashlib.sha256(payload_bytes).hexdigest(),
byte_size=len(payload_bytes),
coordinate_metadata={
"units": "normalized",
"origin": "top-left",
"width": 1,
"height": 1,
"transformations": [],
},
)
)
export = await sources.build_evidence_export(source_id=source.id)
assert export["source"]["digest_sha256"] == "a" * 64
assert [item["attempt_number"] for item in export["attempts"]] == [1, 2]
assert export["artifacts"][0]["id"] == str(artifact.id)
assert "file_path" not in json.dumps(export)
with pytest.raises(JobDeleteBlockedError):
await jobs.delete_job_with_guardrails(job_id=job.id)
detail = await sources.read_source_detail(source.id)
latest_job_source = detail.latest_job_source
assert latest_job_source is not None
assert latest_job_source.execution_attempts == []
assert detail.processing_artifacts == []
latest_attempt = await sources.read_latest_execution_attempt(job_source_id=latest_job_source.id)
assert latest_attempt is not None
assert latest_attempt.attempt_number == 2
def test_benchmark_scoring_preserves_literal_differences():
score = score_transcription(
execution_attempt_id=uuid4(),
reference="Farm house",
candidate="farm house",
assessment=EditorialAssessment(silent_normalizations=1),
latency_ms=125,
)
assert score.character_edits == 1
assert score.word_edits == 1
assert score.assessment.silent_normalizations == 1
@pytest.mark.asyncio
async def test_large_json_artifact_uses_constrained_atomic_storage(
default_session_factory,
tmp_path,
):
settings = Settings(
openrouter_api_key="test-key",
artifact_dir=tmp_path / "artifacts",
artifact_inline_threshold_bytes=10,
)
documents = DocumentService(session_factory=default_session_factory, settings=settings)
sources = SourceService(session_factory=default_session_factory, settings=settings)
document = await documents.create_document(Document(name="External Artifact"))
source = await sources.create_source(
Source(
document_id=document.id,
page_number=1,
upload_name="page.png",
filename="page.png",
file_path="page.png",
file_hash="b" * 64,
file_size_bytes=10,
)
)
artifact = await sources.create_json_artifact(
source_id=source.id,
execution_attempt_id=None,
artifact_type="ocr.layout",
schema_name="example.layout",
schema_version="1",
producer="fixture",
producer_version="1",
payload={"blocks": [{"text": "long enough to be external"}]},
)
assert artifact.inline_payload is None
assert artifact.external_reference is not None
stored_path = settings.artifact_dir / artifact.external_reference
assert stored_path.is_file()
assert hashlib.sha256(stored_path.read_bytes()).hexdigest() == artifact.payload_sha256
with pytest.raises(SourceDeleteBlockedError):
await sources.delete_unlinked_source(source_id=source.id)
stored_path.write_bytes(b'{"tampered":true}')
with pytest.raises(TranscriptionError, match="integrity verification"):
await sources.build_evidence_export(source_id=source.id)
@pytest.mark.asyncio
async def test_rejects_inline_artifact_with_incorrect_integrity(default_session_factory):
documents = DocumentService(session_factory=default_session_factory)
sources = SourceService(session_factory=default_session_factory)
document = await documents.create_document(Document(name="Inline Integrity"))
source = await sources.create_source(
Source(
document_id=document.id,
page_number=1,
upload_name="page.png",
filename="page.png",
file_path="page.png",
file_hash="d" * 64,
file_size_bytes=10,
)
)
with pytest.raises(TranscriptionError, match="integrity verification"):
await sources.create_processing_artifact(
ProcessingArtifact(
source_id=source.id,
artifact_type="ocr.words",
media_type="application/json",
schema_name="example.words",
schema_version="1",
producer="fixture",
producer_version="1",
inline_payload={"words": []},
payload_sha256="0" * 64,
byte_size=1,
)
)
@pytest.mark.asyncio
async def test_standalone_transcription_closes_locally_created_provider(tmp_path, monkeypatch):
image_path = tmp_path / "page.png"
image_path.write_bytes(b"image")
class _Provider:
closed = False
async def transcribe(self, **kwargs):
_ = kwargs
return TranscriptionResult(text="result", provider="fixture", model="model")
async def aclose(self):
self.closed = True
provider = _Provider()
monkeypatch.setattr("transcription.services.sources.get_transcription_provider", lambda **_kwargs: provider)
result = await transcribe_document_image(
image_path,
prompt_text="Prompt",
settings=Settings(openrouter_api_key="test-key"),
)
assert result.text == "result"
assert provider.closed is True
+25 -1
View File
@@ -3,6 +3,7 @@ import logging
import pytest
from transcription.worker import process_next_queued_job
from transcription.worker import run_worker_loop
@@ -26,4 +27,27 @@ async def test_run_worker_loop_survives_process_next_exception(monkeypatch, capl
await run_worker_loop(stop_event=stop_event, poll_interval_seconds=0)
assert calls == 2
assert "Worker loop exception" in caplog.text
assert "Worker loop exception" in caplog.text
@pytest.mark.asyncio
async def test_process_next_closes_initialized_provider(monkeypatch):
closed = False
class _Sources:
async def aclose(self):
nonlocal closed
closed = True
services = type("_Services", (), {"sources": _Sources()})()
monkeypatch.setattr("transcription.worker.ServiceBundle", lambda: services)
async def _no_job(*, services, session):
_ = (services, session)
return False
monkeypatch.setattr("transcription.worker.process_next_queued_job_workflow", _no_job)
assert await process_next_queued_job() is False
assert closed is True
+44
View File
@@ -11,6 +11,8 @@ from transcription.db.models import Job
from transcription.db.models import JobSourceStatus
from transcription.db.models import JobStatus
from transcription.db.models import Source
from transcription.providers.evidence import TransportEvidence
from transcription.services.sources import SourceService
# --- Unit Tests for Model @property Definitions ---
@@ -223,6 +225,48 @@ class TestSourcesPageRendering:
assert "finish_reason" in response.text
assert "response-123" in response.text
@pytest.mark.asyncio
async def test_source_detail_separates_v42_evidence_layers(self, app_client, seed_job):
app, client = app_client
job_id = await seed_job(filename="evidence-source.png")
async with session_scope(session_factory=app.state.runtime.session_factory) as session:
job = await session.get(Job, job_id)
assert job is not None
source = (
await session.exec(select(Source).where(Source.document_id == job.document_id))
).first()
assert source is not None
source_id = source.id
service = SourceService(session_factory=app.state.runtime.session_factory)
await service.update_job_source_transcription(
job_id=job_id,
source_id=source_id,
text="V4.2 transcription",
raw_api_response={"id": "sdk-snapshot"},
ai_metadata={"finish_reason": "stop"},
provider="openrouter",
model="vendor/model",
transport_evidence=TransportEvidence(
response_received=True,
status_code=200,
body=b'{"id":"transport-response"}',
safe_headers={"content-type": "application/json"},
content_type="application/json",
),
)
response = client.get(f"/ui/sources/{source_id}")
assert response.status_code == 200
assert "Export Evidence" in response.text
assert "Request Manifest" in response.text
assert "Transport Response" in response.text
assert "OpenRouter SDK Response Snapshot" in response.text
assert "Normalized Metadata" in response.text
assert "Software Context" in response.text
assert "Derived Artifacts" in response.text
@pytest.mark.asyncio
async def test_source_delete_page_blocks_when_source_is_job_linked(
self, app_client, seed_job