Files
pipecat/tests/genesys/test_genesys_serializer.py
Pablo Ois Lagarde bc0e7130b8 fix: always include parameters field in Genesys AudioHook messages
The AudioHook protocol requires every message to carry a `parameters`
object. `_create_message` conditionally included it only when parameters
were truthy, so pong responses and closed responses without
outputVariables were sent without the field.

Clients that validate message structure (including the Genesys reference
implementation) rejected these messages, which broke server sequence
tracking and prevented outputVariables from reaching the Architect flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:37:53 -03:00

377 lines
14 KiB
Python

#
# Copyright (c) 2024-2026, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Tests for the Genesys AudioHook serializer."""
import json
import pytest
from pipecat.frames.frames import InputDTMFFrame, OutputTransportMessageUrgentFrame
from pipecat.serializers.genesys import AudioHookChannel, GenesysAudioHookSerializer
class TestGenesysAudioHookSerializer:
"""Tests for GenesysAudioHookSerializer."""
# ==================== Initialization Tests ====================
def test_create_serializer_default_params(self):
"""Test creating serializer with default parameters."""
serializer = GenesysAudioHookSerializer()
# session_id is auto-generated as UUID
assert serializer.session_id != ""
assert len(serializer.session_id) == 36 # UUID format
assert serializer.is_open is False
assert serializer.is_paused is False
def test_create_serializer_with_custom_params(self):
"""Test creating serializer with custom parameters."""
params = GenesysAudioHookSerializer.InputParams(
channel=AudioHookChannel.BOTH,
sample_rate=16000,
supported_languages=["es-ES", "en-US"],
selected_language="es-ES",
start_paused=True,
)
serializer = GenesysAudioHookSerializer(params=params)
assert serializer.session_id != ""
# ==================== Response Creation Tests ====================
def test_create_opened_response(self):
"""Test creating an opened response message."""
serializer = GenesysAudioHookSerializer()
msg = serializer.create_opened_response()
assert msg["type"] == "opened"
assert msg["version"] == "2"
assert msg["id"] == serializer.session_id
assert "parameters" in msg
assert serializer.is_open is True
def test_create_opened_response_with_languages(self):
"""Test creating an opened response with language options."""
serializer = GenesysAudioHookSerializer()
msg = serializer.create_opened_response(
supported_languages=["es", "en", "fr"],
selected_language="es",
)
assert msg["parameters"]["supportedLanguages"] == ["es", "en", "fr"]
assert msg["parameters"]["selectedLanguage"] == "es"
def test_create_pong_response(self):
"""Test creating a pong response message."""
serializer = GenesysAudioHookSerializer()
msg = serializer.create_pong_response()
assert msg["type"] == "pong"
assert msg["id"] == serializer.session_id
assert msg["parameters"] == {}
def test_create_closed_response(self):
"""Test creating a closed response message."""
serializer = GenesysAudioHookSerializer()
serializer._is_open = True
msg = serializer.create_closed_response()
assert msg["type"] == "closed"
assert serializer.is_open is False
assert msg["parameters"] == {} # Empty parameters when no output_variables
def test_create_closed_response_with_output_variables(self):
"""Test creating a closed response with custom output variables."""
serializer = GenesysAudioHookSerializer()
serializer._is_open = True
msg = serializer.create_closed_response(
output_variables={
"intent": "billing_inquiry",
"customer_verified": True,
"summary": "Customer asked about their bill",
}
)
assert msg["type"] == "closed"
assert msg["parameters"]["outputVariables"]["intent"] == "billing_inquiry"
assert msg["parameters"]["outputVariables"]["customer_verified"] is True
assert msg["parameters"]["outputVariables"]["summary"] == "Customer asked about their bill"
def test_create_resumed_response(self):
"""Test creating a resumed response message."""
serializer = GenesysAudioHookSerializer()
serializer._is_paused = True
msg = serializer.create_resumed_response()
assert msg["type"] == "resumed"
assert serializer.is_paused is False
def test_create_disconnect_message(self):
"""Test creating a disconnect message."""
serializer = GenesysAudioHookSerializer()
msg = serializer.create_disconnect_message(
reason="completed",
action="transfer",
)
assert msg["type"] == "disconnect"
assert msg["parameters"]["reason"] == "completed"
assert msg["parameters"]["outputVariables"]["action"] == "transfer"
def test_create_disconnect_message_with_output_variables(self):
"""Test creating a disconnect message with custom output variables."""
serializer = GenesysAudioHookSerializer()
msg = serializer.create_disconnect_message(
reason="completed",
action="finished",
output_variables={"result": "success", "code": "123"},
)
assert msg["parameters"]["outputVariables"]["result"] == "success"
assert msg["parameters"]["outputVariables"]["code"] == "123"
def test_create_error_message(self):
"""Test creating an error message."""
serializer = GenesysAudioHookSerializer()
msg = serializer.create_error_message(
code=500,
message="Internal error",
retryable=True,
)
assert msg["type"] == "error"
assert msg["parameters"]["code"] == 500
assert msg["parameters"]["message"] == "Internal error"
assert msg["parameters"]["retryable"] is True
# ==================== Message Handling Tests ====================
@pytest.mark.asyncio
async def test_handle_open_message(self, sample_open_message):
"""Test handling an open message returns opened frame."""
serializer = GenesysAudioHookSerializer()
result = await serializer.deserialize(json.dumps(sample_open_message))
# Now returns OutputTransportMessageUrgentFrame with opened response
assert isinstance(result, OutputTransportMessageUrgentFrame)
assert result.message["type"] == "opened"
assert serializer.session_id == "test-session-123"
assert serializer.conversation_id == "conv-456"
@pytest.mark.asyncio
async def test_handle_open_message_extracts_participant(self, sample_open_message):
"""Test that open message extracts participant info."""
serializer = GenesysAudioHookSerializer()
await serializer.deserialize(json.dumps(sample_open_message))
assert serializer.participant is not None
assert serializer.participant["ani"] == "+1234567890"
assert serializer.participant["dnis"] == "+0987654321"
@pytest.mark.asyncio
async def test_handle_open_message_uses_params(self, sample_open_message):
"""Test that open message uses InputParams for response."""
params = GenesysAudioHookSerializer.InputParams(
supported_languages=["es-ES", "en-US"],
selected_language="es-ES",
start_paused=True,
)
serializer = GenesysAudioHookSerializer(params=params)
result = await serializer.deserialize(json.dumps(sample_open_message))
assert isinstance(result, OutputTransportMessageUrgentFrame)
assert result.message["parameters"]["supportedLanguages"] == ["es-ES", "en-US"]
assert result.message["parameters"]["selectedLanguage"] == "es-ES"
assert result.message["parameters"]["startPaused"] is True
@pytest.mark.asyncio
async def test_handle_open_message_extracts_input_variables(
self, sample_open_message_with_input_variables
):
"""Test that open message extracts inputVariables from Genesys."""
serializer = GenesysAudioHookSerializer()
await serializer.deserialize(json.dumps(sample_open_message_with_input_variables))
assert serializer.input_variables is not None
assert serializer.input_variables["customer_id"] == "cust-789"
assert serializer.input_variables["queue_name"] == "billing"
assert serializer.input_variables["priority"] == "high"
assert serializer.input_variables["language"] == "es-ES"
@pytest.mark.asyncio
async def test_handle_ping_message(self, sample_ping_message):
"""Test handling a ping message returns pong frame."""
serializer = GenesysAudioHookSerializer()
result = await serializer.deserialize(json.dumps(sample_ping_message))
assert isinstance(result, OutputTransportMessageUrgentFrame)
assert result.message["type"] == "pong"
@pytest.mark.asyncio
async def test_handle_close_message(self, sample_close_message):
"""Test handling a close message returns closed frame."""
serializer = GenesysAudioHookSerializer()
serializer._is_open = True
result = await serializer.deserialize(json.dumps(sample_close_message))
assert isinstance(result, OutputTransportMessageUrgentFrame)
assert result.message["type"] == "closed"
assert serializer.is_open is False
@pytest.mark.asyncio
async def test_handle_close_message_includes_output_variables(self, sample_close_message):
"""Test that close response includes output variables when set."""
serializer = GenesysAudioHookSerializer()
serializer._is_open = True
# Set output variables before close
serializer.set_output_variables(
{"intent": "support", "resolved": True, "transfer_to": "agent_queue"}
)
result = await serializer.deserialize(json.dumps(sample_close_message))
assert isinstance(result, OutputTransportMessageUrgentFrame)
assert result.message["type"] == "closed"
assert result.message["parameters"]["outputVariables"]["intent"] == "support"
assert result.message["parameters"]["outputVariables"]["resolved"] is True
assert result.message["parameters"]["outputVariables"]["transfer_to"] == "agent_queue"
# ==================== Output Variables Tests ====================
def test_set_output_variables(self):
"""Test setting output variables."""
serializer = GenesysAudioHookSerializer()
assert serializer.output_variables is None
serializer.set_output_variables({"intent": "billing", "score": 0.95})
assert serializer.output_variables is not None
assert serializer.output_variables["intent"] == "billing"
assert serializer.output_variables["score"] == 0.95
def test_set_output_variables_overwrites(self):
"""Test that setting output variables overwrites previous values."""
serializer = GenesysAudioHookSerializer()
serializer.set_output_variables({"first": "value"})
serializer.set_output_variables({"second": "value"})
assert "first" not in serializer.output_variables
assert serializer.output_variables["second"] == "value"
@pytest.mark.asyncio
async def test_handle_pause_message(self, sample_pause_message):
"""Test handling a pause message."""
serializer = GenesysAudioHookSerializer()
serializer._is_open = True
result = await serializer.deserialize(json.dumps(sample_pause_message))
assert result is None # Pause is handled internally
assert serializer.is_paused is True
@pytest.mark.asyncio
async def test_handle_update_message(self, sample_update_message):
"""Test handling an update message."""
serializer = GenesysAudioHookSerializer()
result = await serializer.deserialize(json.dumps(sample_update_message))
assert result is None # Update is handled internally
assert serializer.participant is not None
assert serializer.participant["name"] == "John Doe"
@pytest.mark.asyncio
async def test_handle_error_message(self, sample_error_message):
"""Test handling an error message."""
serializer = GenesysAudioHookSerializer()
result = await serializer.deserialize(json.dumps(sample_error_message))
assert result is None # Error is logged but returns None
# ==================== DTMF Tests ====================
@pytest.mark.asyncio
async def test_handle_dtmf_digit(self, sample_dtmf_message):
"""Test handling a DTMF digit message."""
serializer = GenesysAudioHookSerializer()
result = await serializer.deserialize(json.dumps(sample_dtmf_message))
assert isinstance(result, InputDTMFFrame)
assert result.button.value == "5"
@pytest.mark.asyncio
async def test_handle_dtmf_star(self, sample_dtmf_star_message):
"""Test handling a DTMF star (*) message."""
serializer = GenesysAudioHookSerializer()
result = await serializer.deserialize(json.dumps(sample_dtmf_star_message))
assert isinstance(result, InputDTMFFrame)
assert result.button.value == "*"
@pytest.mark.asyncio
async def test_handle_dtmf_hash(self, sample_dtmf_hash_message):
"""Test handling a DTMF hash (#) message."""
serializer = GenesysAudioHookSerializer()
result = await serializer.deserialize(json.dumps(sample_dtmf_hash_message))
assert isinstance(result, InputDTMFFrame)
assert result.button.value == "#"
@pytest.mark.asyncio
async def test_handle_dtmf_empty_digit(self):
"""Test handling a DTMF message without digit."""
serializer = GenesysAudioHookSerializer()
dtmf_msg = {
"version": "2",
"type": "dtmf",
"seq": 6,
"id": "test-session-123",
"parameters": {},
}
result = await serializer.deserialize(json.dumps(dtmf_msg))
assert result is None # No digit provided
# ==================== Sequence Number Tests ====================
def test_sequence_numbers_increment(self):
"""Test that sequence numbers increment correctly."""
serializer = GenesysAudioHookSerializer()
response1 = serializer.create_pong_response()
response2 = serializer.create_pong_response()
response3 = serializer.create_pong_response()
assert response1["seq"] == 1
assert response2["seq"] == 2
assert response3["seq"] == 3