mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-10 08:05:22 +02:00
chore: return formatted transcript url
- Return formatted transcript and recording URL - Harden campaign dispatcher logic
This commit is contained in:
parent
0716582aa7
commit
7810923bca
30 changed files with 525 additions and 136 deletions
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue