from __future__ import annotations from datetime import UTC, date, datetime, timedelta import pytest from sqlalchemy import text from monlet_server.services.partitioning import ( _month_start, _partition_name, _shift_months, drop_old_event_partitions, ensure_event_partitions, ) from monlet_server.settings import reset_settings_cache async def _partitions(engine) -> set[str]: async with engine.begin() as conn: rows = ( await conn.execute( text( "SELECT c.relname FROM pg_inherits i " "JOIN pg_class c ON c.oid=i.inhrelid " "JOIN pg_class p ON p.oid=i.inhparent " "WHERE p.relname='events'" ) ) ).all() return {r[0] for r in rows} @pytest.mark.asyncio async def test_ensure_creates_current_and_future_partitions(engine, monkeypatch): monkeypatch.setenv("MONLET_EVENTS_FUTURE_PARTITIONS", "2") reset_settings_cache() try: await ensure_event_partitions(engine) today = _month_start(datetime.now(UTC).date()) for i in range(0, 3): assert _partition_name(_shift_months(today, i)) in await _partitions(engine) finally: reset_settings_cache() @pytest.mark.asyncio async def test_drop_old_partitions_uses_retention(engine, monkeypatch): monkeypatch.setenv("MONLET_EVENTS_RETENTION_MONTHS", "1") reset_settings_cache() try: # Create an old partition directly. old = date(2020, 1, 1) name = _partition_name(old) end = _shift_months(old, 1) async with engine.begin() as conn: await conn.execute( text( f"CREATE TABLE IF NOT EXISTS {name} PARTITION OF events " f"FOR VALUES FROM ('{old.isoformat()} UTC') TO ('{end.isoformat()} UTC');" ) ) assert name in await _partitions(engine) await drop_old_event_partitions(engine) assert name not in await _partitions(engine) finally: reset_settings_cache() @pytest.mark.asyncio async def test_dedup_prevents_replay_via_ingest(app_client, ui_auth_headers, session): from sqlalchemy import func, select from monlet_server.models import Event from ._helpers import make_event, register_agent auth_headers = await register_agent(app_client, ui_auth_headers) ev = make_event(status="critical", exit_code=2) r1 = await app_client.post( "/api/v1/events", json={"agent_id": "agent-1", "events": [ev]}, headers=auth_headers ) assert r1.json() == {"accepted": 1, "deduplicated": 0} # Replay the exact same event_id long after — dedup must block ingestion entirely, # never touching checks/incidents/outbox/events. r2 = await app_client.post( "/api/v1/events", json={"agent_id": "agent-1", "events": [ev]}, headers=auth_headers ) assert r2.json() == {"accepted": 0, "deduplicated": 1} n = (await session.execute(select(func.count()).select_from(Event))).scalar_one() assert n == 1 @pytest.mark.asyncio async def test_late_dedup_does_not_keep_replay_doors_open(engine, monkeypatch): """Dedup retention TTL must be at least documented agent spool window.""" from uuid import uuid4 from sqlalchemy import func, select from sqlalchemy.ext.asyncio import async_sessionmaker from monlet_server.models import EventIngestDedup from monlet_server.services.detector import _prune_history monkeypatch.setenv("MONLET_EVENT_DEDUP_RETENTION_DAYS", "1") reset_settings_cache() try: sm = async_sessionmaker(engine, expire_on_commit=False) async with sm() as s: old = datetime.now(UTC) - timedelta(days=5) s.add(EventIngestDedup(event_id=uuid4(), received_at=old)) s.add(EventIngestDedup(event_id=uuid4(), received_at=datetime.now(UTC))) await s.commit() async with sm() as s: await _prune_history(s) await s.commit() async with sm() as s: n = (await s.execute(select(func.count()).select_from(EventIngestDedup))).scalar_one() assert n == 1 finally: reset_settings_cache()