Files
vllm-omni/tests/engine/duplex/test_delivery.py
T
2026-10-04 14:06:04 -07:00

124 lines
5.1 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import asyncio
import json
from dataclasses import replace
import pytest
from vllm_omni.engine.duplex.delivery import DuplexOutputBuffer, DuplexOutputOverflowError
from vllm_omni.engine.duplex.events import AudioDelta, ResponseDone, SessionClosed, TranscriptDelta
from vllm_omni.engine.duplex.realtime_events import RealtimeProjectionState, project_internal_event
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@pytest.mark.asyncio
@pytest.mark.parametrize(
("event_type", "delta_field", "content_field"),
[
("response.text.delta", "delta", "text"),
("response.output_audio.delta", "text", "transcript"),
],
)
async def test_queued_response_creation_keeps_payload_and_byte_count(event_type, delta_field, content_field):
state = RealtimeProjectionState(session_id="s")
events = project_internal_event(state, {"type": "response.created", "response_id": "r", "modalities": ["text"]})
payloads = [event.to_realtime() for event in events]
byte_count = sum(
len(json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode()) for payload in payloads
)
output = DuplexOutputBuffer(max_bytes=byte_count, max_events=len(events))
for event in events:
output.put(event)
assert output.pending_bytes == byte_count
text = "Growing response. " * 512
project_internal_event(state, {"type": event_type, "response_id": "r", delta_field: text})
item = next(iter(state.conversation_items.values()))
assert item["content"][0][content_field] == text
assert output.pending_bytes == byte_count
delivered = [await output.get() for _ in events]
assert [event.to_realtime() for event in delivered] == payloads
assert output.pending_events == output.pending_bytes == 0
@pytest.mark.asyncio
async def test_cancel_removes_only_matching_audio_and_invalidates_held_audio():
output = DuplexOutputBuffer(max_bytes=4096, max_events=8)
held = AudioDelta(response_id="old", epoch=0, delta="AAAA")
text = TranscriptDelta(response_id="old", epoch=0, delta="kept")
other = replace(held, response_id="other")
newer = replace(held, epoch=1)
done = ResponseDone(response_id="old", response={"status": "cancelled"})
for event in (held, replace(held), text, other, newer, done):
output.put(event)
assert await output.get() is held
assert output.is_valid(held)
output.invalidate("old", through_epoch=0)
assert output.pending_events == 4
with output.guard(held) as valid:
assert not valid
assert [await output.get() for _ in range(4)] == [text, other, newer, done]
assert output.pending_events == output.pending_bytes == 0
@pytest.mark.asyncio
async def test_full_reserve_cannot_block_closure_or_reorder_valid_audio():
output = DuplexOutputBuffer(max_bytes=4096, max_events=1, reserve_events=1)
audio = AudioDelta(response_id="r", epoch=0, delta="AAAA")
done = ResponseDone(response_id="r", response={"status": "completed"})
output.put(audio)
output.put(done)
with pytest.raises(DuplexOutputOverflowError):
output.put(done)
closed = SessionClosed(session_id="s", reason="client_close")
output.close(closed)
output.close(SessionClosed(reason="duplicate"))
assert not output.put(audio)
assert [await output.get() for _ in range(4)] == [audio, done, closed, None]
assert output.pending_events == output.pending_bytes == 0
@pytest.mark.asyncio
async def test_oversized_close_still_ends_stream_without_reopening_it():
output = DuplexOutputBuffer(max_bytes=128, max_events=1)
closed = SessionClosed(session_id="s", details={"large": "x" * 5000})
output.close(closed)
terminal = await output.get()
assert isinstance(terminal, SessionClosed)
assert terminal.event_id == closed.event_id
assert terminal.session_id == "s"
assert terminal.reason == "close_details_exceed_output_limit"
assert terminal.details == {}
assert await output.get() is None
output.close(closed)
assert await output.get() is None
def test_byte_limit_rejects_audio_without_changing_pending_budget():
output = DuplexOutputBuffer(max_bytes=128, max_events=8)
with pytest.raises(DuplexOutputOverflowError):
output.put(AudioDelta(delta="AAAA" * 64))
assert output.pending_events == output.pending_bytes == 0
@pytest.mark.asyncio
async def test_cancelled_waiter_can_be_replaced_and_woken_from_another_thread():
output = DuplexOutputBuffer(max_bytes=4096, max_events=8)
abandoned = asyncio.create_task(output.get())
await asyncio.sleep(0)
abandoned.cancel()
with pytest.raises(asyncio.CancelledError):
await abandoned
waiting = asyncio.create_task(output.get())
await asyncio.sleep(0)
event = AudioDelta(response_id="r", epoch=0, delta="AAAA")
await asyncio.to_thread(output.put, event)
assert await asyncio.wait_for(waiting, timeout=2) is event
closing = asyncio.create_task(output.get())
await asyncio.sleep(0)
await asyncio.to_thread(output.close)
assert await asyncio.wait_for(closing, timeout=2) is None