Files
vllm-omni/tests/protocol/duplex/test_duplex_protocol_facade.py
T

144 lines
5.8 KiB
Python
Raw Normal View History

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""``protocol.duplex`` is the only door a duplex consumer uses.
The layering is a chain::
protocol.realtime <- protocol.duplex <- engine / entrypoints / clients
The middle link only earns its keep if the last arrow is the *only* one. If the
engine may also reach past it straight into ``protocol.realtime``, then giving a
Tier 1 helper a duplex-specific version later means touching every call site
instead of one file --- which is exactly the change
``convert_input_audio_with_rate`` is expected to need (it resamples to
MiniCPM-o's 16 kHz, not the client's rate).
So this asserts two things: nobody skips the middle link, and the middle link is
complete enough that nobody has to.
"""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
REPO_ROOT = Path(__file__).resolve().parents[3]
#: Every module that speaks the duplex protocol. These import
#: ``vllm_omni.protocol.duplex``; reaching into ``vllm_omni.protocol.realtime``
#: from here skips the extension point.
CONSUMER_PATHS = (
"vllm_omni/engine/duplex",
"vllm_omni/engine/duplex_omni_engine.py",
"vllm_omni/entrypoints/duplex",
"vllm_omni/entrypoints/duplex_omni.py",
"vllm_omni/clients/duplex.py",
"vllm_omni/clients/inline_duplex.py",
)
def _consumer_modules() -> list[Path]:
found: list[Path] = []
for entry in CONSUMER_PATHS:
path = REPO_ROOT / entry
found.extend(sorted(path.rglob("*.py")) if path.is_dir() else [path])
assert found, "consumer list is stale"
return found
@pytest.mark.parametrize("module", _consumer_modules(), ids=lambda p: p.name)
def test_a_duplex_consumer_does_not_skip_the_duplex_protocol_package(module: Path) -> None:
# Source-level, so a lazy import inside a function body is caught too.
tree = ast.parse(module.read_text())
offenders = []
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom) and (node.module or "").startswith("vllm_omni.protocol.realtime"):
offenders.append(f"line {node.lineno}: from {node.module} import ...")
elif isinstance(node, ast.Import):
offenders.extend(
f"line {node.lineno}: import {alias.name}"
for alias in node.names
if alias.name.startswith("vllm_omni.protocol.realtime")
)
assert not offenders, (
f"{module.relative_to(REPO_ROOT)} imports the Tier 1 package directly; "
f"import from vllm_omni.protocol.duplex instead:\n " + "\n ".join(offenders)
)
def test_the_duplex_package_carries_the_whole_command_vocabulary() -> None:
from vllm_omni.engine.duplex import commands as engine_commands
from vllm_omni.protocol.duplex import commands as duplex_wire
# Every command the engine handles is nameable through the one door.
engine_names = {
name
for name in engine_commands.__all__
if isinstance(getattr(engine_commands, name), type)
and issubclass(getattr(engine_commands, name), engine_commands.DuplexCommand)
and getattr(engine_commands, name) is not engine_commands.DuplexCommand
}
assert engine_names <= set(duplex_wire.__all__)
def test_the_duplex_package_carries_the_whole_event_vocabulary() -> None:
from vllm_omni.engine.duplex import events as engine_events
from vllm_omni.protocol.duplex import events as duplex_wire
engine_names = {
name
for name in engine_events.__all__
if isinstance(getattr(engine_events, name), type)
and issubclass(getattr(engine_events, name), engine_events.DuplexEvent)
}
assert engine_names <= set(duplex_wire.__all__)
@pytest.mark.parametrize(
"module_pair",
[("events", "events"), ("commands", "commands")],
ids=["events", "commands"],
)
def test_a_reexported_tier1_name_is_the_tier1_object(module_pair: tuple[str, str]) -> None:
"""Re-export, not re-declaration: the Tier 1 class is one object."""
import importlib
duplex_mod = importlib.import_module(f"vllm_omni.protocol.duplex.{module_pair[0]}")
realtime_mod = importlib.import_module(f"vllm_omni.protocol.realtime.{module_pair[1]}")
shared = [n for n in duplex_mod.__all__ if hasattr(realtime_mod, n)]
assert shared, "nothing re-exported --- the facade is not doing its job"
for name in shared:
duplex_obj = getattr(duplex_mod, name)
realtime_obj = getattr(realtime_mod, name)
if duplex_obj is realtime_obj:
continue
# The only permitted divergence is a Tier 2 class extending its twin.
assert isinstance(duplex_obj, type) and issubclass(duplex_obj, realtime_obj), (
f"{name} in protocol.duplex is neither the Tier 1 object nor a subclass of it"
)
def test_duplex_event_is_an_alias_but_duplex_command_is_not() -> None:
"""The asymmetry is deliberate, and a later reader should not 'fix' it.
An event has no engine-internal half --- what the session emits is what the
socket sends --- so ``DuplexEvent`` is simply ``RealtimeEvent``. A command
does: ``payload()`` renders the session runner's mailbox dictionary, and for
four of the seventeen the mailbox channel is not even the client event type.
"""
from vllm_omni.engine.duplex.commands import DuplexCommand
from vllm_omni.protocol.duplex.events import DuplexEvent
from vllm_omni.protocol.realtime.commands import RealtimeCommand
from vllm_omni.protocol.realtime.events import RealtimeEvent
assert DuplexEvent is RealtimeEvent
assert DuplexCommand is not RealtimeCommand
assert issubclass(DuplexCommand, RealtimeCommand)
assert hasattr(DuplexCommand, "payload") and not hasattr(RealtimeCommand, "payload")