import uuid from datetime import timedelta import pytest from fansifter_common.utils import timezone from app.pipeline.enums import RunStatus from app.pipeline.models import PipelineRun from tests.unit.helpers import create_model, override_settings class TestPipelineRunQuery: @pytest.mark.db @pytest.mark.parametrize( "status", [RunStatus.queued, RunStatus.running, RunStatus.paused] ) def test_active_statuses_included(self, status: RunStatus) -> None: create_model(PipelineRun, status=status) assert PipelineRun.query.active().count() == 1 @pytest.mark.db @pytest.mark.parametrize( "status", [RunStatus.idle, RunStatus.done, RunStatus.error, RunStatus.cancelled] ) def test_inactive_statuses_excluded(self, status: RunStatus) -> None: create_model(PipelineRun, status=status) assert PipelineRun.query.active().count() == 0 @pytest.mark.db def test_increments_counters(self) -> None: run = create_model( PipelineRun, status=RunStatus.running, total_batches=3, completed_batches=0, fans_processed=0, errors=0, rate_limited=0, stale_tokens=0, total_requests=0, ) completed, total = PipelineRun.query.increment_batch_result( run.id, processed=10, errors=2, rate_limited=1, stale_tokens=0, api_requests=15, ) assert completed == 1 assert total == 3 run.refresh() assert run.fans_processed == 10 assert run.errors == 2 assert run.rate_limited == 1 assert run.total_requests == 15 @pytest.mark.db def test_increments_are_additive(self) -> None: run = create_model( PipelineRun, status=RunStatus.running, total_batches=2, completed_batches=0, fans_processed=0, errors=0, rate_limited=0, stale_tokens=0, total_requests=0, ) PipelineRun.query.increment_batch_result( run.id, processed=5, errors=0, rate_limited=0, stale_tokens=0 ) completed, total = PipelineRun.query.increment_batch_result( run.id, processed=3, errors=1, rate_limited=0, stale_tokens=0 ) assert completed == 2 assert total == 2 run.refresh() assert run.fans_processed == 8 assert run.errors == 1 @pytest.mark.db def test_missing_run_returns_zero(self) -> None: completed, total = PipelineRun.query.increment_batch_result( uuid.uuid4(), processed=1, errors=0, rate_limited=0, stale_tokens=0 ) assert completed == 0 assert total == 0 class TestPipelineRun: @pytest.mark.db @pytest.mark.parametrize( "status", [RunStatus.idle, RunStatus.done, RunStatus.error, RunStatus.cancelled] ) def test_non_active_is_never_stale(self, status: RunStatus) -> None: run = create_model(PipelineRun, status=status) assert not run.is_stale @pytest.mark.db def test_active_without_started_at_is_not_stale(self) -> None: run = create_model( PipelineRun, status=RunStatus.running, started_at=None, ) assert not run.is_stale @pytest.mark.db def test_stale_when_started_at_exceeds_timeout(self) -> None: run = create_model( PipelineRun, status=RunStatus.running, started_at=timezone.now() - timedelta(seconds=9999), ) with override_settings(pipeline_run_stale_timeout_s=100): assert run.is_stale @pytest.mark.db def test_not_stale_within_timeout(self) -> None: run = create_model( PipelineRun, status=RunStatus.running, started_at=timezone.now() - timedelta(seconds=10), ) with override_settings(pipeline_run_stale_timeout_s=3600): assert not run.is_stale @pytest.mark.db @pytest.mark.parametrize( "status", [ RunStatus.idle, RunStatus.queued, RunStatus.done, RunStatus.error, RunStatus.cancelled, ], ) def test_non_running_returns_none(self, status: RunStatus) -> None: run = create_model(PipelineRun, status=status) assert run.eta_seconds is None @pytest.mark.db def test_no_started_at_returns_none(self) -> None: run = create_model( PipelineRun, status=RunStatus.running, started_at=None, ) assert run.eta_seconds is None @pytest.mark.db def test_no_progress_returns_none(self) -> None: run = create_model( PipelineRun, status=RunStatus.running, started_at=timezone.now() - timedelta(seconds=60), fans_processed=0, total_fans=100, ) assert run.eta_seconds is None @pytest.mark.db def test_returns_estimate_based_on_rate(self) -> None: run = create_model( PipelineRun, status=RunStatus.running, started_at=timezone.now() - timedelta(seconds=10), fans_processed=10, total_fans=20, ) eta = run.eta_seconds assert eta is not None assert 8 <= eta <= 12 # ~10s remaining at 1 fan/s