664 lines
22 KiB
Python
664 lines
22 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from collections.abc import AsyncIterator
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from livekit.agents.llm import ChatContext, ChatMessage, FunctionCall, FunctionCallOutput
|
|
from livekit.agents.metrics import (
|
|
AgentSessionUsage,
|
|
InterruptionModelUsage,
|
|
LLMModelUsage,
|
|
STTModelUsage,
|
|
TTSModelUsage,
|
|
)
|
|
from livekit.agents.voice.events import (
|
|
AgentStateChangedEvent,
|
|
ConversationItemAddedEvent,
|
|
ErrorEvent,
|
|
SessionUsageUpdatedEvent,
|
|
UserInputTranscribedEvent,
|
|
UserStateChangedEvent,
|
|
)
|
|
from livekit.agents.voice.remote_session import (
|
|
RoomSessionTransport,
|
|
SessionHost,
|
|
SessionTransport,
|
|
_chat_item_to_proto,
|
|
_metrics_to_proto,
|
|
_session_usage_to_proto,
|
|
)
|
|
from livekit.protocol.agent_pb import agent_session as agent_pb
|
|
|
|
pytestmark = [pytest.mark.unit, pytest.mark.virtual_time, pytest.mark.no_concurrent]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# In-memory transport for testing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class InMemoryTransport(SessionTransport):
|
|
"""A simple in-memory transport that captures sent messages."""
|
|
|
|
def __init__(self) -> None:
|
|
self.sent: list[agent_pb.AgentSessionMessage] = []
|
|
self._inbound: asyncio.Queue[agent_pb.AgentSessionMessage] = asyncio.Queue()
|
|
self._closed = False
|
|
|
|
async def start(self) -> None:
|
|
pass
|
|
|
|
async def send_message(self, msg: agent_pb.AgentSessionMessage) -> None:
|
|
if self._closed:
|
|
return
|
|
self.sent.append(msg)
|
|
|
|
async def close(self) -> None:
|
|
self._closed = True
|
|
|
|
def inject(self, msg: agent_pb.AgentSessionMessage) -> None:
|
|
self._inbound.put_nowait(msg)
|
|
|
|
def __aiter__(self) -> AsyncIterator[agent_pb.AgentSessionMessage]:
|
|
return self
|
|
|
|
async def __anext__(self) -> agent_pb.AgentSessionMessage:
|
|
if self._closed:
|
|
raise StopAsyncIteration
|
|
try:
|
|
return await asyncio.wait_for(self._inbound.get(), timeout=0.5)
|
|
except (asyncio.TimeoutError, asyncio.CancelledError):
|
|
raise StopAsyncIteration from None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Proto conversion helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestChatItemToProto:
|
|
def test_chat_message(self) -> None:
|
|
msg = ChatMessage(role="user", content=["hello"], id="msg-1")
|
|
pb_item = _chat_item_to_proto(msg)
|
|
assert pb_item.HasField("message")
|
|
assert pb_item.message.id == "msg-1"
|
|
assert pb_item.message.role == agent_pb.USER
|
|
assert len(pb_item.message.content) == 1
|
|
assert pb_item.message.content[0].text == "hello"
|
|
|
|
def test_chat_message_assistant(self) -> None:
|
|
msg = ChatMessage(role="assistant", content=["hi there"], id="msg-2")
|
|
pb_item = _chat_item_to_proto(msg)
|
|
assert pb_item.message.role == agent_pb.ASSISTANT
|
|
|
|
def test_chat_message_developer(self) -> None:
|
|
msg = ChatMessage(role="developer", content=["system prompt"], id="msg-3")
|
|
pb_item = _chat_item_to_proto(msg)
|
|
assert pb_item.message.role == agent_pb.DEVELOPER
|
|
|
|
def test_function_call(self) -> None:
|
|
fc = FunctionCall(
|
|
id="fc-1",
|
|
call_id="call-1",
|
|
name="get_weather",
|
|
arguments='{"location": "NYC"}',
|
|
)
|
|
pb_item = _chat_item_to_proto(fc)
|
|
assert pb_item.HasField("function_call")
|
|
assert pb_item.function_call.name == "get_weather"
|
|
assert pb_item.function_call.call_id == "call-1"
|
|
assert pb_item.function_call.arguments == '{"location": "NYC"}'
|
|
|
|
def test_function_call_output(self) -> None:
|
|
fco = FunctionCallOutput(
|
|
call_id="call-1",
|
|
output="sunny, 72F",
|
|
is_error=False,
|
|
)
|
|
pb_item = _chat_item_to_proto(fco)
|
|
assert pb_item.HasField("function_call_output")
|
|
assert pb_item.function_call_output.call_id == "call-1"
|
|
assert pb_item.function_call_output.output == "sunny, 72F"
|
|
assert pb_item.function_call_output.is_error is False
|
|
|
|
def test_function_call_output_error(self) -> None:
|
|
fco = FunctionCallOutput(
|
|
call_id="call-2",
|
|
output="not found",
|
|
is_error=True,
|
|
)
|
|
pb_item = _chat_item_to_proto(fco)
|
|
assert pb_item.function_call_output.is_error is True
|
|
|
|
|
|
class TestMetricsToProto:
|
|
def test_empty(self) -> None:
|
|
pb = _metrics_to_proto(None)
|
|
assert isinstance(pb, agent_pb.MetricsReport)
|
|
|
|
def test_with_fields(self) -> None:
|
|
metrics = {
|
|
"transcription_delay": 0.1,
|
|
"end_of_turn_delay": 0.2,
|
|
"llm_node_ttft": 0.3,
|
|
"tts_node_ttfb": 0.4,
|
|
"e2e_latency": 0.5,
|
|
}
|
|
pb = _metrics_to_proto(metrics)
|
|
assert pb.transcription_delay == pytest.approx(0.1)
|
|
assert pb.end_of_turn_delay == pytest.approx(0.2)
|
|
assert pb.llm_node_ttft == pytest.approx(0.3)
|
|
assert pb.tts_node_ttfb == pytest.approx(0.4)
|
|
assert pb.e2e_latency == pytest.approx(0.5)
|
|
|
|
def test_partial_fields(self) -> None:
|
|
metrics = {"transcription_delay": 0.42}
|
|
pb = _metrics_to_proto(metrics)
|
|
assert pb.transcription_delay == pytest.approx(0.42)
|
|
|
|
|
|
class TestSessionUsageToProto:
|
|
def test_llm_usage(self) -> None:
|
|
usage = AgentSessionUsage(
|
|
model_usage=[
|
|
LLMModelUsage(
|
|
provider="openai",
|
|
model="gpt-4",
|
|
input_tokens=100,
|
|
output_tokens=50,
|
|
)
|
|
]
|
|
)
|
|
pb = _session_usage_to_proto(usage)
|
|
assert len(pb.model_usage) == 1
|
|
assert pb.model_usage[0].HasField("llm")
|
|
assert pb.model_usage[0].llm.provider == "openai"
|
|
assert pb.model_usage[0].llm.model == "gpt-4"
|
|
assert pb.model_usage[0].llm.input_tokens == 100
|
|
assert pb.model_usage[0].llm.output_tokens == 50
|
|
|
|
def test_tts_usage(self) -> None:
|
|
usage = AgentSessionUsage(
|
|
model_usage=[
|
|
TTSModelUsage(
|
|
provider="elevenlabs",
|
|
model="eleven_turbo_v2",
|
|
characters_count=500,
|
|
audio_duration=3.5,
|
|
)
|
|
]
|
|
)
|
|
pb = _session_usage_to_proto(usage)
|
|
assert len(pb.model_usage) == 1
|
|
assert pb.model_usage[0].HasField("tts")
|
|
assert pb.model_usage[0].tts.provider == "elevenlabs"
|
|
assert pb.model_usage[0].tts.characters_count == 500
|
|
|
|
def test_stt_usage(self) -> None:
|
|
usage = AgentSessionUsage(
|
|
model_usage=[
|
|
STTModelUsage(
|
|
provider="deepgram",
|
|
model="nova-2",
|
|
audio_duration=10.0,
|
|
)
|
|
]
|
|
)
|
|
pb = _session_usage_to_proto(usage)
|
|
assert len(pb.model_usage) == 1
|
|
assert pb.model_usage[0].HasField("stt")
|
|
assert pb.model_usage[0].stt.audio_duration == pytest.approx(10.0)
|
|
|
|
def test_interruption_usage(self) -> None:
|
|
usage = AgentSessionUsage(
|
|
model_usage=[
|
|
InterruptionModelUsage(
|
|
provider="livekit",
|
|
model="turn-detector",
|
|
total_requests=42,
|
|
)
|
|
]
|
|
)
|
|
pb = _session_usage_to_proto(usage)
|
|
assert len(pb.model_usage) == 1
|
|
assert pb.model_usage[0].HasField("interruption")
|
|
assert pb.model_usage[0].interruption.total_requests == 42
|
|
|
|
def test_mixed_usage(self) -> None:
|
|
usage = AgentSessionUsage(
|
|
model_usage=[
|
|
LLMModelUsage(provider="openai", model="gpt-4"),
|
|
TTSModelUsage(provider="elevenlabs", model="v2"),
|
|
STTModelUsage(provider="deepgram", model="nova"),
|
|
]
|
|
)
|
|
pb = _session_usage_to_proto(usage)
|
|
assert len(pb.model_usage) == 3
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# RoomSessionTransport
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestRoomSessionTransport:
|
|
def test_remote_identity_property(self) -> None:
|
|
room = MagicMock()
|
|
transport = RoomSessionTransport(room, remote_identity="user-1")
|
|
assert transport.remote_identity == "user-1"
|
|
|
|
transport.remote_identity = "user-2"
|
|
assert transport.remote_identity == "user-2"
|
|
|
|
def test_remote_identity_none(self) -> None:
|
|
room = MagicMock()
|
|
transport = RoomSessionTransport(room)
|
|
assert transport.remote_identity is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SessionHost event forwarding
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSessionHostEvents:
|
|
@pytest.fixture
|
|
def transport(self) -> InMemoryTransport:
|
|
return InMemoryTransport()
|
|
|
|
@pytest.fixture
|
|
def mock_session(self) -> MagicMock:
|
|
session = MagicMock()
|
|
session.on = MagicMock()
|
|
session.off = MagicMock()
|
|
return session
|
|
|
|
def test_register_session(self, transport: InMemoryTransport, mock_session: MagicMock) -> None:
|
|
host = SessionHost(transport)
|
|
host.register_session(mock_session)
|
|
assert mock_session.on.call_count == 10
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agent_state_changed(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
await host.start()
|
|
|
|
event = AgentStateChangedEvent(
|
|
type="agent_state_changed",
|
|
old_state="idle",
|
|
new_state="listening",
|
|
created_at=1000.0,
|
|
)
|
|
host._on_agent_state_changed(event)
|
|
await asyncio.sleep(0.1)
|
|
|
|
assert len(transport.sent) == 1
|
|
msg = transport.sent[0]
|
|
assert msg.HasField("event")
|
|
assert msg.event.HasField("agent_state_changed")
|
|
assert msg.event.agent_state_changed.old_state == agent_pb.AS_IDLE
|
|
assert msg.event.agent_state_changed.new_state == agent_pb.AS_LISTENING
|
|
assert msg.event.HasField("created_at")
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_state_changed(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
await host.start()
|
|
|
|
event = UserStateChangedEvent(
|
|
type="user_state_changed",
|
|
old_state="listening",
|
|
new_state="speaking",
|
|
created_at=1000.0,
|
|
)
|
|
host._on_user_state_changed(event)
|
|
await asyncio.sleep(0.1)
|
|
|
|
assert len(transport.sent) == 1
|
|
msg = transport.sent[0]
|
|
assert msg.event.user_state_changed.old_state == agent_pb.US_LISTENING
|
|
assert msg.event.user_state_changed.new_state == agent_pb.US_SPEAKING
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tool_execution_updated(self, transport: InMemoryTransport) -> None:
|
|
from livekit.agents.llm import FunctionCall
|
|
from livekit.agents.voice.events import (
|
|
ToolCallEnded,
|
|
ToolCallStarted,
|
|
ToolCallUpdated,
|
|
ToolExecutionUpdatedEvent,
|
|
ToolReplyUpdated,
|
|
)
|
|
|
|
host = SessionHost(transport)
|
|
await host.start()
|
|
|
|
host._on_tool_execution_updated(
|
|
ToolExecutionUpdatedEvent(
|
|
update=ToolCallStarted(
|
|
function_call=FunctionCall(call_id="c1", name="my_tool", arguments="{}")
|
|
)
|
|
)
|
|
)
|
|
host._on_tool_execution_updated(
|
|
ToolExecutionUpdatedEvent(
|
|
update=ToolCallUpdated(id="c1_update_1", call_id="c1", message="working")
|
|
)
|
|
)
|
|
host._on_tool_execution_updated(
|
|
ToolExecutionUpdatedEvent(
|
|
update=ToolCallEnded(id="c1_final", call_id="c1", message="result", status="done")
|
|
)
|
|
)
|
|
host._on_tool_execution_updated(
|
|
ToolExecutionUpdatedEvent(
|
|
update=ToolReplyUpdated(
|
|
update_ids=["c1_update_1", "c1_final"],
|
|
status="completed",
|
|
speech_id="speech_1",
|
|
)
|
|
)
|
|
)
|
|
await asyncio.sleep(0.1)
|
|
|
|
assert len(transport.sent) == 4
|
|
started = transport.sent[0].event.tool_execution_updated
|
|
assert started.WhichOneof("update") == "started"
|
|
assert started.started.function_call.call_id == "c1"
|
|
assert started.started.function_call.name == "my_tool"
|
|
|
|
call_updated = transport.sent[1].event.tool_execution_updated
|
|
assert call_updated.WhichOneof("update") == "call_updated"
|
|
assert call_updated.call_updated.id == "c1_update_1"
|
|
assert call_updated.call_updated.call_id == "c1"
|
|
assert call_updated.call_updated.message == "working"
|
|
|
|
ended = transport.sent[2].event.tool_execution_updated
|
|
assert ended.WhichOneof("update") == "ended"
|
|
assert ended.ended.id == "c1_final"
|
|
assert ended.ended.call_id == "c1"
|
|
assert ended.ended.message == "result"
|
|
assert ended.ended.status == agent_pb.TC_DONE
|
|
|
|
reply_updated = transport.sent[3].event.tool_execution_updated
|
|
assert reply_updated.WhichOneof("update") == "reply_updated"
|
|
assert list(reply_updated.reply_updated.update_ids) == ["c1_update_1", "c1_final"]
|
|
assert reply_updated.reply_updated.status == agent_pb.TR_COMPLETED
|
|
assert reply_updated.reply_updated.speech_id == "speech_1"
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_input_transcribed(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
await host.start()
|
|
|
|
event = UserInputTranscribedEvent(
|
|
type="user_input_transcribed",
|
|
transcript="hello world",
|
|
is_final=True,
|
|
created_at=1000.0,
|
|
)
|
|
host._on_user_input_transcribed(event)
|
|
await asyncio.sleep(0.1)
|
|
|
|
assert len(transport.sent) == 1
|
|
msg = transport.sent[0]
|
|
assert msg.event.user_input_transcribed.transcript == "hello world"
|
|
assert msg.event.user_input_transcribed.is_final is True
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_conversation_item_added(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
await host.start()
|
|
|
|
chat_msg = ChatMessage(role="user", content=["hello"], id="msg-1")
|
|
event = ConversationItemAddedEvent(
|
|
type="conversation_item_added",
|
|
item=chat_msg,
|
|
created_at=1000.0,
|
|
)
|
|
host._on_conversation_item_added(event)
|
|
await asyncio.sleep(0.1)
|
|
|
|
assert len(transport.sent) == 1
|
|
msg = transport.sent[0]
|
|
assert msg.event.conversation_item_added.item.message.id == "msg-1"
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_error_event(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
await host.start()
|
|
|
|
event = ErrorEvent(
|
|
type="error",
|
|
error=RuntimeError("something went wrong"),
|
|
source="llm",
|
|
created_at=1000.0,
|
|
)
|
|
host._on_error(event)
|
|
await asyncio.sleep(0.1)
|
|
|
|
assert len(transport.sent) == 1
|
|
msg = transport.sent[0]
|
|
assert msg.event.error.message == "something went wrong"
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_session_usage_updated(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
await host.start()
|
|
|
|
usage = AgentSessionUsage(
|
|
model_usage=[
|
|
LLMModelUsage(provider="openai", model="gpt-4", input_tokens=100, output_tokens=50)
|
|
]
|
|
)
|
|
event = SessionUsageUpdatedEvent(
|
|
type="session_usage_updated",
|
|
usage=usage,
|
|
created_at=1000.0,
|
|
)
|
|
host._on_session_usage_updated(event)
|
|
await asyncio.sleep(0.1)
|
|
|
|
assert len(transport.sent) == 1
|
|
msg = transport.sent[0]
|
|
assert msg.event.session_usage_updated.usage.model_usage[0].llm.provider == "openai"
|
|
|
|
await host.aclose()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SessionHost request handling
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSessionHostRequests:
|
|
@pytest.fixture
|
|
def transport(self) -> InMemoryTransport:
|
|
return InMemoryTransport()
|
|
|
|
def _make_mock_session(self) -> MagicMock:
|
|
session = MagicMock()
|
|
session.on = MagicMock()
|
|
session.off = MagicMock()
|
|
|
|
history = MagicMock()
|
|
history.items = [ChatMessage(role="user", content=["hi"], id="h-1")]
|
|
session.history = history
|
|
|
|
agent = MagicMock()
|
|
agent.id = "agent-1"
|
|
agent.instructions = "Be helpful"
|
|
agent.tools = []
|
|
agent.chat_ctx = ChatContext()
|
|
session.current_agent = agent
|
|
|
|
session.agent_state = "idle"
|
|
session.user_state = "listening"
|
|
session._started_at = 1000.0
|
|
|
|
options = MagicMock()
|
|
options.endpointing = MagicMock(__iter__=lambda s: iter([]))
|
|
options.interruption = MagicMock(__iter__=lambda s: iter([]))
|
|
options.max_tool_steps = 5
|
|
options.user_away_timeout = 30
|
|
options.preemptive_generation = {"enabled": False}
|
|
options.min_consecutive_speech_delay = 0.5
|
|
options.use_tts_aligned_transcript = True
|
|
options.ivr_detection = False
|
|
session.options = options
|
|
|
|
usage = AgentSessionUsage(model_usage=[])
|
|
session.usage = usage
|
|
|
|
return session
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_ping_pong(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
session = self._make_mock_session()
|
|
host.register_session(session)
|
|
await host.start()
|
|
|
|
req = agent_pb.SessionRequest(
|
|
request_id="req-1",
|
|
ping=agent_pb.SessionRequest.Ping(),
|
|
)
|
|
await host._handle_request(req)
|
|
|
|
assert len(transport.sent) == 1
|
|
resp = transport.sent[0].response
|
|
assert resp.request_id == "req-1"
|
|
assert resp.HasField("pong")
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_chat_history(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
session = self._make_mock_session()
|
|
host.register_session(session)
|
|
await host.start()
|
|
|
|
req = agent_pb.SessionRequest(
|
|
request_id="req-2",
|
|
get_chat_history=agent_pb.SessionRequest.GetChatHistory(),
|
|
)
|
|
await host._handle_request(req)
|
|
|
|
assert len(transport.sent) == 1
|
|
resp = transport.sent[0].response
|
|
assert resp.request_id == "req-2"
|
|
assert resp.HasField("get_chat_history")
|
|
assert len(resp.get_chat_history.items) == 1
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_agent_info(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
session = self._make_mock_session()
|
|
host.register_session(session)
|
|
await host.start()
|
|
|
|
req = agent_pb.SessionRequest(
|
|
request_id="req-3",
|
|
get_agent_info=agent_pb.SessionRequest.GetAgentInfo(),
|
|
)
|
|
await host._handle_request(req)
|
|
|
|
assert len(transport.sent) == 1
|
|
resp = transport.sent[0].response
|
|
assert resp.request_id == "req-3"
|
|
assert resp.HasField("get_agent_info")
|
|
assert resp.get_agent_info.id == "agent-1"
|
|
assert resp.get_agent_info.instructions == "Be helpful"
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_session_state(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
session = self._make_mock_session()
|
|
host.register_session(session)
|
|
await host.start()
|
|
|
|
req = agent_pb.SessionRequest(
|
|
request_id="req-4",
|
|
get_session_state=agent_pb.SessionRequest.GetSessionState(),
|
|
)
|
|
await host._handle_request(req)
|
|
|
|
assert len(transport.sent) == 1
|
|
resp = transport.sent[0].response
|
|
assert resp.request_id == "req-4"
|
|
assert resp.HasField("get_session_state")
|
|
assert resp.get_session_state.agent_id == "agent-1"
|
|
assert resp.get_session_state.agent_state == agent_pb.AS_IDLE
|
|
assert resp.get_session_state.user_state == agent_pb.US_LISTENING
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_session_usage(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
session = self._make_mock_session()
|
|
host.register_session(session)
|
|
await host.start()
|
|
|
|
req = agent_pb.SessionRequest(
|
|
request_id="req-5",
|
|
get_session_usage=agent_pb.SessionRequest.GetSessionUsage(),
|
|
)
|
|
await host._handle_request(req)
|
|
|
|
assert len(transport.sent) == 1
|
|
resp = transport.sent[0].response
|
|
assert resp.request_id == "req-5"
|
|
assert resp.HasField("get_session_usage")
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_request_error(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
session = self._make_mock_session()
|
|
session.history = property(lambda s: (_ for _ in ()).throw(RuntimeError("broken")))
|
|
host.register_session(session)
|
|
await host.start()
|
|
|
|
req = agent_pb.SessionRequest(
|
|
request_id="req-err",
|
|
get_chat_history=agent_pb.SessionRequest.GetChatHistory(),
|
|
)
|
|
await host._handle_request_safe(req)
|
|
|
|
assert len(transport.sent) == 1
|
|
resp = transport.sent[0].response
|
|
assert resp.request_id == "req-err"
|
|
assert resp.error == "internal error"
|
|
|
|
await host.aclose()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_aclose_unregisters_events(self, transport: InMemoryTransport) -> None:
|
|
host = SessionHost(transport)
|
|
session = self._make_mock_session()
|
|
host.register_session(session)
|
|
await host.start()
|
|
await host.aclose()
|
|
assert session.off.call_count == 10
|