mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-07 11:02:12 +02:00
fix: fix agent stream contract with cloudonix (#504)
This commit is contained in:
parent
7e2750f83f
commit
5a822232da
9 changed files with 319 additions and 114 deletions
|
|
@ -6,6 +6,8 @@ or a ``null`` ``session`` / ``disposition``) must produce a graceful error
|
|||
response, not an unhandled ``AttributeError`` (HTTP 500).
|
||||
"""
|
||||
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -34,6 +36,116 @@ def _json_request(body: bytes) -> Request:
|
|||
)
|
||||
|
||||
|
||||
class _FakeWebSocket:
|
||||
def __init__(self, *messages: str):
|
||||
self.receive_text = AsyncMock(side_effect=messages)
|
||||
self.close = AsyncMock()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_stream_reads_call_metadata_from_start_message():
|
||||
"""Cloudonix agent-stream uses start metadata, not call query params."""
|
||||
provider = CloudonixProvider({})
|
||||
websocket = _FakeWebSocket(
|
||||
json.dumps({"event": "connected"}),
|
||||
json.dumps(
|
||||
{
|
||||
"event": "start",
|
||||
"start": {
|
||||
"streamSid": "stream-123",
|
||||
"callSid": "call-123",
|
||||
"session": "session-123",
|
||||
"accountSid": "acme",
|
||||
"from": "+15551230001",
|
||||
"to": "+15551230002",
|
||||
"context": "inbound",
|
||||
"tracks": ["inbound"],
|
||||
"mediaFormat": {
|
||||
"encoding": "audio/x-mulaw",
|
||||
"sampleRate": 8000,
|
||||
"channels": 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
config = SimpleNamespace(
|
||||
id=7,
|
||||
credentials={
|
||||
"domain_id": "acme.cloudonix.net",
|
||||
"bearer_token": "secret-token",
|
||||
},
|
||||
)
|
||||
provider._find_config_by_domain = AsyncMock(return_value=config)
|
||||
provider._validate_session = AsyncMock(return_value=True)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"api.services.telephony.providers.cloudonix.provider.db_client"
|
||||
) as db_client,
|
||||
patch(
|
||||
"api.services.pipecat.run_pipeline.run_pipeline_telephony",
|
||||
new_callable=AsyncMock,
|
||||
) as run_pipeline,
|
||||
):
|
||||
db_client.update_workflow_run = AsyncMock()
|
||||
|
||||
await provider.handle_external_websocket(
|
||||
websocket,
|
||||
organization_id=44,
|
||||
workflow_id=101,
|
||||
user_id=202,
|
||||
workflow_run_id=303,
|
||||
params={},
|
||||
)
|
||||
|
||||
provider._find_config_by_domain.assert_awaited_once_with(44, "acme.cloudonix.net")
|
||||
provider._validate_session.assert_awaited_once_with(
|
||||
"acme.cloudonix.net", "session-123", "secret-token"
|
||||
)
|
||||
db_client.update_workflow_run.assert_awaited_once_with(
|
||||
run_id=303,
|
||||
initial_context={
|
||||
"caller_number": "+15551230001",
|
||||
"called_number": "+15551230002",
|
||||
"direction": "inbound",
|
||||
"cloudonix_context": "inbound",
|
||||
},
|
||||
gathered_context={
|
||||
"call_id": "session-123",
|
||||
"cloudonix_call_sid": "call-123",
|
||||
"cloudonix_stream_sid": "stream-123",
|
||||
},
|
||||
logs={
|
||||
"inbound_webhook": {
|
||||
"domain": "acme.cloudonix.net",
|
||||
"session": "session-123",
|
||||
"callSid": "call-123",
|
||||
"streamSid": "stream-123",
|
||||
"from": "+15551230001",
|
||||
"to": "+15551230002",
|
||||
"context": "inbound",
|
||||
"tracks": ["inbound"],
|
||||
"mediaFormat": {
|
||||
"encoding": "audio/x-mulaw",
|
||||
"sampleRate": 8000,
|
||||
"channels": 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
run_pipeline.assert_awaited_once()
|
||||
_, kwargs = run_pipeline.await_args
|
||||
assert kwargs["call_id"] == "session-123"
|
||||
assert kwargs["transport_kwargs"] == {
|
||||
"call_id": "session-123",
|
||||
"stream_sid": "stream-123",
|
||||
"bearer_token": "secret-token",
|
||||
"domain_id": "acme.cloudonix.net",
|
||||
}
|
||||
websocket.close.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cdr_route_handles_payload_without_session():
|
||||
"""A CDR payload missing the ``session`` object returns a graceful error
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@ class _RecordingARIConnection(ARIConnection):
|
|||
async def _handle_transfer_failed(
|
||||
self, transfer_id: str, channel_id: str, reason: str
|
||||
):
|
||||
raise AssertionError("completed transfer hangup should not publish transfer failure")
|
||||
raise AssertionError(
|
||||
"completed transfer hangup should not publish transfer failure"
|
||||
)
|
||||
|
||||
|
||||
def _completed_transfer_context():
|
||||
|
|
@ -73,7 +75,9 @@ def _completed_transfer_context():
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completed_transfer_tears_down_destination_when_caller_leaves(monkeypatch):
|
||||
async def test_completed_transfer_tears_down_destination_when_caller_leaves(
|
||||
monkeypatch,
|
||||
):
|
||||
fake_db = _FakeDbClient(_completed_transfer_context())
|
||||
monkeypatch.setattr(ari_manager, "db_client", fake_db)
|
||||
conn = _RecordingARIConnection()
|
||||
|
|
@ -89,7 +93,9 @@ async def test_completed_transfer_tears_down_destination_when_caller_leaves(monk
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_completed_transfer_tears_down_caller_when_destination_leaves(monkeypatch):
|
||||
async def test_completed_transfer_tears_down_caller_when_destination_leaves(
|
||||
monkeypatch,
|
||||
):
|
||||
fake_db = _FakeDbClient(_completed_transfer_context())
|
||||
monkeypatch.setattr(ari_manager, "db_client", fake_db)
|
||||
conn = _RecordingARIConnection()
|
||||
|
|
|
|||
64
api/tests/test_agent_stream_route.py
Normal file
64
api/tests/test_agent_stream_route.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class _FakeWebSocket:
|
||||
def __init__(self, query_params: dict[str, str] | None = None):
|
||||
self.query_params = query_params or {}
|
||||
self.accept = AsyncMock()
|
||||
self.close = AsyncMock()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_stream_uses_provider_path_param_not_query_param():
|
||||
from api.routes.agent_stream import agent_stream_websocket
|
||||
|
||||
websocket = _FakeWebSocket(
|
||||
{
|
||||
"provider": "twilio",
|
||||
"custom": "value",
|
||||
}
|
||||
)
|
||||
workflow = SimpleNamespace(
|
||||
id=11,
|
||||
user_id=22,
|
||||
organization_id=33,
|
||||
template_context_variables={"existing": "context"},
|
||||
)
|
||||
workflow_run = SimpleNamespace(id=44)
|
||||
provider = SimpleNamespace(handle_external_websocket=AsyncMock())
|
||||
spec = SimpleNamespace(provider_cls=lambda _config: provider)
|
||||
|
||||
with (
|
||||
patch("api.routes.agent_stream.telephony_registry") as registry,
|
||||
patch("api.routes.agent_stream.db_client") as db_client,
|
||||
patch(
|
||||
"api.routes.agent_stream.authorize_workflow_run_start",
|
||||
new=AsyncMock(
|
||||
return_value=SimpleNamespace(has_quota=True, error_message=None)
|
||||
),
|
||||
),
|
||||
):
|
||||
registry.get_optional.return_value = spec
|
||||
db_client.get_workflow_by_uuid_unscoped = AsyncMock(return_value=workflow)
|
||||
db_client.create_workflow_run = AsyncMock(return_value=workflow_run)
|
||||
db_client.update_workflow_run = AsyncMock()
|
||||
|
||||
await agent_stream_websocket(websocket, "cloudonix", "agent-uuid")
|
||||
|
||||
registry.get_optional.assert_called_once_with("cloudonix")
|
||||
db_client.create_workflow_run.assert_awaited_once()
|
||||
create_args = db_client.create_workflow_run.await_args.args
|
||||
create_kwargs = db_client.create_workflow_run.await_args.kwargs
|
||||
assert create_args[2] == "cloudonix"
|
||||
assert create_kwargs["initial_context"] == {
|
||||
"existing": "context",
|
||||
"provider": "cloudonix",
|
||||
"direction": "inbound",
|
||||
}
|
||||
provider.handle_external_websocket.assert_awaited_once()
|
||||
_, provider_kwargs = provider.handle_external_websocket.await_args
|
||||
assert provider_kwargs["params"] == {"custom": "value"}
|
||||
websocket.close.assert_not_awaited()
|
||||
Loading…
Add table
Add a link
Reference in a new issue