fix(tests): the rule-version round trip must not touch Postgres after dispose (#3240)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / integration (push) Successful in 30s
CI & Build / Python tests (push) Successful in 1m5s
CI & Build / Build & push image (push) Successful in 23s

`_dispose_engine` is a usefixtures entry, so it sets up AFTER an autouse
fixture and tears down BEFORE it. The purge running after this file's
`yield` therefore opened a fresh pooled connection that the closing loop
immediately orphaned, and the next test to touch Postgres died on "Future
attached to a different loop" — two of this file's own tests and
test_run_maintenance_vacuums_real_tables, which shares nothing with it but
the engine.

The autouse fixture is setup-only now, matching its sibling in
test_integration_backup_note_roundtrip.py, and the cleanup moved into
`restored`, whose teardown runs while the engine is still live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 18:41:18 -04:00
co-authored by Claude Opus 5
parent 9006affda8
commit 7a0dc93270
@@ -67,10 +67,16 @@ async def _purge_restored() -> None:
@pytest_asyncio.fixture(autouse=True)
async def _no_leftovers():
"""The usernames are fixed, so a previous failed run would leave rows that
make the fixtures below pick the wrong user — or hit `.one()` with two."""
await _purge_restored()
await _purge_books(OWNER_USERNAME)
yield
make the fixtures below pick the wrong user — or hit `.one()` with two.
SETUP ONLY, and that is not a stylistic choice. `_dispose_engine` is a
usefixtures entry, so it sets up AFTER this autouse one and therefore
tears down BEFORE it. Any database call here after a `yield` would open a
fresh pooled connection that the closing loop then orphans, and the next
test to touch Postgres dies on "Future attached to a different loop"
including tests in other files. Cleanup belongs in the fixtures below,
whose teardowns run while the engine is still live.
"""
await _purge_restored()
await _purge_books(OWNER_USERNAME)
@@ -173,6 +179,12 @@ async def source():
@pytest_asyncio.fixture
async def restored(source):
"""Runs the real restore, then hands back the new rows.
The restore mints a NEW user from the payload, so the restored corpus is
entirely separate from the source one — which is what makes the id
assertions below able to fail.
"""
await backup.restore_full_backup(source["payload"])
async with async_session() as s:
user = (await s.execute(
@@ -192,7 +204,11 @@ async def restored(source):
select(RuleVersion).where(RuleVersion.rule_id == rule.id)
.order_by(RuleVersion.id)
)).scalars().all()
return {"user": user, "rule": rule, "versions": versions, "source": source}
yield {"user": user, "rule": rule, "versions": versions, "source": source}
# Here rather than in the autouse fixture: this teardown still runs while
# the engine is live. See _no_leftovers.
await _purge_restored()
async def test_both_snapshots_come_back(restored):