from datetime import datetime, timedelta, timezone import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from app.db.base import Base from app.db.models import Round, RoundConfig from app.rounds.scheduler import RoundScheduler class FakeListener: client = object() # truthy sentinel; _tick only checks "is not None" tip_height = 100 tip_header_hex = "00" @pytest.fixture async def session_factory(): engine = create_async_engine("sqlite+aiosqlite:///:memory:") async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) yield async_sessionmaker(engine, expire_on_commit=False) await engine.dispose() async def test_tick_survives_sqlite_naive_datetime_roundtrip(session_factory, monkeypatch): """Regression test: SQLite drops tzinfo on round-trip, so opened_at comes back naive even though it was written as an aware UTC datetime. A prior bug compared it directly against datetime.now(timezone.utc) and crashed with "can't compare offset-naive and offset-aware datetimes" on every tick once a round existed — this must not happen.""" async with session_factory() as session: session.add(RoundConfig(fee_address="", round_duration_seconds=3600)) # not due yet session.add(Round(status="open", opened_at=datetime.now(timezone.utc))) await session.commit() scheduler = RoundScheduler(session_factory, FakeListener()) await scheduler._tick() # must not raise async def test_tick_closes_round_with_no_participants_once_due(session_factory, monkeypatch): past = datetime.now(timezone.utc) - timedelta(seconds=10) async with session_factory() as session: session.add(RoundConfig(fee_address="", round_duration_seconds=1)) session.add(Round(status="open", opened_at=past)) await session.commit() scheduler = RoundScheduler(session_factory, FakeListener()) await scheduler._tick() async with session_factory() as session: round_ = (await session.scalars(select(Round))).one() assert round_.status == "closed"