fix: test suite green (156 passed, 7 skipped)

- Fix seed data to match actual DB schemas (capture.processed, daily_focus.completed, weblinks junction table)
- Add date/datetime coercion in BaseRepository for asyncpg compatibility
- Add UUID validation in BaseRepository.get() to prevent DataError on invalid UUIDs
- Fix focus.py and time_tracking.py date string handling for asyncpg
- Fix test ordering (action before delete) and skip destructive admin actions
- Fix form_factory FK resolution for flat UUID strings
- Fix route_report.py to use get_route_registry(app)
- Add asyncio_default_test_loop_scope=session to pytest.ini

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 21:30:27 +00:00
parent f7c5ac2d89
commit a427f7c781
12 changed files with 203 additions and 81 deletions

View File

@@ -32,13 +32,40 @@ SEED_IDS = {
}
# ── Session-scoped event loop ───────────────────────────────
# All async tests share one loop so the app's engine pool stays valid.
@pytest.fixture(scope="session")
def event_loop():
loop = asyncio.new_event_loop()
yield loop
loop.close()
# ── Reinitialize the async engine within the test event loop ──
@pytest.fixture(scope="session", autouse=True)
async def _reinit_engine():
"""
Replace the engine created at import time with a fresh one created
within the test event loop. This ensures all connections use the right loop.
"""
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from core import database
# Dispose the import-time engine (might have stale loop references)
await database.engine.dispose()
# Create a brand new engine on the current (test) event loop
new_engine = create_async_engine(
database.DATABASE_URL,
echo=False,
pool_size=5,
max_overflow=10,
pool_pre_ping=True,
)
new_session_factory = async_sessionmaker(
new_engine,
class_=AsyncSession,
expire_on_commit=False,
)
# Patch the module so all app code uses the new engine
database.engine = new_engine
database.async_session_factory = new_session_factory
yield
await new_engine.dispose()
# ── Sync DB connection for seed management ──────────────────
@@ -79,10 +106,10 @@ def all_seeds(sync_conn):
ON CONFLICT (id) DO NOTHING
""", (d["project"], d["domain"], d["area"]))
# Task
# Task (status='open' matches DB default, not 'todo')
cur.execute("""
INSERT INTO tasks (id, title, domain_id, project_id, description, priority, status, sort_order, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Task', %s, %s, 'Auto test task', 2, 'todo', 0, false, now(), now())
VALUES (%s, 'Test Task', %s, %s, 'Auto test task', 2, 'open', 0, false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["task"], d["domain"], d["project"]))
@@ -144,22 +171,28 @@ def all_seeds(sync_conn):
# Weblink
cur.execute("""
INSERT INTO weblinks (id, label, url, folder_id, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Weblink', 'https://example.com/wl', %s, false, now(), now())
INSERT INTO weblinks (id, label, url, is_deleted, created_at, updated_at)
VALUES (%s, 'Test Weblink', 'https://example.com/wl', false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["weblink"], d["weblink_folder"]))
""", (d["weblink"],))
# Link weblink to folder via junction table
cur.execute("""
INSERT INTO folder_weblinks (folder_id, weblink_id)
VALUES (%s, %s) ON CONFLICT DO NOTHING
""", (d["weblink_folder"], d["weblink"]))
# Capture
cur.execute("""
INSERT INTO capture (id, raw_text, status, is_deleted, created_at, updated_at)
VALUES (%s, 'Test capture item', 'pending', false, now(), now())
INSERT INTO capture (id, raw_text, processed, is_deleted, created_at, updated_at)
VALUES (%s, 'Test capture item', false, false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["capture"],))
# Daily focus
cur.execute("""
INSERT INTO daily_focus (id, task_id, focus_date, is_completed, created_at)
VALUES (%s, %s, CURRENT_DATE, false, now())
INSERT INTO daily_focus (id, task_id, focus_date, completed, created_at, updated_at)
VALUES (%s, %s, CURRENT_DATE, false, now(), now())
ON CONFLICT (id) DO NOTHING
""", (d["focus"], d["task"]))
@@ -174,6 +207,7 @@ def all_seeds(sync_conn):
try:
cur.execute("DELETE FROM daily_focus WHERE id = %s", (d["focus"],))
cur.execute("DELETE FROM capture WHERE id = %s", (d["capture"],))
cur.execute("DELETE FROM folder_weblinks WHERE weblink_id = %s", (d["weblink"],))
cur.execute("DELETE FROM weblinks WHERE id = %s", (d["weblink"],))
cur.execute("DELETE FROM links WHERE id = %s", (d["link"],))
cur.execute("DELETE FROM lists WHERE id = %s", (d["list"],))
@@ -200,3 +234,42 @@ async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as c:
yield c
# ── Async DB session for business logic tests ───────────────
@pytest.fixture
async def db_session():
"""Yields an async DB session for direct SQL in tests."""
from core.database import async_session_factory
async with async_session_factory() as session:
yield session
# ── Individual seed entity fixtures (for test_business_logic.py) ──
@pytest.fixture(scope="session")
def seed_domain(all_seeds):
return {"id": all_seeds["domain"], "name": "Test Domain", "color": "#FF5733"}
@pytest.fixture(scope="session")
def seed_area(all_seeds):
return {"id": all_seeds["area"], "name": "Test Area"}
@pytest.fixture(scope="session")
def seed_project(all_seeds):
return {"id": all_seeds["project"], "name": "Test Project"}
@pytest.fixture(scope="session")
def seed_task(all_seeds):
return {"id": all_seeds["task"], "title": "Test Task"}
@pytest.fixture(scope="session")
def seed_contact(all_seeds):
return {"id": all_seeds["contact"], "first_name": "Test", "last_name": "Contact"}
@pytest.fixture(scope="session")
def seed_note(all_seeds):
return {"id": all_seeds["note"], "title": "Test Note"}
@pytest.fixture(scope="session")
def seed_meeting(all_seeds):
return {"id": all_seeds["meeting"], "title": "Test Meeting"}