176 lines
6.4 KiB
Python
176 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
import socket
|
|
import struct
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import cast
|
|
|
|
from agent_call.real_cell import (
|
|
CellCallConfig,
|
|
CellCallError,
|
|
CellExecutionLedger,
|
|
RealCellCall,
|
|
RealCellWorker,
|
|
RTPMedia,
|
|
alaw_to_pcm16,
|
|
voice_level,
|
|
)
|
|
|
|
|
|
class FakeBroker:
|
|
def __init__(self, messages: list[dict]) -> None:
|
|
self.messages = list(messages)
|
|
self.acked: list[dict] = []
|
|
self.requeued: list[dict] = []
|
|
self.rejected: list[dict] = []
|
|
self.published: list[dict] = []
|
|
|
|
def declare_tenant(self, tenant_key: str) -> None:
|
|
self.tenant_key = tenant_key
|
|
|
|
def consume(self, _queue: str) -> dict | None:
|
|
return self.messages.pop(0) if self.messages else None
|
|
|
|
def ack(self, message: dict) -> None:
|
|
self.acked.append(message)
|
|
|
|
def requeue(self, message: dict) -> None:
|
|
self.requeued.append(message)
|
|
|
|
def reject(self, message: dict) -> None:
|
|
self.rejected.append(message)
|
|
|
|
def publish(
|
|
self, _exchange: str, _route: str, body: dict, **_kwargs: object
|
|
) -> None:
|
|
self.published.append(body)
|
|
|
|
|
|
class RealCellTests(unittest.TestCase):
|
|
def test_pcma_decode_and_rtp_payload(self) -> None:
|
|
media = RTPMedia("127.0.0.1", 0)
|
|
receiver = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
receiver.bind(("127.0.0.1", 0))
|
|
try:
|
|
media.peer = ("127.0.0.1", receiver.getsockname()[1])
|
|
sent = media.send_pcm16(b"\x00\x00" * 160, 8000)
|
|
packet, _ = receiver.recvfrom(512)
|
|
self.assertEqual(sent, 160)
|
|
self.assertEqual(len(packet), 172)
|
|
self.assertEqual(packet[1] & 0x7F, 8)
|
|
self.assertEqual(packet[12:], b"\xd5" * 160)
|
|
self.assertEqual(len(alaw_to_pcm16(packet[12:])), 320)
|
|
self.assertEqual(voice_level(alaw_to_pcm16(packet[12:])), 8)
|
|
finally:
|
|
media.close()
|
|
receiver.close()
|
|
|
|
def test_rtp_payload_handles_extension_and_padding(self) -> None:
|
|
payload = b"abc"
|
|
packet = struct.pack("!BBHII", 0xB0, 8, 1, 2, 3)
|
|
packet += struct.pack("!HH", 0, 1) + b"xxxx" + payload + b"\x00\x00\x03"
|
|
self.assertEqual(RTPMedia._payload(packet), payload)
|
|
self.assertIsNone(RTPMedia._payload(b"bad"))
|
|
|
|
def test_cell_config_rejects_non_pcma_and_bad_port(self) -> None:
|
|
with self.assertRaises(CellCallError):
|
|
CellCallConfig("http://127.0.0.1:8088", "u", "p", rtp_format="ulaw")
|
|
with self.assertRaises(CellCallError):
|
|
CellCallConfig("http://127.0.0.1:8088", "u", "p", rtp_bind_port=65536)
|
|
|
|
def test_ledger_marks_in_progress_as_in_doubt(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
ledger = CellExecutionLedger(Path(directory) / "ledger.sqlite3")
|
|
first = ledger.claim("exec-1", "15003164745")
|
|
second = ledger.claim("exec-1", "15003164745")
|
|
self.assertTrue(first["claimed"])
|
|
self.assertTrue(second["in_doubt"])
|
|
|
|
def test_worker_uses_queue_and_never_redials_terminal_execution(self) -> None:
|
|
command = {
|
|
"body": {
|
|
"command_type": "call.execute",
|
|
"tenant_id": "tenant-demo",
|
|
"tenant_key": "tenant-key",
|
|
"payload": {
|
|
"execution_id": "exec-1",
|
|
"agent_version_id": "agent_v1",
|
|
"callee": "15003164745",
|
|
},
|
|
}
|
|
}
|
|
broker = FakeBroker([command, command])
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
ledger = CellExecutionLedger(Path(directory) / "ledger.sqlite3")
|
|
fake_result = SimpleNamespace(
|
|
as_dict=lambda: {
|
|
"call_id": "call-1",
|
|
"status": "failed",
|
|
"reason_code": "CUSTOMER_SILENT",
|
|
"recording_path": "/private/recording.wav",
|
|
}
|
|
)
|
|
|
|
class FakeExecutor:
|
|
def __init__(self) -> None:
|
|
self.engine = SimpleNamespace(
|
|
config={"agent_version_id": "agent_v1"}
|
|
)
|
|
self.calls: list[str] = []
|
|
|
|
def start_authorized_call(self, callee: str) -> SimpleNamespace:
|
|
self.calls.append(callee)
|
|
return fake_result
|
|
|
|
fake_executor = FakeExecutor()
|
|
executor = cast(RealCellCall, fake_executor)
|
|
worker = RealCellWorker(broker, "tenant-key", ledger, executor)
|
|
first = worker.process_once()
|
|
second = worker.process_once()
|
|
if first is None or second is None:
|
|
self.fail("worker did not publish a call.finished event")
|
|
self.assertEqual(first["event_type"], "call.finished")
|
|
self.assertEqual(second["event_type"], "call.finished")
|
|
self.assertEqual(fake_executor.calls, ["15003164745"])
|
|
self.assertEqual(len(broker.acked), 2)
|
|
self.assertNotIn("recording_path", broker.published[0]["payload"])
|
|
|
|
def test_worker_rejects_wrong_tenant_route(self) -> None:
|
|
broker = FakeBroker(
|
|
[
|
|
{
|
|
"body": {
|
|
"command_type": "call.execute",
|
|
"tenant_id": "tenant-demo",
|
|
"tenant_key": "other-tenant",
|
|
"payload": {
|
|
"execution_id": "exec-1",
|
|
"agent_version_id": "agent_v1",
|
|
"callee": "15003164745",
|
|
},
|
|
}
|
|
}
|
|
]
|
|
)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
ledger = CellExecutionLedger(Path(directory) / "ledger.sqlite3")
|
|
executor = cast(
|
|
RealCellCall,
|
|
SimpleNamespace(
|
|
engine=SimpleNamespace(config={"agent_version_id": "agent_v1"})
|
|
),
|
|
)
|
|
worker = RealCellWorker(broker, "tenant-key", ledger, executor)
|
|
result = worker.process_once()
|
|
if result is None:
|
|
self.fail("worker returned no command result")
|
|
self.assertEqual(result["reason_code"], "COMMAND_INVALID")
|
|
self.assertEqual(len(broker.rejected), 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|