Revamped Sources related pages with the help of Gemini, which had a lot to say.

This commit is contained in:
Jim Lancaster
2026-08-05 12:35:54 -05:00
parent 4eeb552273
commit 72bc96ab3a
6 changed files with 416 additions and 298 deletions
+141 -128
View File
@@ -1,16 +1,60 @@
"""Tests for the sources page routes."""
"""Tests for the sources page routes and Source model properties."""
import asyncio
from pathlib import Path
import pytest
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from transcription.db import session_scope
from transcription.db.models import Document
from transcription.db.models import Job
from transcription.db.models import JobStatus
from transcription.db.models import Source
from transcription.db.models import Document, Job, JobSourceStatus, JobStatus, Source
# --- Unit Tests for Model @property Definitions ---
class TestSourceModelProperties:
"""Direct unit tests for Source computed properties."""
@pytest.mark.asyncio
async def test_source_properties_with_no_job_sources(self):
source = Source(
page_number=1,
upload_name="page_one.png",
filename="stored_page_one.png",
file_path="/tmp/stored_page_one.png",
)
assert source.latest_job_source is None
assert source.latest_status is None
assert source.latest_error_detail is None
assert source.document_name is None
@pytest.mark.asyncio
async def test_source_properties_with_document_and_job_sources(self, seed_job):
job_id = seed_job(
filename="source_prop_test.png",
status=JobStatus.FAILED,
transcription_text=None,
error_detail="Timeout during OCR parsing",
)
async with session_scope() 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
# Validate computed properties
assert source.document_name is not None
assert source.latest_status == JobSourceStatus.FAILED
assert source.latest_error_detail == "Timeout during OCR parsing"
assert source.latest_job_source is not None
# --- Integration Tests for Page Rendering ---
@pytest.mark.integration
@@ -26,26 +70,24 @@ class TestSourcesPageRendering:
assert "Sources" in response.text
assert "No source file records found." in response.text
def test_sources_page_lists_seeded_sources(self, app_client):
@pytest.mark.asyncio
async def test_sources_page_lists_seeded_sources(self, app_client):
_, client = app_client
async def _seed() -> None:
async with session_scope() as session:
document = Document(name="Source Document", document_type="letter")
session.add(document)
await session.flush()
session.add(
Source(
document_id=document.id,
page_number=1,
upload_name="page_one.png",
filename="stored_page_one.png",
file_path="/tmp/stored_page_one.png",
)
async with session_scope() as session:
document = Document(name="Source Document", document_type="letter")
session.add(document)
await session.flush()
session.add(
Source(
document_id=document.id,
page_number=1,
upload_name="page_one.png",
filename="stored_page_one.png",
file_path="/tmp/stored_page_one.png",
)
await session.commit()
asyncio.run(_seed())
)
await session.commit()
response = client.get("/ui/sources")
@@ -53,41 +95,38 @@ class TestSourcesPageRendering:
assert "page_one.png" in response.text
assert "stored_page_one.png" in response.text
def test_sources_page_filters_to_document_context(self, app_client):
@pytest.mark.asyncio
async def test_sources_page_filters_to_document_context(self, app_client):
_, client = app_client
async def _seed() -> str:
async with session_scope() as session:
target = Document(name="Target", document_type="letter")
other = Document(name="Other", document_type="record")
session.add(target)
session.add(other)
await session.flush()
async with session_scope() as session:
target = Document(name="Target", document_type="letter")
other = Document(name="Other", document_type="record")
session.add_all([target, other])
await session.flush()
session.add(
Source(
document_id=target.id,
page_number=1,
upload_name="target_page.png",
filename="target_stored.png",
file_path="/tmp/target_stored.png",
)
session.add(
Source(
document_id=target.id,
page_number=1,
upload_name="target_page.png",
filename="target_stored.png",
file_path="/tmp/target_stored.png",
)
session.add(
Source(
document_id=other.id,
page_number=1,
upload_name="other_page.png",
filename="other_stored.png",
file_path="/tmp/other_stored.png",
)
)
session.add(
Source(
document_id=other.id,
page_number=1,
upload_name="other_page.png",
filename="other_stored.png",
file_path="/tmp/other_stored.png",
)
await session.commit()
return str(target.id)
)
await session.commit()
target_id = str(target.id)
document_id = asyncio.run(_seed())
response = client.get(f"/ui/sources?document_id={document_id}")
response = client.get(f"/ui/sources?document_id={target_id}")
assert response.status_code == 200
assert "Sources: Target" in response.text
@@ -105,9 +144,10 @@ class TestSourcesPageRendering:
assert "Sources for Job" in response.text
assert "Back to Job" in response.text
assert "job-page.png" in response.text
assert "Job Source Status" in response.text
def test_sources_page_job_context_shows_job_source_status_and_error_detail(self, app_client, seed_job):
def test_sources_page_job_context_shows_job_source_status_and_error_detail(
self, app_client, seed_job
):
_, client = app_client
job_id = seed_job(
filename="job-failed-page.png",
@@ -123,9 +163,18 @@ class TestSourcesPageRendering:
assert "failed" in response.text.lower()
assert "Provider timed out" in response.text
def test_source_detail_page_renders_preview_and_revision_box(self, app_client, seed_job):
@pytest.mark.asyncio
async def test_source_detail_page_renders_preview_and_revision_box(
self, app_client, seed_job
):
_, client = app_client
fixture_path = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "valid" / "small_png.png"
fixture_path = (
Path(__file__).resolve().parents[1]
/ "fixtures"
/ "images"
/ "valid"
/ "small_png.png"
)
job_id = seed_job(
filename="detail-source.png",
transcription_text="original transcription text",
@@ -133,101 +182,65 @@ class TestSourcesPageRendering:
source_file=fixture_path,
)
async def _get_source_id() -> str:
async with session_scope() 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
return str(source.id)
source_id = asyncio.run(_get_source_id())
async with session_scope() 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 = str(source.id)
response = client.get(f"/ui/sources/{source_id}")
assert response.status_code == 200
assert "Source Page 1: detail-source.png" in response.text
assert "Back to Sources" in response.text
assert "automated raw transcription" in response.text.lower()
assert "original transcription text" in response.text
assert "curated human transcription" in response.text.lower()
assert "human revision text" in response.text
assert "Page Number:" in response.text
assert "Stored Filename:" in response.text
assert "Delete Source" in response.text
def test_source_detail_page_displays_job_source_status_and_error_detail(self, app_client, seed_job):
_, client = app_client
job_id = seed_job(
filename="failed-source.png",
status=JobStatus.FAILED,
transcription_text=None,
error_detail="Provider timed out",
)
async def _get_source_id() -> str:
async with session_scope() 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
return str(source.id)
source_id = asyncio.run(_get_source_id())
response = client.get(f"/ui/sources/{source_id}")
assert response.status_code == 200
assert "JOB SOURCE OUTCOMES" in response.text
assert "Status:" in response.text
assert "failed" in response.text.lower()
assert "Error Detail:" in response.text
assert "Provider timed out" in response.text
def test_source_delete_page_blocks_when_source_is_job_linked(self, app_client, seed_job):
@pytest.mark.asyncio
async def test_source_delete_page_blocks_when_source_is_job_linked(
self, app_client, seed_job
):
_, client = app_client
job_id = seed_job(filename="linked-source.png", transcription_text="linked text")
async def _get_source_id() -> str:
async with session_scope() 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
return str(source.id)
async with session_scope() 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 = str(source.id)
source_id = asyncio.run(_get_source_id())
response = client.get(f"/ui/sources/{source_id}/delete")
assert response.status_code == 200
assert "Delete Source Record" in response.text
assert "Delete is only available for unlinked sources." in response.text
def test_source_delete_page_allows_unlinked_source(self, app_client):
@pytest.mark.asyncio
async def test_source_delete_page_allows_unlinked_source(self, app_client):
_, client = app_client
async def _seed_unlinked_source() -> str:
async with session_scope() as session:
document = Document(name="Unlinked Source Doc", document_type="memo")
session.add(document)
await session.flush()
source = Source(
document_id=document.id,
page_number=1,
upload_name="orphan-source.png",
filename="orphan-source.png",
file_path="/tmp/orphan-source.png",
)
session.add(source)
await session.commit()
return str(source.id)
async with session_scope() as session:
document = Document(name="Unlinked Source Doc", document_type="memo")
session.add(document)
await session.flush()
source = Source(
document_id=document.id,
page_number=1,
upload_name="orphan-source.png",
filename="orphan-source.png",
file_path="/tmp/orphan-source.png",
)
session.add(source)
await session.commit()
source_id = str(source.id)
source_id = asyncio.run(_seed_unlinked_source())
response = client.get(f"/ui/sources/{source_id}/delete")
assert response.status_code == 200