first git
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
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
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user