fix recording timing and sync call contracts
management-images / build-and-publish (push) Successful in 34s
management-images / build-and-publish (push) Successful in 34s
This commit is contained in:
+125
-1
@@ -1,10 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import socket
|
||||
import struct
|
||||
import tempfile
|
||||
import unittest
|
||||
import wave
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, cast
|
||||
@@ -52,7 +54,106 @@ class FakeBroker:
|
||||
self.published.append(body)
|
||||
|
||||
|
||||
def wav_bytes(pcm: bytes) -> bytes:
|
||||
output = io.BytesIO()
|
||||
with wave.open(output, "wb") as source:
|
||||
source.setnchannels(1)
|
||||
source.setsampwidth(2)
|
||||
source.setframerate(8000)
|
||||
source.writeframes(pcm)
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
class RealCellTests(unittest.TestCase):
|
||||
def _build_lifecycle_call(
|
||||
self, order: list[str], answer_error: CellCallError | None = None
|
||||
) -> tuple[RealCellCall, Any]:
|
||||
config = CellCallConfig(
|
||||
"http://127.0.0.1:8088", "u", "p", event_timeout_s=0.1
|
||||
)
|
||||
engine = SimpleNamespace(
|
||||
config={
|
||||
"agent_version_id": "agent_v1",
|
||||
"conversation": {
|
||||
"opening": "hello",
|
||||
"max_turns": 1,
|
||||
"allow_interrupt": False,
|
||||
},
|
||||
},
|
||||
history=[],
|
||||
)
|
||||
engine.speak = lambda _text, _turn_id: (
|
||||
order.append("opening") or {"audio": b""}
|
||||
)
|
||||
call = RealCellCall(config, cast(Any, engine), cast(Any, SimpleNamespace()))
|
||||
|
||||
class FakeARI:
|
||||
def __init__(self) -> None:
|
||||
self.record_requests = 0
|
||||
|
||||
def events(self, _app: str) -> Any:
|
||||
return SimpleNamespace(close=lambda: None)
|
||||
|
||||
def request(
|
||||
self, method: str, resource: str, _params=None, **_kwargs: object
|
||||
) -> dict[str, str] | dict:
|
||||
if method == "POST" and resource.endswith("/record"):
|
||||
self.record_requests += 1
|
||||
order.append("record")
|
||||
if resource == "channels/externalMedia":
|
||||
return {"id": "external"}
|
||||
if resource == "channels":
|
||||
return {"id": "target"}
|
||||
return {}
|
||||
|
||||
fake_ari = FakeARI()
|
||||
call_any = cast(Any, call)
|
||||
call_any.ari = fake_ari
|
||||
call_any._start_event_reader = lambda: None
|
||||
call_any._try_set_external_media_peer = lambda: False
|
||||
call_any._add_when_ready = lambda _channel_id: True
|
||||
|
||||
def wait_answer(_result: Any) -> None:
|
||||
order.append("answer")
|
||||
if answer_error is not None:
|
||||
raise answer_error
|
||||
|
||||
call_any._wait_answer = wait_answer
|
||||
call_any._play = lambda _audio: order.append("play")
|
||||
call_any._capture_turn = lambda _timeout: (
|
||||
order.append("capture") or b""
|
||||
)
|
||||
call_any._finish_recording = lambda: None
|
||||
call_any._cleanup = lambda: None
|
||||
return call, fake_ari
|
||||
|
||||
def test_recording_starts_after_answer_before_opening_tts(self) -> None:
|
||||
order: list[str] = []
|
||||
call, fake_ari = self._build_lifecycle_call(order)
|
||||
try:
|
||||
result = call.start_authorized_call("123")
|
||||
finally:
|
||||
if call.media is not None:
|
||||
call.media.close()
|
||||
self.assertTrue(result.connected)
|
||||
self.assertEqual(fake_ari.record_requests, 1)
|
||||
self.assertLess(order.index("answer"), order.index("record"))
|
||||
self.assertLess(order.index("record"), order.index("opening"))
|
||||
|
||||
def test_answer_failure_does_not_start_recording(self) -> None:
|
||||
order: list[str] = []
|
||||
call, fake_ari = self._build_lifecycle_call(
|
||||
order, CellCallError("CALL_NOT_ANSWERED", "not answered")
|
||||
)
|
||||
try:
|
||||
result = call.start_authorized_call("123")
|
||||
finally:
|
||||
if call.media is not None:
|
||||
call.media.close()
|
||||
self.assertFalse(result.connected)
|
||||
self.assertEqual(fake_ari.record_requests, 0)
|
||||
self.assertNotIn("opening", order)
|
||||
|
||||
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)
|
||||
@@ -202,7 +303,7 @@ class RealCellTests(unittest.TestCase):
|
||||
|
||||
def request(self, method: str, resource: str, **_kwargs):
|
||||
self.calls.append((method, resource))
|
||||
return {} if method == "POST" else b"RIFFtest"
|
||||
return {} if method == "POST" else wav_bytes(b"\x00\x00")
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
config = CellCallConfig(
|
||||
@@ -226,6 +327,29 @@ class RealCellTests(unittest.TestCase):
|
||||
("DELETE", fake_ari.calls[0][1].rsplit("/stop", 1)[0]), fake_ari.calls
|
||||
)
|
||||
|
||||
def test_finish_recording_rejects_zero_frame_wav(self) -> None:
|
||||
class FakeARI:
|
||||
def request(self, method: str, resource: str, **_kwargs):
|
||||
return {} if method == "POST" else wav_bytes(b"")
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
config = CellCallConfig(
|
||||
"http://127.0.0.1:8088", "u", "p", recording_dir=directory
|
||||
)
|
||||
call = cast(
|
||||
Any,
|
||||
RealCellCall(
|
||||
config,
|
||||
cast(Any, SimpleNamespace(config={})),
|
||||
cast(Any, SimpleNamespace()),
|
||||
),
|
||||
)
|
||||
call.ari = FakeARI()
|
||||
self.assertIsNone(call._finish_recording())
|
||||
self.assertFalse(
|
||||
(Path(directory) / f"{call.recording_name}.wav").exists()
|
||||
)
|
||||
|
||||
def test_cell_config_rejects_non_pcma_bad_port_and_prefix(self) -> None:
|
||||
with self.assertRaises(CellCallError):
|
||||
CellCallConfig("http://127.0.0.1:8088", "u", "p", rtp_format="ulaw")
|
||||
|
||||
Reference in New Issue
Block a user