fix(integration): asyncio auto-mode, per-test truncation, artist images IN-subquery

This commit is contained in:
2026-05-15 22:23:32 -04:00
parent 65a055408b
commit 13be9085b5
3 changed files with 41 additions and 6 deletions
+8 -4
View File
@@ -157,12 +157,16 @@ class ArtistService:
if artist is None: if artist is None:
return None return None
stmt = ( # Dedupe via IN-subquery rather than JOIN + DISTINCT: an image can
select(ImageRecord) # have several provenance rows for one artist, and SELECT DISTINCT
.join(ImageProvenance, ImageProvenance.image_record_id == ImageRecord.id) # over ImageRecord fails in Postgres because its json columns have
# no equality operator.
stmt = select(ImageRecord).where(
ImageRecord.id.in_(
select(ImageProvenance.image_record_id)
.join(Source, Source.id == ImageProvenance.source_id) .join(Source, Source.id == ImageProvenance.source_id)
.where(Source.artist_id == artist.id) .where(Source.artist_id == artist.id)
.distinct() )
) )
if cursor: if cursor:
cur_ts, cur_id = decode_cursor(cursor) cur_ts, cur_id = decode_cursor(cursor)
+5
View File
@@ -9,6 +9,11 @@ where = ["."]
include = ["backend*"] include = ["backend*"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
# auto mode: async test fns and async fixtures don't need explicit
# @pytest.mark.asyncio / @pytest_asyncio.fixture decoration. Required
# because the test_api_* fixtures use plain @pytest.fixture on async
# app/client; strict mode errors on those under pytest-asyncio 1.x.
asyncio_mode = "auto"
# The fast backend job runs `-m "not integration"`; the dedicated CI # The fast backend job runs `-m "not integration"`; the dedicated CI
# `integration` job (pgvector Postgres + Redis service containers) runs # `integration` job (pgvector Postgres + Redis service containers) runs
# `-m integration`. Both also runnable locally via docker-compose. # `-m integration`. Both also runnable locally via docker-compose.
+26
View File
@@ -18,6 +18,8 @@ from sqlalchemy.ext.asyncio import (
) )
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from backend.app.models import Base
def _async_database_url() -> str: def _async_database_url() -> str:
user = os.environ.get("DB_USER", "fabledcurator") user = os.environ.get("DB_USER", "fabledcurator")
@@ -73,3 +75,27 @@ def db_sync(sync_engine):
yield session yield session
finally: finally:
session.rollback() session.rollback()
@pytest.fixture(autouse=True)
def _truncate_after_integration(request):
"""Integration tests exercise app/celery code that commits on its own
connection, which the rollback fixtures above can't undo — so data
would leak between tests. Truncate every model table after each
integration-marked test. Scoped to the integration marker so the
DB-less fast unit job never connects here.
"""
yield
if request.node.get_closest_marker("integration") is None:
return
tables = ", ".join(t.name for t in Base.metadata.sorted_tables)
if not tables:
return
eng = create_engine(_sync_database_url(), future=True)
try:
with eng.begin() as conn:
conn.exec_driver_sql(
f"TRUNCATE {tables} RESTART IDENTITY CASCADE"
)
finally:
eng.dispose()