fix recording timing and sync call contracts
management-images / build-and-publish (push) Successful in 34s

This commit is contained in:
2026-09-17 17:43:37 +08:00
parent fa6925010b
commit d17bbe15d9
6 changed files with 773 additions and 19 deletions
+125 -1
View File
@@ -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")