Files
CIAgent/apps/api/tests/integration/test_scheduler_task.py
saksham 1a4c80958f Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring,
source collection, LLM-based change analysis, enrichment, and account
security (Turnstile, escalating lockout, email verification).
2026-08-05 10:48:20 -04:00

139 lines
4.7 KiB
Python

"""Celery Beat's sync_schedules task: dynamic due-schedule discovery,
idempotent skip of companies already mid-run, and delegation to
run_monitoring.delay for everything else."""
from __future__ import annotations
import uuid
from datetime import UTC, datetime, timedelta
from unittest.mock import patch
import pytest
from sqlalchemy import select
from app.models.company import Company
from app.models.enums import MonitoringFrequency, MonitoringRunTrigger, SourceType
from app.models.monitor_configuration import MonitorConfiguration
from app.models.monitoring_run import MonitoringRun
from app.models.source import Source
from app.repositories.monitoring_run_repository import MonitoringRunRepository
from app.tasks.scheduler import _sync_schedules_async
async def _make_due_company(db_session, *, next_run_offset_minutes: int, enabled: bool = True):
company = Company(
id=uuid.uuid4(),
user_id=uuid.uuid4(),
name="Scheduled Co",
slug=f"scheduled-co-{uuid.uuid4().hex[:6]}",
)
db_session.add(company)
db_session.add(
MonitorConfiguration(
company_id=company.id,
frequency_type=MonitoringFrequency.WEEKLY,
enabled=enabled,
next_run=datetime.now(UTC) + timedelta(minutes=next_run_offset_minutes),
)
)
await db_session.commit()
return company
@pytest.mark.asyncio
async def test_sync_schedules_enqueues_due_companies(db_session):
company = await _make_due_company(db_session, next_run_offset_minutes=-5) # 5 min overdue
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
await _sync_schedules_async()
mock_delay.assert_called_once()
result = await db_session.execute(
select(MonitoringRun).where(MonitoringRun.company_id == company.id)
)
run = result.scalar_one()
assert run.trigger_type == MonitoringRunTrigger.SCHEDULED
@pytest.mark.asyncio
async def test_sync_schedules_skips_not_yet_due_companies(db_session):
await _make_due_company(db_session, next_run_offset_minutes=60) # due in the future
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
await _sync_schedules_async()
mock_delay.assert_not_called()
@pytest.mark.asyncio
async def test_sync_schedules_skips_disabled_configs(db_session):
await _make_due_company(db_session, next_run_offset_minutes=-5, enabled=False)
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
await _sync_schedules_async()
mock_delay.assert_not_called()
@pytest.mark.asyncio
async def test_sync_schedules_does_not_double_enqueue_a_company_with_an_active_run(db_session):
company = await _make_due_company(db_session, next_run_offset_minutes=-5)
run_repo = MonitoringRunRepository(db_session)
await run_repo.create(company_id=company.id, trigger_type=MonitoringRunTrigger.MANUAL)
await db_session.commit()
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
await _sync_schedules_async()
mock_delay.assert_not_called()
@pytest.mark.asyncio
async def test_sync_schedules_enqueues_via_a_source_override_even_when_company_default_isnt_due(
db_session,
):
"""A source with its own faster cadence (e.g. "check news daily") must
be able to trigger a run even while the company's own default schedule
(e.g. weekly) isn't due yet - this is the whole point of per-source
scheduling."""
company = await _make_due_company(db_session, next_run_offset_minutes=60) # not due
db_session.add(
Source(
company_id=company.id,
source_type=SourceType.RSS,
name="Daily News",
active=True,
frequency_type=MonitoringFrequency.DAILY,
next_check=datetime.now(UTC) - timedelta(minutes=5), # overdue
)
)
await db_session.commit()
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
await _sync_schedules_async()
mock_delay.assert_called_once()
@pytest.mark.asyncio
async def test_sync_schedules_skips_when_neither_company_default_nor_any_source_is_due(db_session):
company = await _make_due_company(db_session, next_run_offset_minutes=60) # not due
db_session.add(
Source(
company_id=company.id,
source_type=SourceType.RSS,
name="Daily News",
active=True,
frequency_type=MonitoringFrequency.DAILY,
next_check=datetime.now(UTC) + timedelta(hours=12), # not due yet
)
)
await db_session.commit()
with patch("app.tasks.collection.run_monitoring.delay") as mock_delay:
await _sync_schedules_async()
mock_delay.assert_not_called()