Files
hold-slayer/tests/test_call_flows.py
Robert Helewka ecf37658ce feat: add initial Hold Slayer AI telephony gateway implementation
Complete project scaffolding and core implementation of an AI-powered
telephony system that calls companies, navigates IVR menus, waits on
hold, and transfers to the user when a human answers.

Key components:
- FastAPI server with REST API, WebSocket, and MCP (SSE) interfaces
- SIP/VoIP call management via PJSUA2 with RTP audio streaming
- LLM-powered IVR navigation using OpenAI/Anthropic with tool calling
- Hold detection service combining audio analysis and silence detection
- Real-time STT (Whisper/Deepgram) and TTS (OpenAI/Piper) pipelines
- Call recording with per-channel and mixed audio capture
- Event bus (asyncio pub/sub) for real-time client updates
- Web dashboard with live call monitoring
- SQLite persistence via SQLAlchemy with call history and analytics
- Notification support (email, SMS, webhook, desktop)
- Docker Compose deployment with Opal VoIP and Opal Media containers
- Comprehensive test suite with unit, integration, and E2E tests
- Simplified .gitignore and full project documentation in README
2026-03-21 19:23:26 +00:00

174 lines
5.4 KiB
Python

"""
Tests for call flow models and serialization.
"""
import pytest
from models.call_flow import ActionType, CallFlow, CallFlowCreate, CallFlowStep, CallFlowSummary
class TestCallFlowStep:
"""Test CallFlowStep model."""
def test_basic_dtmf_step(self):
step = CallFlowStep(
id="press_1",
description="Press 1 for English",
action=ActionType.DTMF,
action_value="1",
expect="for english|para español",
next_step="main_menu",
)
assert step.id == "press_1"
assert step.action == ActionType.DTMF
assert step.action_value == "1"
assert step.timeout == 30 # default
def test_hold_step(self):
step = CallFlowStep(
id="hold_queue",
description="On hold waiting for agent",
action=ActionType.HOLD,
timeout=7200,
next_step="agent_connected",
notes="Average hold: 25-45 min. Plays Vivaldi. Kill me.",
)
assert step.action == ActionType.HOLD
assert step.timeout == 7200
assert "Vivaldi" in step.notes
def test_transfer_step(self):
step = CallFlowStep(
id="connected",
description="Agent picked up!",
action=ActionType.TRANSFER,
action_value="sip_phone",
)
assert step.action == ActionType.TRANSFER
class TestCallFlow:
"""Test CallFlow model."""
@pytest.fixture
def sample_flow(self):
return CallFlow(
id="test-bank",
name="Test Bank - Main Line",
phone_number="+18005551234",
description="Test bank IVR",
steps=[
CallFlowStep(
id="greeting",
description="Language selection",
action=ActionType.DTMF,
action_value="1",
expect="for english",
next_step="main_menu",
),
CallFlowStep(
id="main_menu",
description="Main menu",
action=ActionType.LISTEN,
next_step="agent_request",
fallback_step="agent_request",
),
CallFlowStep(
id="agent_request",
description="Request agent",
action=ActionType.DTMF,
action_value="0",
next_step="hold_queue",
),
CallFlowStep(
id="hold_queue",
description="Hold queue",
action=ActionType.HOLD,
timeout=3600,
next_step="agent_connected",
),
CallFlowStep(
id="agent_connected",
description="Agent connected",
action=ActionType.TRANSFER,
action_value="sip_phone",
),
],
tags=["bank", "personal"],
avg_hold_time=2100,
success_rate=0.92,
)
def test_step_count(self, sample_flow):
assert len(sample_flow.steps) == 5
def test_get_step(self, sample_flow):
step = sample_flow.get_step("hold_queue")
assert step is not None
assert step.action == ActionType.HOLD
assert step.timeout == 3600
def test_get_step_not_found(self, sample_flow):
assert sample_flow.get_step("nonexistent") is None
def test_first_step(self, sample_flow):
first = sample_flow.first_step()
assert first is not None
assert first.id == "greeting"
def test_steps_by_id(self, sample_flow):
steps = sample_flow.steps_by_id()
assert len(steps) == 5
assert "greeting" in steps
assert "agent_connected" in steps
assert steps["agent_connected"].action == ActionType.TRANSFER
def test_serialization_roundtrip(self, sample_flow):
"""Test JSON serialization and deserialization."""
json_str = sample_flow.model_dump_json()
restored = CallFlow.model_validate_json(json_str)
assert restored.id == sample_flow.id
assert len(restored.steps) == len(sample_flow.steps)
assert restored.steps[0].id == "greeting"
assert restored.avg_hold_time == 2100
class TestCallFlowCreate:
"""Test call flow creation model."""
def test_minimal_create(self):
create = CallFlowCreate(
name="My Bank",
phone_number="+18005551234",
steps=[
CallFlowStep(
id="start",
description="Start",
action=ActionType.HOLD,
next_step="end",
),
],
)
assert create.name == "My Bank"
assert len(create.steps) == 1
assert create.tags == []
assert create.notes is None
class TestCallFlowSummary:
"""Test lightweight summary model."""
def test_summary(self):
summary = CallFlowSummary(
id="chase-bank-main",
name="Chase Bank - Main",
phone_number="+18005551234",
step_count=6,
avg_hold_time=2100,
success_rate=0.92,
times_used=15,
tags=["bank"],
)
assert summary.step_count == 6
assert summary.success_rate == 0.92