V4.3 revision to Document Types

This commit is contained in:
Jim Lancaster
2026-08-15 13:29:53 -05:00
parent aed827babe
commit a78b58ff40
30 changed files with 1481 additions and 291 deletions
+19 -11
View File
@@ -10,6 +10,7 @@ from uuid import UUID
from fastapi import FastAPI
from fastapi.testclient import TestClient
from sqlmodel import select
from transcription.api.errors import register_error_handlers
from transcription.api.v4_documents import get_document_service
@@ -21,6 +22,7 @@ from transcription.db import create_all
from transcription.db.engine import get_database_url
from transcription.db.engine import get_engine
from transcription.db.models import Document
from transcription.db.models import DocumentType
from transcription.db.models import Person
from transcription.db.session import dispose_session_factory
from transcription.db.session import session_scope
@@ -45,6 +47,12 @@ def _seed_document_and_person(
return asyncio.run(_seed())
async def _document_type_id(*, db_url: str, label: str) -> UUID:
async with session_scope(database_url=db_url) as session:
document_type = (await session.exec(select(DocumentType).where(DocumentType.label == label))).one()
return document_type.id
@contextmanager
def _v4_api_client(tmp_path: Path, *, db_filename: str) -> Generator[tuple[TestClient, str]]:
settings = Settings(
@@ -88,8 +96,8 @@ def test_list_document_types_returns_seeded_registry(tmp_path):
assert response.status_code == 200
payload = response.json()
codes = {item["code"] for item in payload}
assert {"letter", "record", "memo"}.issubset(codes)
labels = {item["label"] for item in payload}
assert {"Letter", "Record", "Memo"}.issubset(labels)
def test_list_person_roles_returns_seeded_registry(tmp_path):
@@ -102,37 +110,37 @@ def test_list_person_roles_returns_seeded_registry(tmp_path):
assert {"author", "recipient", "mentioned"}.issubset(codes)
def test_set_document_type_by_code_updates_canonical_fields(tmp_path):
def test_set_document_type_by_id_updates_canonical_field(tmp_path):
with _v4_api_client(tmp_path, db_filename="api-doc-type.db") as (client, db_url):
document_id, _ = _seed_document_and_person(db_url=db_url)
type_id = asyncio.run(_document_type_id(db_url=db_url, label="Record"))
response = client.put(
f"/api/v4/documents/{document_id}/type",
json={"document_type_code": "record"},
json={"document_type_id": str(type_id)},
)
assert response.status_code == 200
payload = response.json()
assert payload["document_id"] == str(document_id)
assert payload["document_type_id"] is not None
assert payload["document_type_code"] == "record"
assert payload["document_type_id"] == str(type_id)
def test_document_type_payload_requires_exactly_one_selector(tmp_path):
def test_document_type_payload_requires_uuid_only(tmp_path):
with _v4_api_client(tmp_path, db_filename="api-doc-type-validation.db") as (client, db_url):
document_id, _ = _seed_document_and_person(db_url=db_url)
missing = client.put(f"/api/v4/documents/{document_id}/type", json={})
conflicting = client.put(
invalid = client.put(
f"/api/v4/documents/{document_id}/type",
json={"document_type_id": str(UUID(int=1)), "document_type_code": "record"},
json={"document_type_id": "record"},
)
unexpected = client.put(
f"/api/v4/documents/{document_id}/type",
json={"document_type_code": "record", "ignored": True},
json={"document_type_id": str(UUID(int=1)), "ignored": True},
)
assert missing.status_code == 422
assert conflicting.status_code == 422
assert invalid.status_code == 422
assert unexpected.status_code == 422
+6 -17
View File
@@ -11,7 +11,6 @@ from transcription.config import Settings
from transcription.db.models import Document
from transcription.db.models import DocumentPerson
from transcription.db.models import DocumentPersonRole
from transcription.db.models import DocumentType
from transcription.db.models import Job
from transcription.db.models import Person
from transcription.db.models import PersonRole
@@ -26,12 +25,13 @@ from transcription.services.people import PeopleService
@pytest.mark.asyncio
async def test_read_document_detail_allows_missing_sources(default_session_factory):
service = DocumentService(session_factory=default_session_factory)
document_type = await service.create_document_type(label="Letter")
created = await service.create_document(
Document(
id=uuid4(),
name="detail-doc",
document_type="letter",
document_type_id=document_type.id,
)
)
@@ -41,7 +41,7 @@ async def test_read_document_detail_allows_missing_sources(default_session_facto
assert detail.sources == []
assert detail.document_type_id is not None
assert detail.document_type_ref is not None
assert detail.document_type_ref.code == "letter"
assert detail.document_type_ref.label == "Letter"
@pytest.mark.asyncio
@@ -52,7 +52,6 @@ async def test_update_document_refreshes_updated_timestamp(default_session_facto
Document(
id=uuid4(),
name="timestamp-doc",
document_type="letter",
updated_at=datetime(2000, 1, 1, tzinfo=UTC),
)
)
@@ -74,7 +73,6 @@ async def test_delete_document_blocks_when_dependencies_exist(default_session_fa
Document(
id=uuid4(),
name="blocked-delete",
document_type="record",
)
)
@@ -106,7 +104,6 @@ async def test_delete_document_succeeds_when_unlinked(default_session_factory, t
Document(
id=uuid4(),
name="free-delete",
document_type="memo",
)
)
@@ -132,7 +129,6 @@ async def test_delete_document_removes_person_links(default_session_factory, tmp
Document(
id=uuid4(),
name="person-linked-delete",
document_type="memo",
)
)
person = await people_service.create_person(Person(full_name="Linked Person"))
@@ -171,7 +167,6 @@ async def test_delete_document_removes_populated_storage_tree(default_session_fa
Document(
id=uuid4(),
name="tree-delete",
document_type="memo",
)
)
@@ -198,7 +193,6 @@ async def test_read_person_detail_loads_document_links(default_session_factory):
Document(
id=uuid4(),
name="linked-doc",
document_type="letter",
)
)
person = await people_service.create_person(Person(full_name="Linked Person"))
@@ -246,7 +240,6 @@ async def test_delete_person_removes_links_when_linked_documents_exist(default_s
Document(
id=uuid4(),
name="block-person-delete-doc",
document_type="record",
)
)
person = await service.create_person(Person(full_name="Blocked Person"))
@@ -280,16 +273,12 @@ async def test_delete_person_succeeds_when_unlinked(default_session_factory):
@pytest.mark.asyncio
async def test_create_document_reuses_existing_document_type_registry(default_session_factory):
async def test_create_document_uses_existing_document_type_registry(default_session_factory):
service = DocumentService(session_factory=default_session_factory)
async with service._session_scope() as session:
existing = DocumentType(code="record", label="Record")
session.add(existing)
await session.commit()
await session.refresh(existing)
existing = await service.create_document_type(label="Record")
created = await service.create_document(Document(id=uuid4(), name="typed-doc", document_type="record"))
created = await service.create_document(Document(id=uuid4(), name="typed-doc", document_type_id=existing.id))
assert created.document_type_id is not None
assert created.document_type_id == existing.id
+109
View File
@@ -0,0 +1,109 @@
from __future__ import annotations
import pytest
from transcription.config import Settings
from transcription.errors import ErrorCategory
from transcription.services.prompts import PromptStore
from transcription.services.prompts import PromptStoreError
@pytest.fixture
def prompt_store(tmp_path):
prompt_dir = tmp_path / "prompts"
prompt_dir.mkdir()
(prompt_dir / "transcribe_document.md").write_text("Original prompt\n", encoding="utf-8")
(prompt_dir / "notes.txt").write_text("Not a prompt\n", encoding="utf-8")
settings = Settings(openrouter_api_key="test-key", prompt_dir=prompt_dir)
return PromptStore(settings=settings), prompt_dir
def test_list_and_read_existing_markdown_prompts(prompt_store):
store, _ = prompt_store
prompts = store.list_prompts()
assert [item.name for item in prompts] == ["transcribe_document.md"]
assert prompts[0].is_default is True
assert prompts[0].has_backup is False
assert store.read_prompt("transcribe_document.md") == "Original prompt\n"
def test_write_rotates_single_backup_and_recovery_swaps_versions(prompt_store):
store, prompt_dir = prompt_store
prompt_path = prompt_dir / "transcribe_document.md"
backup_path = prompt_dir / "transcribe_document.md.bak"
store.write_prompt(prompt_path.name, "Second prompt")
assert prompt_path.read_text(encoding="utf-8") == "Second prompt\n"
assert backup_path.read_text(encoding="utf-8") == "Original prompt\n"
store.write_prompt(prompt_path.name, "Third prompt")
assert prompt_path.read_text(encoding="utf-8") == "Third prompt\n"
assert backup_path.read_text(encoding="utf-8") == "Second prompt\n"
assert store.list_prompts()[0].has_backup is True
store.recover_prompt(prompt_path.name)
assert prompt_path.read_text(encoding="utf-8") == "Second prompt\n"
assert backup_path.read_text(encoding="utf-8") == "Third prompt\n"
assert list(prompt_dir.glob("*.tmp")) == []
@pytest.mark.parametrize(
"name",
[
"../outside.md",
"nested/prompt.md",
r"nested\prompt.md",
"prompt.txt",
],
)
def test_prompt_names_are_constrained(prompt_store, name):
store, _ = prompt_store
with pytest.raises(PromptStoreError) as caught:
store.read_prompt(name)
assert caught.value.category == ErrorCategory.VALIDATION
def test_prompt_creation_and_empty_content_are_rejected(prompt_store):
store, _ = prompt_store
with pytest.raises(PromptStoreError) as missing:
store.write_prompt("new_prompt.md", "content")
with pytest.raises(PromptStoreError) as empty:
store.write_prompt("transcribe_document.md", " \n")
assert missing.value.category == ErrorCategory.NOT_FOUND
assert empty.value.category == ErrorCategory.VALIDATION
def test_recovery_requires_a_backup(prompt_store):
store, _ = prompt_store
with pytest.raises(PromptStoreError) as caught:
store.recover_prompt("transcribe_document.md")
assert caught.value.category == ErrorCategory.NOT_FOUND
def test_failed_active_replace_preserves_complete_prompt(prompt_store, monkeypatch):
store, prompt_dir = prompt_store
prompt_path = prompt_dir / "transcribe_document.md"
original_replace = type(prompt_path).replace
def fail_active_replace(path, target):
if target == prompt_path:
raise OSError("simulated replace failure")
return original_replace(path, target)
monkeypatch.setattr(type(prompt_path), "replace", fail_active_replace)
with pytest.raises(PromptStoreError) as caught:
store.write_prompt(prompt_path.name, "Replacement prompt")
assert caught.value.category == ErrorCategory.INFRA_PERSISTENT
assert prompt_path.read_text(encoding="utf-8") == "Original prompt\n"
assert (prompt_dir / "transcribe_document.md.bak").read_text(encoding="utf-8") == "Original prompt\n"
assert list(prompt_dir.glob(".*.tmp")) == []
+168
View File
@@ -0,0 +1,168 @@
from __future__ import annotations
from uuid import uuid4
import pytest
from transcription.db.models import Document
from transcription.db.models import DocumentPerson
from transcription.db.models import DocumentPersonRole
from transcription.db.models import Person
from transcription.errors import ErrorCategory
from transcription.services.documents import DocumentService
from transcription.services.documents import DocumentTypeError
from transcription.services.people import PeopleService
from transcription.services.people import PersonRoleError
@pytest.mark.asyncio
async def test_document_type_maintenance_uses_alphabetical_labels(default_session_factory):
service = DocumentService(session_factory=default_session_factory)
second = await service.create_document_type(label="Court Record")
first = await service.create_document_type(label="Letter")
updated = await service.update_document_type(
second.id,
label="Archive",
is_active=False,
)
assert updated.label == "Archive"
assert updated.is_active is False
assert [item.id for item in await service.list_document_types(active_only=False)] == [second.id, first.id]
assert [item.id for item in await service.list_document_types()] == [first.id]
summaries = await service.list_document_type_summaries()
assert [item.label for item in summaries] == ["Archive", "Letter"]
assert [item.document_count for item in summaries] == [0, 0]
@pytest.mark.asyncio
async def test_document_type_duplicate_normalized_label_is_conflict(default_session_factory):
service = DocumentService(session_factory=default_session_factory)
await service.create_document_type(label="Letter")
with pytest.raises(DocumentTypeError) as caught:
await service.create_document_type(label=" letter ")
assert caught.value.category == ErrorCategory.CONFLICT
@pytest.mark.asyncio
async def test_document_type_delete_allows_unreferenced_and_blocks_referenced(default_session_factory):
service = DocumentService(session_factory=default_session_factory)
unused = await service.create_document_type(label="Unused")
referenced = await service.create_document_type(label="Record")
await service.create_document(Document(id=uuid4(), name="Typed document", document_type_id=referenced.id))
summaries = {item.id: item for item in await service.list_document_type_summaries()}
assert summaries[referenced.id].document_count == 1
await service.delete_document_type(unused.id)
with pytest.raises(DocumentTypeError) as caught:
await service.delete_document_type(referenced.id)
assert caught.value.category == ErrorCategory.CONFLICT
relabeled = await service.update_document_type(
referenced.id,
label="Referenced Record",
is_active=False,
)
assert relabeled.label == "Referenced Record"
assert relabeled.is_active is False
@pytest.mark.asyncio
async def test_person_role_maintenance_orders_by_label_then_code(default_session_factory):
service = PeopleService(session_factory=default_session_factory)
second = await service.create_person_role(code="witness", label="Witness")
first = await service.create_person_role(code="author", label="Author")
updated = await service.update_person_role(second.id, label="Attestor", is_active=False)
assert updated.code == "witness"
assert [item.id for item in await service.list_person_roles(active_only=False)] == [second.id, first.id]
assert [item.id for item in await service.list_person_roles()] == [first.id]
@pytest.mark.asyncio
async def test_person_role_delete_allows_unreferenced_and_blocks_referenced(default_session_factory):
documents = DocumentService(session_factory=default_session_factory)
people = PeopleService(session_factory=default_session_factory)
unused = await people.create_person_role(code="witness", label="Witness")
referenced = await people.create_person_role(code="author", label="Author")
document = await documents.create_document(Document(name="Role document"))
person = await people.create_person(Person(full_name="Role Person"))
await people.create_document_person(
DocumentPerson(
document_id=document.id,
person_id=person.id,
role_id=referenced.id,
role=DocumentPersonRole.AUTHOR,
)
)
await people.delete_person_role(unused.id)
with pytest.raises(PersonRoleError) as caught:
await people.delete_person_role(referenced.id)
assert caught.value.category == ErrorCategory.CONFLICT
relabeled = await people.update_person_role(referenced.id, label="Creator", is_active=False)
assert relabeled.label == "Creator"
assert relabeled.is_active is False
@pytest.mark.asyncio
async def test_person_role_duplicate_code_is_conflict(default_session_factory):
service = PeopleService(session_factory=default_session_factory)
await service.create_person_role(code="author", label="Author")
with pytest.raises(PersonRoleError) as caught:
await service.create_person_role(code=" AUTHOR ", label="Duplicate")
assert caught.value.category == ErrorCategory.CONFLICT
@pytest.mark.asyncio
async def test_custom_person_role_can_be_used_for_document_link(default_session_factory):
documents = DocumentService(session_factory=default_session_factory)
people = PeopleService(session_factory=default_session_factory)
role = await people.create_person_role(code="witness", label="Witness")
document = await documents.create_document(Document(name="Witnessed document"))
person = await people.create_person(Person(full_name="Archive Witness"))
link = await people.add_document_person_link(
document_id=document.id,
person_id=person.id,
role_id=role.id,
)
loaded = await people.list_document_people(document_id=document.id)
assert link.role == "witness"
assert loaded[0].role_ref is not None
assert loaded[0].role_ref.code == "witness"
@pytest.mark.asyncio
async def test_custom_person_role_delete_blocks_legacy_only_reference(default_session_factory):
documents = DocumentService(session_factory=default_session_factory)
people = PeopleService(session_factory=default_session_factory)
role = await people.create_person_role(code="witness", label="Witness")
document = await documents.create_document(Document(name="Legacy role document"))
person = await people.create_person(Person(full_name="Legacy Witness"))
async with people._session_scope() as session:
session.add(
DocumentPerson(
document_id=document.id,
person_id=person.id,
role="witness",
role_id=None,
)
)
await session.commit()
assert await people.is_person_role_referenced(role.id) is True
with pytest.raises(PersonRoleError) as caught:
await people.delete_person_role(role.id)
assert caught.value.category == ErrorCategory.CONFLICT
+93 -8
View File
@@ -1,5 +1,7 @@
"""Tests for the database runtime and V2 schema bootstrap behavior."""
from uuid import uuid4
import pytest
from sqlalchemy import inspect
from sqlalchemy import text
@@ -97,10 +99,10 @@ async def test_create_all_seeds_default_registry_rows(tmp_path):
await create_all(engine=runtime.engine)
async with AsyncSession(runtime.engine, expire_on_commit=False) as session:
role_codes = set((await session.exec(select(PersonRole.code))).all())
type_codes = set((await session.exec(select(DocumentType.code))).all())
type_labels = set((await session.exec(select(DocumentType.label))).all())
assert {"author", "recipient", "mentioned"}.issubset(role_codes)
assert {"letter", "record", "memo"}.issubset(type_codes)
assert {"Letter", "Record", "Memo"}.issubset(type_labels)
finally:
await dispose_database_runtime()
@@ -130,9 +132,94 @@ async def test_create_all_upgrades_existing_person_table_for_family_search(tmp_p
)
assert "family_search_id" in columns
assert any(
index["column_names"] == ["family_search_id"] and index["unique"] for index in indexes
)
assert any(index["column_names"] == ["family_search_id"] and index["unique"] for index in indexes)
finally:
await dispose_database_runtime()
@pytest.mark.asyncio
async def test_upgrade_migrates_document_types_to_uuid_only_identity(tmp_path):
settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "type-upgrade.db")),
environment="test",
)
runtime = initialize_database_runtime(settings=settings)
type_id = uuid4().hex
document_id = uuid4().hex
try:
async with runtime.engine.begin() as connection:
await connection.execute(
text(
"CREATE TABLE document_type ("
"id CHAR(32) PRIMARY KEY NOT NULL, "
"code VARCHAR NOT NULL, "
"label VARCHAR NOT NULL, "
"is_active BOOLEAN NOT NULL, "
"sort_order INTEGER NOT NULL, "
"created_at DATETIME NOT NULL, "
"updated_at DATETIME NOT NULL"
")"
)
)
await connection.execute(text("CREATE UNIQUE INDEX ix_document_type_code ON document_type (code)"))
await connection.execute(
text(
"CREATE TABLE document ("
"id CHAR(32) PRIMARY KEY NOT NULL, "
"name VARCHAR NOT NULL, "
"document_type_id CHAR(32), "
"document_type VARCHAR, "
"document_date DATE, "
"document_date_raw VARCHAR, "
"location_created VARCHAR, "
"notes VARCHAR, "
"archive_identifier VARCHAR, "
"created_at DATETIME NOT NULL, "
"updated_at DATETIME NOT NULL"
")"
)
)
await connection.execute(
text(
"INSERT INTO document_type "
"(id, code, label, is_active, sort_order, created_at, updated_at) "
"VALUES (:id, 'letter', 'Letter', 1, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
),
{"id": type_id},
)
await connection.execute(
text(
"INSERT INTO document "
"(id, name, document_type_id, document_type, created_at, updated_at) "
"VALUES (:id, 'Legacy Letter', NULL, 'letter', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
),
{"id": document_id},
)
await upgrade_schema(engine=runtime.engine)
async with runtime.engine.connect() as connection:
type_columns, document_columns, migrated_type_id, normalized_label = await connection.run_sync(
lambda sync_connection: (
{column["name"] for column in inspect(sync_connection).get_columns("document_type")},
{column["name"] for column in inspect(sync_connection).get_columns("document")},
sync_connection.execute(
text("SELECT document_type_id FROM document WHERE id = :id"),
{"id": document_id},
).scalar_one(),
sync_connection.execute(
text("SELECT normalized_label FROM document_type WHERE id = :id"),
{"id": type_id},
).scalar_one(),
)
)
assert {"code", "sort_order"}.isdisjoint(type_columns)
assert "document_type" not in document_columns
assert migrated_type_id == type_id
assert normalized_label == "letter"
finally:
await dispose_database_runtime()
@@ -175,9 +262,7 @@ async def test_v42_upgrade_adds_evidence_tables_without_rewriting_legacy_snapsho
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'")
)
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)
+8 -8
View File
@@ -27,8 +27,8 @@ def _make_document(**overrides) -> Document:
return Document(**defaults)
def _persist_document_type(session, *, code: str = "letter", label: str = "Letter") -> DocumentType:
document_type = DocumentType(code=code, label=label)
def _persist_document_type(session, *, label: str = "Letter") -> DocumentType:
document_type = DocumentType(label=label, normalized_label=label.strip().casefold())
session.add(document_type)
session.commit()
session.refresh(document_type)
@@ -45,7 +45,7 @@ def _persist_person_role(session, *, code: str = "author", label: str = "Author"
def _persist_document(session) -> Document:
document_type = _persist_document_type(session)
document = _make_document(document_type_id=document_type.id, document_type=document_type.code)
document = _make_document(document_type_id=document_type.id)
session.add(document)
session.commit()
session.refresh(document)
@@ -117,8 +117,8 @@ class TestDocumentModel:
assert document.updated_at is not None
def test_can_reference_document_type_registry(self, session):
document_type = _persist_document_type(session, code="record", label="Record")
document = _make_document(document_type_id=document_type.id, document_type=document_type.code)
document_type = _persist_document_type(session, label="Record")
document = _make_document(document_type_id=document_type.id)
session.add(document)
session.commit()
session.refresh(document)
@@ -264,9 +264,9 @@ class TestRegistryModels:
with pytest.raises(IntegrityError):
session.commit()
def test_document_type_code_is_unique(self, session):
_persist_document_type(session, code="journal", label="Journal")
duplicate = DocumentType(code="journal", label="Journal Duplicate")
def test_document_type_normalized_label_is_unique(self, session):
_persist_document_type(session, label="Journal")
duplicate = DocumentType(label=" journal ", normalized_label="journal")
session.add(duplicate)
with pytest.raises(IntegrityError):
session.commit()
+14 -12
View File
@@ -4,11 +4,13 @@ from datetime import date
import pytest
import pytest_asyncio
from sqlmodel import select
from transcription.db import session_scope
from transcription.db.models import Document
from transcription.db.models import DocumentPerson
from transcription.db.models import DocumentPersonRole
from transcription.db.models import DocumentType
from transcription.db.models import Job
from transcription.db.models import Person
from transcription.db.models import Source
@@ -20,13 +22,14 @@ from transcription.db.models import Source
async def seed_person_and_document():
"""Seed a Person and Document linked by DocumentPerson role."""
async with session_scope() as session:
letter_type = (await session.exec(select(DocumentType).where(DocumentType.label == "Letter"))).one()
person = Person(full_name="Zenna Cochran")
session.add(person)
await session.flush()
doc = Document(
name="Letter from Hig",
document_type="letter",
document_type_id=letter_type.id,
archive_identifier="ZC-1924-001",
)
session.add(doc)
@@ -63,7 +66,12 @@ class TestDocumentsPageRendering:
_, client = app_client
async with session_scope() as session:
doc = Document(name="1924 Postcard", document_type="postcard", archive_identifier="PC-001")
postcard_type = (await session.exec(select(DocumentType).where(DocumentType.label == "Postcard"))).one()
doc = Document(
name="1924 Postcard",
document_type_id=postcard_type.id,
archive_identifier="PC-001",
)
session.add(doc)
await session.commit()
@@ -71,7 +79,7 @@ class TestDocumentsPageRendering:
assert response.status_code == 200
assert "1924 Postcard" in response.text
assert "postcard" in response.text
assert "Postcard" in response.text
assert "PC-001" in response.text
assert "Document Date" in response.text
assert "Author" in response.text
@@ -106,9 +114,7 @@ class TestDocumentsPageRendering:
assert "Hig - Albert Edward Higgins (1885)" in response.text
@pytest.mark.asyncio
async def test_document_detail_page_renders_bento_grid_and_metadata(
self, app_client, seed_person_and_document
):
async def test_document_detail_page_renders_bento_grid_and_metadata(self, app_client, seed_person_and_document):
_, client = app_client
doc_id, _ = seed_person_and_document
@@ -143,9 +149,7 @@ class TestDocumentsPageRendering:
assert f"Job ID: {job_id}" in response.text
@pytest.mark.asyncio
async def test_document_edit_page_prefills_existing_values(
self, app_client, seed_person_and_document
):
async def test_document_edit_page_prefills_existing_values(self, app_client, seed_person_and_document):
_, client = app_client
doc_id, _ = seed_person_and_document
@@ -157,9 +161,7 @@ class TestDocumentsPageRendering:
assert "ZC-1924-001" in response.text
@pytest.mark.asyncio
async def test_document_delete_page_blocks_deletion_when_dependencies_exist(
self, app_client
):
async def test_document_delete_page_blocks_deletion_when_dependencies_exist(self, app_client):
_, client = app_client
async with session_scope() as session:
+3 -9
View File
@@ -84,9 +84,7 @@ class TestJobsPageRendering:
assert "Preselected Journal Entry" in response.text
@pytest.mark.asyncio
async def test_job_detail_page_renders_logistics_and_links(
self, app_client, seed_document_with_unlinked_job
):
async def test_job_detail_page_renders_logistics_and_links(self, app_client, seed_document_with_unlinked_job):
_, client = app_client
_, job_id = seed_document_with_unlinked_job
@@ -102,9 +100,7 @@ class TestJobsPageRendering:
assert "updates automatically while the job is active" in response.text
@pytest.mark.asyncio
async def test_job_cancel_page_renders_confirmation(
self, app_client, seed_document_with_unlinked_job
):
async def test_job_cancel_page_renders_confirmation(self, app_client, seed_document_with_unlinked_job):
_, client = app_client
_, job_id = seed_document_with_unlinked_job
@@ -116,9 +112,7 @@ class TestJobsPageRendering:
assert "Cancel job" in response.text
@pytest.mark.asyncio
async def test_job_resubmit_page_renders_counts(
self, app_client, seed_job
):
async def test_job_resubmit_page_renders_counts(self, app_client, seed_job):
_, client = app_client
job_id = await seed_job(
filename="failed-resubmit.png",
+2 -1
View File
@@ -31,6 +31,7 @@ class TestNavigationAndMounts:
"/ui/people",
"/ui/sources",
"/ui/jobs",
"/ui/settings",
],
)
def test_registered_pages_render_successfully(self, app_client, route_path: str):
@@ -39,4 +40,4 @@ class TestNavigationAndMounts:
response = client.get(route_path)
assert response.status_code == 200
assert "html" in response.headers.get("content-type", "").lower()
assert "html" in response.headers.get("content-type", "").lower()
+2
View File
@@ -16,9 +16,11 @@ class TestPageRegistration:
people_response = client.get("/ui/people")
sources_response = client.get("/ui/sources")
jobs_response = client.get("/ui/jobs")
settings_response = client.get("/ui/settings")
assert homepage_response.status_code == 200
assert documents_response.status_code == 200
assert people_response.status_code == 200
assert sources_response.status_code == 200
assert jobs_response.status_code == 200
assert settings_response.status_code == 200
+8 -28
View File
@@ -48,9 +48,7 @@ class TestSourceModelProperties:
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()
source = (await session.exec(select(Source).where(Source.document_id == job.document_id))).first()
assert source is not None
# Validate computed properties
@@ -159,9 +157,7 @@ class TestSourcesPageRendering:
assert "job-page.png" in response.text
@pytest.mark.asyncio
async def test_sources_page_job_context_shows_job_source_status_and_error_detail(
self, app_client, seed_job
):
async def test_sources_page_job_context_shows_job_source_status_and_error_detail(self, app_client, seed_job):
_, client = app_client
job_id = await seed_job(
filename="job-failed-page.png",
@@ -178,17 +174,9 @@ class TestSourcesPageRendering:
assert "Provider timed out" in response.text
@pytest.mark.asyncio
async def test_source_detail_page_renders_preview_and_revision_box(
self, app_client, seed_job
):
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 = await seed_job(
filename="detail-source.png",
transcription_text="original transcription text",
@@ -201,9 +189,7 @@ class TestSourcesPageRendering:
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()
source = (await session.exec(select(Source).where(Source.document_id == job.document_id))).first()
assert source is not None
source_id = str(source.id)
@@ -232,9 +218,7 @@ class TestSourcesPageRendering:
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()
source = (await session.exec(select(Source).where(Source.document_id == job.document_id))).first()
assert source is not None
source_id = source.id
@@ -268,18 +252,14 @@ class TestSourcesPageRendering:
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
):
async def test_source_delete_page_blocks_when_source_is_job_linked(self, app_client, seed_job):
_, client = app_client
job_id = await seed_job(filename="linked-source.png", transcription_text="linked text")
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()
source = (await session.exec(select(Source).where(Source.document_id == job.document_id))).first()
assert source is not None
source_id = str(source.id)