"""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