chore: return formatted transcript url

- Return formatted transcript and recording URL
- Harden campaign dispatcher logic
This commit is contained in:
Abhishek Kumar 2026-05-26 13:24:12 +05:30
parent 0716582aa7
commit 7810923bca
30 changed files with 525 additions and 136 deletions

View file

@ -681,6 +681,54 @@ class TestProcessBatchConcurrency:
assert states.get("processing", 0) == 0
class TestProcessBatchCancellation:
"""Cancellation cleanup for claimed queued runs."""
@pytest.mark.asyncio
async def test_cancelled_batch_returns_claimed_runs_without_workflows(self):
dispatcher = CampaignCallDispatcher()
campaign = MagicMock()
campaign.id = 42
campaign.state = "running"
campaign.organization_id = 7
campaign.rate_limit_per_second = 1
campaign.telephony_configuration_id = 170
queued_runs = [MagicMock(id=101), MagicMock(id=102), MagicMock(id=103)]
provider = MagicMock()
provider.from_numbers = []
with (
patch(
"api.services.campaign.campaign_call_dispatcher.db_client"
) as mock_db,
patch.object(
dispatcher,
"get_provider_for_campaign",
AsyncMock(return_value=provider),
),
patch.object(
dispatcher,
"apply_rate_limit",
AsyncMock(side_effect=asyncio.CancelledError),
),
):
mock_db.get_campaign_by_id = AsyncMock(return_value=campaign)
mock_db.claim_queued_runs_for_processing = AsyncMock(
return_value=queued_runs
)
mock_db.return_processing_queued_runs_without_workflow = AsyncMock(
return_value=3
)
with pytest.raises(asyncio.CancelledError):
await dispatcher.process_batch(campaign_id=42, batch_size=3)
mock_db.return_processing_queued_runs_without_workflow.assert_awaited_once_with(
[101, 102, 103]
)
class TestProcessBatchEdgeCases:
"""Edge case tests for process_batch."""

View file

@ -23,10 +23,9 @@ class TestProcessCampaignBatchFailureLogs:
``batch_failed`` entry."""
@pytest.mark.asyncio
async def test_phone_number_pool_exhausted_logs_specific_event(self):
"""When PhoneNumberPoolExhaustedError propagates from process_batch,
the campaign log entry should use event='phone_number_pool_exhausted'
with a clear message not the generic 'batch_failed' bucket."""
async def test_phone_number_pool_exhausted_retries_before_final_failure(self):
"""The first two consecutive pool exhaustion attempts keep the
campaign running and schedule another batch."""
with (
patch("api.tasks.campaign_tasks.campaign_call_dispatcher") as mock_disp,
patch("api.tasks.campaign_tasks.db_client") as mock_db,
@ -37,6 +36,46 @@ class TestProcessCampaignBatchFailureLogs:
mock_disp.process_batch = AsyncMock(
side_effect=PhoneNumberPoolExhaustedError(organization_id=7)
)
mock_db.increment_campaign_metadata_counter = AsyncMock(return_value=2)
mock_db.update_campaign = AsyncMock()
mock_db.append_campaign_log = AsyncMock()
mock_pub = AsyncMock()
mock_get_pub.return_value = mock_pub
await process_campaign_batch({}, campaign_id=42)
mock_db.update_campaign.assert_not_awaited()
mock_pub.publish_batch_failed.assert_not_awaited()
mock_pub.publish_batch_completed.assert_awaited_once_with(
campaign_id=42,
processed_count=0,
failed_count=0,
batch_size=10,
)
mock_db.append_campaign_log.assert_called_once()
kwargs = mock_db.append_campaign_log.call_args.kwargs
assert kwargs["campaign_id"] == 42
assert kwargs["event"] == "phone_number_pool_exhausted_retry"
assert kwargs["level"] == "warning"
assert kwargs["details"]["organization_id"] == 7
assert kwargs["details"]["attempt"] == 2
@pytest.mark.asyncio
async def test_phone_number_pool_exhausted_fails_on_third_attempt(self):
"""The third consecutive pool exhaustion attempt marks the campaign
failed with a specific operator-facing log entry."""
with (
patch("api.tasks.campaign_tasks.campaign_call_dispatcher") as mock_disp,
patch("api.tasks.campaign_tasks.db_client") as mock_db,
patch(
"api.tasks.campaign_tasks.get_campaign_event_publisher"
) as mock_get_pub,
):
mock_disp.process_batch = AsyncMock(
side_effect=PhoneNumberPoolExhaustedError(organization_id=7)
)
mock_db.increment_campaign_metadata_counter = AsyncMock(return_value=3)
mock_db.update_campaign = AsyncMock()
mock_db.append_campaign_log = AsyncMock()
mock_pub = AsyncMock()
@ -48,6 +87,7 @@ class TestProcessCampaignBatchFailureLogs:
mock_db.update_campaign.assert_called_once_with(
campaign_id=42, state="failed"
)
mock_pub.publish_batch_failed.assert_awaited_once()
mock_db.append_campaign_log.assert_called_once()
kwargs = mock_db.append_campaign_log.call_args.kwargs
@ -56,6 +96,7 @@ class TestProcessCampaignBatchFailureLogs:
assert kwargs["level"] == "error"
assert "phone number" in kwargs["message"].lower()
assert kwargs["details"]["organization_id"] == 7
assert kwargs["details"]["attempt"] == 3
@pytest.mark.asyncio
async def test_concurrent_slot_timeout_still_logs_specific_event(self):