Files
vllm-omni/tests/entrypoints/openai/test_duplex_session_attachment.py
T
chickeytonandClaude Opus 5 99ff4f3070 [Core][Frontend] Unified Full-duplex Framework (#7413)
Signed-off-by: chickeyton <ngton2014@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 15:35:36 +08:00

555 lines
19 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import asyncio
import base64
import json
import pytest
from vllm_omni.entrypoints.duplex.session_attachment import (
DuplexEventJournal,
DuplexJournalGapError,
DuplexJournalOverflowError,
DuplexResumeCredential,
DuplexSessionAttachmentRegistry,
InvalidResumeTokenError,
ResumeToken,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _Clock:
def __init__(self) -> None:
self.value = 0.0
def __call__(self) -> float:
return self.value
def advance(self, seconds: float) -> None:
self.value += seconds
def _decoded_token_bytes(token: ResumeToken) -> bytes:
padding = "=" * (-len(token.plaintext) % 4)
return base64.urlsafe_b64decode(token.plaintext + padding)
def test_resume_credential_uses_256_bit_token_digest_and_redacted_repr(monkeypatch) -> None:
token = ResumeToken.generate()
credential = DuplexResumeCredential.from_token(token)
calls = []
import vllm_omni.entrypoints.duplex.session_attachment as attachment_module
original_compare = attachment_module.hmac.compare_digest
def recording_compare(left, right):
calls.append((left, right))
return original_compare(left, right)
monkeypatch.setattr(attachment_module.hmac, "compare_digest", recording_compare)
assert len(_decoded_token_bytes(token)) == 32
assert credential.verify(token.plaintext) is True
assert credential.verify("invalid-token") is False
assert len(calls) == 2
assert token.plaintext not in repr(token)
assert token.plaintext not in repr(credential)
assert not hasattr(credential, "plaintext")
assert len(credential.token_digest) == 32
def test_resume_credential_rotation_revokes_old_token() -> None:
first = ResumeToken.generate()
credential = DuplexResumeCredential.from_token(first)
second = credential.rotate()
assert credential.verify(first.plaintext) is False
assert credential.verify(second.plaintext) is True
assert second.plaintext != first.plaintext
def test_event_journal_sequences_acknowledges_and_replays_exact_payloads() -> None:
clock = _Clock()
journal = DuplexEventJournal(max_bytes=4096, ttl_s=60.0, clock=clock)
first = journal.record({"type": "response.output_audio.delta", "delta": "AAAA"})
second = journal.record({"type": "response.done", "response_id": "resp-1"})
assert first.sequence == 1
assert second.sequence == 2
assert first.payload["server_event_seq"] == 1
assert second.payload["server_event_seq"] == 2
assert first.encoded_bytes == len(
json.dumps(dict(first.payload), separators=(",", ":"), ensure_ascii=False).encode("utf-8")
)
assert [entry.sequence for entry in journal.replay_after(0)] == [1, 2]
assert journal.acknowledge(1) == 1
assert [entry.sequence for entry in journal.replay_after(1)] == [2]
assert journal.acknowledge(1) == 0
def test_event_journal_ttl_pruning_reports_replay_gap() -> None:
clock = _Clock()
journal = DuplexEventJournal(max_bytes=4096, ttl_s=5.0, clock=clock)
journal.record({"type": "one"})
clock.advance(3.0)
journal.record({"type": "two"})
clock.advance(3.0)
assert journal.prune() == 1
with pytest.raises(DuplexJournalGapError, match="older than retained"):
journal.replay_after(0)
assert [entry.payload["type"] for entry in journal.replay_after(1)] == ["two"]
def test_event_journal_overflow_is_explicit_and_does_not_evict_silently() -> None:
clock = _Clock()
journal = DuplexEventJournal(max_bytes=100, ttl_s=60.0, clock=clock)
first = journal.record({"type": "small", "value": "x"})
with pytest.raises(DuplexJournalOverflowError, match="byte limit"):
journal.record({"type": "large", "value": "x" * 200})
assert journal.overflowed is True
assert journal.retained_bytes == first.encoded_bytes
with pytest.raises(DuplexJournalGapError, match="overflowed"):
journal.replay_after(0)
@pytest.mark.asyncio
@pytest.mark.parametrize("journal", [False, True])
async def test_registry_failed_send_is_accepted_only_when_journaled(mocker, journal: bool) -> None:
registry = DuplexSessionAttachmentRegistry(replay_ttl_s=60.0, replay_max_bytes_per_session=4096)
accepted = mocker.Mock()
expected_calls = [mocker.call()] if journal else []
async def failing_send(payload):
assert accepted.call_args_list == expected_calls
raise RuntimeError("transport lost")
await registry.create("sid-send-failure", send=failing_send, close=mocker.AsyncMock())
with pytest.raises(RuntimeError, match="transport lost"):
await registry.send_event(
"sid-send-failure",
{"type": "response.audio.delta", "delta": "AAAA"},
journal=journal,
on_accepted=accepted,
)
assert accepted.call_args_list == expected_calls
@pytest.mark.asyncio
@pytest.mark.parametrize("journal", [False, True])
async def test_registry_detached_event_is_accepted_only_when_journaled(mocker, journal: bool) -> None:
registry = DuplexSessionAttachmentRegistry(replay_ttl_s=60.0, replay_max_bytes_per_session=4096)
send = mocker.AsyncMock()
accepted = mocker.Mock()
created = await registry.create("sid-detached-acceptance", send=send, close=mocker.AsyncMock())
await registry.detach("sid-detached-acceptance", attachment_generation=created.attachment_generation)
entry = await registry.send_event(
"sid-detached-acceptance",
{"type": "response.audio.delta", "delta": "AAAA"},
journal=journal,
on_accepted=accepted,
)
send.assert_not_awaited()
assert (entry is not None) is journal
assert accepted.call_count == int(journal)
@pytest.mark.asyncio
async def test_registry_unjournaled_event_is_accepted_after_successful_send(mocker) -> None:
registry = DuplexSessionAttachmentRegistry(replay_ttl_s=60.0, replay_max_bytes_per_session=4096)
accepted = mocker.Mock()
async def send(payload):
accepted.assert_not_called()
await registry.create("sid-live-acceptance", send=send, close=mocker.AsyncMock())
entry = await registry.send_event(
"sid-live-acceptance",
{"type": "response.audio.delta", "delta": "AAAA"},
journal=False,
on_accepted=accepted,
)
assert entry is None
accepted.assert_called_once_with()
@pytest.mark.asyncio
async def test_registry_overflow_does_not_accept_or_send_event(mocker) -> None:
registry = DuplexSessionAttachmentRegistry(replay_ttl_s=60.0, replay_max_bytes_per_session=100)
accepted = mocker.Mock()
send = mocker.AsyncMock()
await registry.create("sid-overflow-acceptance", send=send, close=mocker.AsyncMock())
with pytest.raises(DuplexJournalOverflowError, match="byte limit"):
await registry.send_event(
"sid-overflow-acceptance",
{"type": "response.audio.delta", "delta": "A" * 200},
on_accepted=accepted,
)
accepted.assert_not_called()
send.assert_not_awaited()
@pytest.mark.asyncio
async def test_registry_resume_rotates_token_replays_and_atomically_replaces_attachment() -> None:
clock = _Clock()
registry = DuplexSessionAttachmentRegistry(
replay_ttl_s=60.0,
replay_max_bytes_per_session=4096,
clock=clock,
)
sends_a = []
closes_a = []
sends_b = []
closes_b = []
async def send_a(payload):
sends_a.append(payload)
async def close_a(reason):
closes_a.append(reason)
async def send_b(payload):
sends_b.append(payload)
async def close_b(reason):
closes_b.append(reason)
created = await registry.create("sid", send=send_a, close=close_a)
await registry.send_event("sid", {"type": "event-1"})
await registry.send_event("sid", {"type": "event-2"})
resumed = await registry.resume(
"sid",
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=1,
send=send_b,
close=close_b,
)
assert resumed.attachment_generation == 2
assert resumed.resume_token.plaintext != created.resume_token.plaintext
assert [entry.sequence for entry in resumed.replay_entries] == [2]
assert resumed.replaced_attachment is not None
assert resumed.replaced_attachment.generation == 1
assert await registry.is_current_attachment("sid", created.attachment_generation) is False
assert await registry.is_current_attachment("sid", resumed.attachment_generation) is True
assert await registry.detach("sid", attachment_generation=1) is False
assert await registry.detach("sid", attachment_generation=2) is True
assert [(payload["type"], payload["server_event_seq"]) for payload in sends_a] == [
("event-1", 1),
("event-2", 2),
]
assert closes_a == [] and sends_b == [] and closes_b == []
@pytest.mark.asyncio
async def test_registry_detach_without_a_generation_targets_the_current_attachment_once() -> None:
registry = DuplexSessionAttachmentRegistry(replay_ttl_s=60.0, replay_max_bytes_per_session=4096)
sends: list[dict] = []
async def send(payload):
sends.append(payload)
async def close(reason):
return None
await registry.create("sid-current", send=send, close=close)
# The outbound pump outlives any one connection and only knows the session:
# ``None`` means "whichever socket is attached right now".
assert await registry.detach("sid-current", attachment_generation=None) is True
# A second report of the same dead socket is not a new detach, so it cannot
# restart the engine's disconnect grace window.
assert await registry.detach("sid-current", attachment_generation=None) is False
assert await registry.detach("sid-current", attachment_generation=1) is False
assert await registry.is_current_attachment("sid-current", 1) is False
# A detached session still journals for a later resume.
await registry.send_event("sid-current", {"type": "event-1"})
assert sends == []
assert await registry.detach("missing-session", attachment_generation=None) is False
@pytest.mark.asyncio
async def test_registry_resume_sends_activation_then_replay_before_new_live_events() -> None:
registry = DuplexSessionAttachmentRegistry(
replay_ttl_s=60.0,
replay_max_bytes_per_session=4096,
)
wire = []
activation_started = asyncio.Event()
release_activation = asyncio.Event()
async def old_send(payload):
del payload
async def close(reason):
del reason
async def new_send(payload):
wire.append(payload["type"])
if payload["type"] == "session.resumed":
activation_started.set()
await release_activation.wait()
created = await registry.create("sid-order", send=old_send, close=close)
await registry.detach("sid-order", attachment_generation=1)
await registry.send_event("sid-order", {"type": "replayed"})
resume_task = asyncio.create_task(
registry.resume(
"sid-order",
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=new_send,
close=close,
activation_payload_factory=lambda _token, _generation: {"type": "session.resumed"},
)
)
await activation_started.wait()
live_task = asyncio.create_task(registry.send_event("sid-order", {"type": "live"}))
release_activation.set()
await asyncio.gather(resume_task, live_task)
assert wire == ["session.resumed", "replayed", "live"]
@pytest.mark.asyncio
async def test_registry_resume_delivery_failure_keeps_one_shot_old_token_recovery() -> None:
registry = DuplexSessionAttachmentRegistry(
replay_ttl_s=60.0,
replay_max_bytes_per_session=4096,
)
async def send(payload):
del payload
async def failing_send(payload):
del payload
raise RuntimeError("transport lost before rotated token arrived")
async def close(reason):
del reason
created = await registry.create("sid-recovery", send=send, close=close)
await registry.detach("sid-recovery", attachment_generation=1)
with pytest.raises(RuntimeError, match="transport lost"):
await registry.resume(
"sid-recovery",
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=failing_send,
close=close,
activation_payload_factory=lambda token, generation: {
"type": "session.resumed",
"resume_token": token.plaintext,
"attachment_generation": generation,
},
)
recovered = await registry.resume(
"sid-recovery",
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
close=close,
)
assert recovered.attachment_generation == 3
with pytest.raises(InvalidResumeTokenError):
await registry.resume(
"sid-recovery",
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
close=close,
)
@pytest.mark.asyncio
async def test_registry_concurrent_resume_allows_exactly_one_rotated_token_winner() -> None:
registry = DuplexSessionAttachmentRegistry(
replay_ttl_s=60.0,
replay_max_bytes_per_session=4096,
)
async def send(payload):
del payload
async def close(reason):
del reason
created = await registry.create("sid-race", send=send, close=close)
async def attempt():
try:
return await registry.resume(
"sid-race",
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
close=close,
)
except InvalidResumeTokenError as exc:
return exc
results = await asyncio.gather(attempt(), attempt())
assert sum(not isinstance(result, Exception) for result in results) == 1
assert sum(isinstance(result, InvalidResumeTokenError) for result in results) == 1
@pytest.mark.asyncio
async def test_registry_keeps_sessions_journals_and_tokens_isolated() -> None:
registry = DuplexSessionAttachmentRegistry(
replay_ttl_s=60.0,
replay_max_bytes_per_session=4096,
)
async def send(payload):
del payload
async def close(reason):
del reason
created_a = await registry.create("sid-a", send=send, close=close)
created_b = await registry.create("sid-b", send=send, close=close)
await registry.detach("sid-a", attachment_generation=1)
await registry.detach("sid-b", attachment_generation=1)
await registry.send_event("sid-a", {"type": "a-only"})
await registry.send_event("sid-b", {"type": "b-only"})
resumed_a = await registry.resume(
"sid-a",
resume_token=created_a.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
close=close,
)
resumed_b = await registry.resume(
"sid-b",
resume_token=created_b.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
close=close,
)
assert [entry.payload["type"] for entry in resumed_a.replay_entries] == ["a-only"]
assert [entry.payload["type"] for entry in resumed_b.replay_entries] == ["b-only"]
with pytest.raises(InvalidResumeTokenError):
await registry.resume(
"sid-a",
resume_token=resumed_b.resume_token.plaintext,
last_received_server_event_seq=1,
send=send,
close=close,
)
@pytest.mark.asyncio
async def test_registry_repr_never_contains_plaintext_tokens() -> None:
registry = DuplexSessionAttachmentRegistry(
replay_ttl_s=60.0,
replay_max_bytes_per_session=4096,
)
async def send(payload):
del payload
async def close(reason):
del reason
created = await registry.create("sid-repr", send=send, close=close)
await registry.detach("sid-repr", attachment_generation=1)
entry = await registry.send_event(
"sid-repr",
{"type": "session.resumed", "resume_token": created.resume_token.plaintext},
)
assert entry is not None
assert created.resume_token.plaintext not in repr(created)
assert created.resume_token.plaintext not in repr(entry)
assert created.resume_token.plaintext not in repr(registry)
@pytest.mark.asyncio
@pytest.mark.parametrize("cancel_on", ["activation", "replay"])
async def test_registry_resume_cancelled_mid_delivery_rolls_back_like_a_failure(cancel_on: str) -> None:
"""Cancellation must take the same rollback as an ordinary delivery failure.
``CancelledError`` derives from ``BaseException``, so an ``except
Exception`` around the activation and replay sends let a cancelled resume
keep its attachment as the current generation while the token it rotated
past was already gone — the session left attached to an unusable transport
with no way back. Both send points are covered because either can be the
one that gets cancelled.
"""
registry = DuplexSessionAttachmentRegistry(replay_ttl_s=60.0, replay_max_bytes_per_session=4096)
async def send(payload):
del payload
async def close(reason):
del reason
created = await registry.create("sid-cancel", send=send, close=close)
# One journaled event, so the replay loop has something to send.
await registry.send_event("sid-cancel", {"type": "response.output_text.delta", "delta": "a"})
await registry.detach("sid-cancel", attachment_generation=1)
sends = 0
cancel_at = 1 if cancel_on == "activation" else 2
async def cancelling_send(payload):
del payload
nonlocal sends
sends += 1
if sends == cancel_at:
raise asyncio.CancelledError
with pytest.raises(asyncio.CancelledError):
await registry.resume(
"sid-cancel",
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=cancelling_send,
close=close,
activation_payload_factory=lambda token, generation: {
"type": "session.resumed",
"resume_token": token.plaintext,
"attachment_generation": generation,
},
)
assert sends == cancel_at, "the send that was cancelled is the one under test"
# The cancelled attachment must not remain current...
assert not await registry.has_attachment("sid-cancel")
# ...and the token the caller presented has to work again, or the session
# is unreachable: resume() had already rotated past it.
recovered = await registry.resume(
"sid-cancel",
resume_token=created.resume_token.plaintext,
last_received_server_event_seq=0,
send=send,
close=close,
)
assert recovered.attachment_generation == 3