vocero-s2s/tests/openai_realtime/test_runtime_config.py
valenti b5f82fb48c
Some checks are pending
CI / ruff (push) Waiting to run
CI / mypy (push) Waiting to run
CI / pytest (push) Waiting to run
CI / package (push) Waiting to run
CI / Install smoke (${{ matrix.label }}) (linux, ubuntu-latest) (push) Blocked by required conditions
CI / Install smoke (${{ matrix.label }}) (macos-arm64, macos-14) (push) Blocked by required conditions
first git
2026-08-26 11:30:14 +00:00

161 lines
6.0 KiB
Python

"""Unit tests for RuntimeConfig.apply_session_update / _apply_update.
Verifies the merge semantics: only explicitly-set fields (model_fields_set)
are written, nested BaseModels recurse without clobbering siblings, and
explicit None clears a previously-set value.
"""
from openai.types.realtime import RealtimeSessionCreateRequest
from openai.types.realtime.session_update_event import SessionUpdateEvent
from speech_to_speech.api.openai_realtime.runtime_config import RuntimeConfig
def _parse_session(**session_fields) -> RealtimeSessionCreateRequest:
"""Parse a session dict the same way the SDK does (tracks model_fields_set)."""
session_fields.setdefault("type", "realtime")
evt = SessionUpdateEvent.model_validate(
{
"type": "session.update",
"session": session_fields,
}
)
return evt.session # type: ignore[return-value]
class TestApplySessionUpdate:
def test_partial_update_preserves_untouched_fields(self):
cfg = RuntimeConfig()
cfg.apply_session_update(
_parse_session(
instructions="Be a pirate",
audio={"output": {"voice": "coral"}},
tool_choice="auto",
)
)
assert cfg.session.instructions == "Be a pirate"
assert cfg.session.audio.output.voice == "coral"
assert cfg.session.tool_choice == "auto"
cfg.apply_session_update(_parse_session(instructions="Be an astronaut"))
assert cfg.session.instructions == "Be an astronaut"
assert cfg.session.audio.output.voice == "coral"
assert cfg.session.tool_choice == "auto"
def test_explicit_none_clears_field(self):
"""Sending ``"turn_detection": null`` clears a previously-set value."""
cfg = RuntimeConfig()
cfg.apply_session_update(
_parse_session(
audio={"input": {"turn_detection": {"type": "server_vad"}}},
)
)
assert cfg.session.audio.input.turn_detection is not None
assert cfg.session.audio.input.turn_detection.type == "server_vad"
cfg.apply_session_update(
_parse_session(
audio={"input": {"turn_detection": None}},
)
)
assert cfg.session.audio.input.turn_detection is None
def test_nested_sibling_preserved(self):
"""Updating audio.output.voice must not touch audio.input.turn_detection."""
cfg = RuntimeConfig()
cfg.apply_session_update(
_parse_session(
audio={
"input": {"turn_detection": {"type": "server_vad", "threshold": 0.6}},
"output": {"voice": "echo"},
},
)
)
assert cfg.session.audio.output.voice == "echo"
assert cfg.session.audio.input.turn_detection.threshold == 0.6
cfg.apply_session_update(
_parse_session(
audio={"output": {"voice": "shimmer"}},
)
)
assert cfg.session.audio.output.voice == "shimmer"
assert cfg.session.audio.input.turn_detection.type == "server_vad"
assert cfg.session.audio.input.turn_detection.threshold == 0.6
def test_sequential_updates_accumulate(self):
cfg = RuntimeConfig()
cfg.apply_session_update(_parse_session(instructions="Step 1"))
cfg.apply_session_update(_parse_session(audio={"output": {"voice": "alloy"}}))
cfg.apply_session_update(_parse_session(tool_choice="required"))
assert cfg.session.instructions == "Step 1"
assert cfg.session.audio.output.voice == "alloy"
assert cfg.session.tool_choice == "required"
def test_deep_nested_leaf_update(self):
"""Changing only turn_detection.threshold preserves the rest."""
cfg = RuntimeConfig()
cfg.apply_session_update(
_parse_session(
audio={
"input": {
"turn_detection": {
"type": "server_vad",
"threshold": 0.5,
"silence_duration_ms": 800,
},
},
},
)
)
assert cfg.session.audio.input.turn_detection.threshold == 0.5
assert cfg.session.audio.input.turn_detection.silence_duration_ms == 800
cfg.apply_session_update(
_parse_session(
audio={"input": {"turn_detection": {"type": "server_vad", "threshold": 0.8}}},
)
)
assert cfg.session.audio.input.turn_detection.threshold == 0.8
assert cfg.session.audio.input.turn_detection.silence_duration_ms == 800
def test_tools_replaced_wholesale(self):
"""Tools is a list, not a BaseModel — the whole list is replaced."""
cfg = RuntimeConfig()
cfg.apply_session_update(
_parse_session(
tools=[{"type": "function", "name": "get_weather"}],
)
)
assert len(cfg.session.tools) == 1
cfg.apply_session_update(
_parse_session(
tools=[
{"type": "function", "name": "get_weather"},
{"type": "function", "name": "get_time"},
],
)
)
assert len(cfg.session.tools) == 2
def test_update_after_fresh_init(self):
"""apply_session_update works on a freshly-constructed RuntimeConfig."""
cfg = RuntimeConfig()
cfg.apply_session_update(
_parse_session(
instructions="Hello",
audio={"output": {"voice": "sage"}},
)
)
assert cfg.session.instructions == "Hello"
assert cfg.session.audio.output.voice == "sage"
def test_validator_ensures_audio_structure_on_init(self):
"""Default construction guarantees audio.input and audio.output are not None."""
cfg = RuntimeConfig()
assert cfg.session.audio is not None
assert cfg.session.audio.input is not None
assert cfg.session.audio.output is not None