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).
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user