generated from john/python-template
@@ -18,6 +18,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 normalize_legacy_status_spellings
|
||||
from transcription.db import reconcile_legacy_job_source_columns
|
||||
from transcription.db import session_scope
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentType
|
||||
@@ -211,6 +212,47 @@ async def test_normalize_legacy_status_spellings_repairs_job_source_status_rows(
|
||||
await dispose_database_runtime()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reconcile_legacy_job_source_columns_drops_executed_at(tmp_path):
|
||||
settings = Settings(
|
||||
openrouter_api_key="test-key",
|
||||
database=SqliteSettings(path=str(tmp_path / "legacy-column.db")),
|
||||
environment="test",
|
||||
)
|
||||
runtime = initialize_database_runtime(settings=settings)
|
||||
|
||||
try:
|
||||
await create_all(engine=runtime.engine)
|
||||
async with runtime.engine.begin() as connection:
|
||||
await connection.execute(text("PRAGMA foreign_keys=OFF"))
|
||||
await connection.execute(text('alter table "job_source" rename to "job_source_current"'))
|
||||
await connection.execute(
|
||||
text(
|
||||
'create table "job_source" ('
|
||||
'id char(32) not null primary key, '
|
||||
'job_id char(32) not null, '
|
||||
'source_id char(32) not null, '
|
||||
'status varchar(11) not null, '
|
||||
'executed_at datetime not null, '
|
||||
'constraint "uq_job_source_job_source" unique ("job_id", "source_id"), '
|
||||
'foreign key("job_id") references "job" ("id"), '
|
||||
'foreign key("source_id") references "source" ("id")'
|
||||
")"
|
||||
)
|
||||
)
|
||||
await connection.execute(text('drop table "job_source_current"'))
|
||||
await connection.execute(text("PRAGMA foreign_keys=ON"))
|
||||
|
||||
dropped = await reconcile_legacy_job_source_columns(engine=runtime.engine)
|
||||
assert dropped == 1
|
||||
|
||||
async with runtime.engine.connect() as connection:
|
||||
columns = await connection.run_sync(lambda c: [col["name"] for col in inspect(c).get_columns("job_source")])
|
||||
assert "executed_at" not in columns
|
||||
finally:
|
||||
await dispose_database_runtime()
|
||||
|
||||
|
||||
def test_metadata_has_no_unresolvable_table_cycle():
|
||||
"""create_all must be able to order every table, including on PostgreSQL."""
|
||||
with warnings.catch_warnings():
|
||||
|
||||
@@ -131,7 +131,7 @@ class TestDocumentsPageRendering:
|
||||
assert "Edit Document" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_jobs_page_renders_job_links(self, app_client):
|
||||
async def test_document_jobs_page_redirects_to_filtered_jobs(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async with session_scope() as session:
|
||||
@@ -143,13 +143,14 @@ class TestDocumentsPageRendering:
|
||||
session.add(job)
|
||||
await session.commit()
|
||||
doc_id = str(doc.id)
|
||||
job_id = str(job.id)
|
||||
_ = str(job.id)
|
||||
|
||||
response = client.get(f"/ui/documents/{doc_id}/jobs")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Jobs for Doc With Job" in response.text
|
||||
assert f"Job ID: {job_id}" in response.text
|
||||
assert "Jobs for Document" in response.text
|
||||
assert "Create job" not in response.text
|
||||
assert "Refresh" not in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_document_edit_page_prefills_existing_values(self, app_client, seed_person_and_document):
|
||||
|
||||
@@ -3,6 +3,7 @@ from datetime import date
|
||||
from transcription.db.models import Person
|
||||
from transcription.ui.components.formatters import compact_date
|
||||
from transcription.ui.components.formatters import family_search_url
|
||||
from transcription.ui.components.formatters import google_maps_search_url
|
||||
from transcription.ui.components.formatters import person_selector_label
|
||||
|
||||
|
||||
@@ -32,3 +33,9 @@ def test_family_search_url_uses_fixed_person_details_route():
|
||||
assert family_search_url("G8T4-MDQ") == (
|
||||
"https://www.familysearch.org/tree/person/details/G8T4-MDQ"
|
||||
)
|
||||
|
||||
|
||||
def test_google_maps_search_url_encodes_place_query():
|
||||
assert google_maps_search_url("New York, NY") == (
|
||||
"https://www.google.com/maps/search/?api=1&query=New+York%2C+NY"
|
||||
)
|
||||
|
||||
@@ -54,8 +54,38 @@ class TestJobsPageRendering:
|
||||
response = client.get("/ui/jobs")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Document Name" in response.text
|
||||
assert "Source Filename" not in response.text
|
||||
assert "Updated" in response.text
|
||||
assert "Created" not in response.text
|
||||
assert str(job_id) in response.text
|
||||
assert "seeded-document-page.png" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_jobs_page_filters_for_document_context(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async with session_scope() as session:
|
||||
first = Document(name="First Doc")
|
||||
second = Document(name="Second Doc")
|
||||
session.add_all([first, second])
|
||||
await session.flush()
|
||||
first_job = Job(document_id=first.id, status=JobStatus.QUEUED, provider="openai", model="gpt-4o")
|
||||
second_job = Job(document_id=second.id, status=JobStatus.QUEUED, provider="openai", model="gpt-4o")
|
||||
session.add_all([first_job, second_job])
|
||||
await session.commit()
|
||||
|
||||
first_id = str(first.id)
|
||||
first_job_id = str(first_job.id)
|
||||
second_job_id = str(second_job.id)
|
||||
|
||||
response = client.get(f"/ui/jobs?document_id={first_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Jobs for Document" in response.text
|
||||
assert "Create job" not in response.text
|
||||
assert "Refresh" not in response.text
|
||||
assert first_job_id in response.text
|
||||
assert second_job_id not in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_job_create_page_shows_empty_document_warning_when_no_docs(self, app_client):
|
||||
@@ -95,8 +125,11 @@ class TestJobsPageRendering:
|
||||
assert "Execution Logistics".upper() in response.text.upper()
|
||||
assert "openai" in response.text
|
||||
assert "gpt-4o" in response.text
|
||||
assert "View Linked Document" in response.text
|
||||
assert "View Linked Sources" in response.text
|
||||
assert "Document Name:" in response.text
|
||||
assert "Sources:" in response.text
|
||||
assert "View Sources" in response.text
|
||||
assert "View Linked Document" not in response.text
|
||||
assert "View Linked Sources" not in response.text
|
||||
assert "updates automatically while the job is active" in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -24,3 +24,8 @@ class TestPageRegistration:
|
||||
assert sources_response.status_code == 200
|
||||
assert jobs_response.status_code == 200
|
||||
assert settings_response.status_code == 200
|
||||
assert "Document Types" in settings_response.text
|
||||
assert "Person Roles" in settings_response.text
|
||||
assert "Prompts" in settings_response.text
|
||||
assert "Home Page Text" in settings_response.text
|
||||
assert "README.md" not in settings_response.text
|
||||
|
||||
@@ -12,6 +12,7 @@ from transcription.db.models import Document
|
||||
from transcription.db.models import DocumentPerson
|
||||
from transcription.db.models import Person
|
||||
from transcription.db.models import PersonRole
|
||||
from transcription.db.models import Source
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@@ -55,6 +56,9 @@ class TestPeoplePageRendering:
|
||||
assert "Birth date (YYYY-MM-DD)" not in response.text
|
||||
assert "Death date (YYYY-MM-DD)" not in response.text
|
||||
assert "FamilySearch ID" in response.text
|
||||
assert "Auto-fill from FamilySearch" not in response.text
|
||||
assert "Marriage date (FamilySearch)" not in response.text
|
||||
assert "Spouse (FamilySearch)" not in response.text
|
||||
assert "Biography" in response.text
|
||||
assert "Save person" in response.text
|
||||
|
||||
@@ -92,11 +96,16 @@ class TestPeoplePageRendering:
|
||||
assert "1906-12-09" in response.text
|
||||
assert "Death Date:" in response.text
|
||||
assert "1992-01-01" in response.text
|
||||
assert "Open Birth Place in Google Maps" not in response.text
|
||||
assert "Open Death Place in Google Maps" not in response.text
|
||||
assert "google.com/maps/search/?api=1&query=New+York" in response.text
|
||||
assert "google.com/maps/search/?api=1&query=Arlington" in response.text
|
||||
assert "biography" in response.text.lower()
|
||||
assert "Computer pioneer" in response.text
|
||||
assert "Created:" in response.text
|
||||
assert "Updated:" in response.text
|
||||
assert "Open in FamilySearch" in response.text
|
||||
assert "Open in FamilySearch" not in response.text
|
||||
assert "FamilySearch ID:" in response.text
|
||||
assert "familysearch.org/tree/person/details/G8T4-MDQ" in response.text
|
||||
assert "New Document" in response.text
|
||||
assert "No linked documents yet." in response.text
|
||||
@@ -134,6 +143,28 @@ class TestPeoplePageRendering:
|
||||
document = Document(name="Linked Document")
|
||||
session.add_all([person, document])
|
||||
await session.flush()
|
||||
session.add_all(
|
||||
[
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=1,
|
||||
upload_name="linked-page-1.png",
|
||||
filename="linked-page-1.png",
|
||||
file_path="/tmp/linked-page-1.png",
|
||||
file_hash="1" * 64,
|
||||
file_size_bytes=1,
|
||||
),
|
||||
Source(
|
||||
document_id=document.id,
|
||||
page_number=2,
|
||||
upload_name="linked-page-2.png",
|
||||
filename="linked-page-2.png",
|
||||
file_path="/tmp/linked-page-2.png",
|
||||
file_hash="2" * 64,
|
||||
file_size_bytes=1,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
session.add(
|
||||
DocumentPerson(
|
||||
@@ -148,8 +179,27 @@ class TestPeoplePageRendering:
|
||||
response = client.get(f"/ui/people/{person_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Document Name" in response.text
|
||||
assert "Number of Pages" in response.text
|
||||
assert "Linked Document" in response.text
|
||||
assert "Role: Author" in response.text
|
||||
assert "Author" in response.text
|
||||
assert '"page_count":2' in response.text
|
||||
assert "Open" not in response.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_person_detail_page_hides_empty_maiden_name(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
async with session_scope() as session:
|
||||
person = Person(full_name="No Maiden Name")
|
||||
session.add(person)
|
||||
await session.commit()
|
||||
person_id = str(person.id)
|
||||
|
||||
response = client.get(f"/ui/people/{person_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "Maiden Name:" not in response.text
|
||||
|
||||
def test_person_detail_page_handles_invalid_id(self, app_client):
|
||||
_, client = app_client
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Tests for the sources page routes and Source model properties."""
|
||||
|
||||
from datetime import UTC
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -10,6 +12,7 @@ from transcription.db import session_scope
|
||||
from transcription.db.loading import orm_attribute
|
||||
from transcription.db.loading import selectinload
|
||||
from transcription.db.models import Document
|
||||
from transcription.db.models import ExecutionAttempt
|
||||
from transcription.db.models import Job
|
||||
from transcription.db.models import JobSource
|
||||
from transcription.db.models import JobSourceStatus
|
||||
@@ -71,6 +74,51 @@ class TestSourceModelProperties:
|
||||
assert source.latest_error_detail == "Timeout during OCR parsing"
|
||||
assert source.latest_job_source is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_latest_error_detail_ignores_older_failures_when_latest_attempt_succeeds(self):
|
||||
source = Source(
|
||||
document_id=uuid4(),
|
||||
page_number=1,
|
||||
upload_name="page_one.png",
|
||||
filename="stored_page_one.png",
|
||||
file_path="/tmp/stored_page_one.png",
|
||||
file_hash="b" * 64,
|
||||
file_size_bytes=1,
|
||||
)
|
||||
job_source = JobSource(
|
||||
job_id=uuid4(),
|
||||
source_id=source.id,
|
||||
status=JobSourceStatus.TRANSCRIBED,
|
||||
)
|
||||
failed_attempt = ExecutionAttempt(
|
||||
job_source_id=job_source.id,
|
||||
job_id=job_source.job_id,
|
||||
source_id=source.id,
|
||||
attempt_number=1,
|
||||
status=JobSourceStatus.FAILED,
|
||||
provider="openrouter",
|
||||
error_detail="Provider timeout",
|
||||
started_at=datetime.now(UTC),
|
||||
finished_at=datetime.now(UTC),
|
||||
duration_ms=10,
|
||||
)
|
||||
successful_attempt = ExecutionAttempt(
|
||||
job_source_id=job_source.id,
|
||||
job_id=job_source.job_id,
|
||||
source_id=source.id,
|
||||
attempt_number=2,
|
||||
status=JobSourceStatus.TRANSCRIBED,
|
||||
provider="openrouter",
|
||||
error_detail=None,
|
||||
started_at=datetime.now(UTC),
|
||||
finished_at=datetime.now(UTC),
|
||||
duration_ms=10,
|
||||
)
|
||||
job_source.execution_attempts = [failed_attempt, successful_attempt]
|
||||
source.job_sources = [job_source]
|
||||
|
||||
assert source.latest_error_detail is None
|
||||
|
||||
|
||||
# --- Integration Tests for Page Rendering ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user