from __future__ import annotations import unittest from app.livekit.call_config import ( normalize_call_config, resolve_fake_agent_overrides, resolve_agent_backend_name, resolve_stt_overrides, resolve_tts_overrides, resolve_vad_logging_overrides, resolve_vad_overrides, resolve_ws_overrides, ) from app.ws_gateway.call_config import build_call_config class CallConfigTests(unittest.TestCase): def test_build_and_normalize_call_config(self) -> None: payload = { "agentBackend": "remote_ws_fake", "stt": { "provider": "internal_http", "language": "pt-BR", "configOverride": "{\"processor\":{\"strategy\":\"faster_default\"}}", "minProbSingleWord": "0.15", "disableVosk": True, }, "tts": { "provider": "elevenlabs", "voiceId": "voice-123", "modelId": "model-456", }, "vad": { "minSpeechDuration": 0.04, "activationThreshold": 0.18, "deactivationThreshold": 0.10, "minSilenceDuration": 0.7, "prefixPaddingDuration": 0.75, "preBackendWaitNoticeFastOnVadPause": True, "deferredInterruptionMinAudioMs": 1350, "deferredInterruptionEnabled": False, }, "vadLogging": { "logDecisions": True, "logActivity": False, "activityMinProbability": 0.03, }, "ws": { "outputGain": 1.2, "audioInputBacklogShedEnabled": True, "audioInputBacklogShedThresholdMs": 700, "audioInputBacklogShedKeepMs": 250, "audioInputLatencyMetricsEnabled": True, "audioInputLatencyAlertMs": 900, "audioInputLatencyLogIntervalS": 10.5, "livekitAudioSourceQueueSizeMs": 400, "livekitAudioSourceClearOnShed": False, "audioInputBacklogEnergyShedEnabled": True, "audioInputBacklogEnergyShedMaxExcessMs": 600, "audioInputBacklogSilenceDbfs": -60, }, "agentFake": { "delayMs": 2500, "responses": ( "Esta e a primeira resposta simulada com tamanho intermediario;" "Esta e a resposta final simulada encerrando o atendimento" ), }, } built = build_call_config(payload) normalized = normalize_call_config(payload) self.assertEqual(built, normalized) self.assertEqual(resolve_agent_backend_name(payload, "remote_ws"), "remote_ws_fake") self.assertEqual(resolve_stt_overrides(payload)["language"], "pt-BR") self.assertEqual(resolve_stt_overrides(payload)["disable_vosk"], "True") self.assertEqual( resolve_stt_overrides(payload)["config_override"], "{\"processor\":{\"strategy\":\"faster_default\"}}", ) self.assertEqual(resolve_tts_overrides(payload)["voice_id"], "voice-123") self.assertEqual(resolve_vad_overrides(payload)["min_speech_duration"], "0.04") self.assertEqual(resolve_vad_overrides(payload)["activation_threshold"], "0.18") self.assertEqual( resolve_vad_overrides(payload)["pre_backend_wait_notice_fast_on_vad_pause"], "True", ) self.assertEqual( resolve_vad_overrides(payload)["deferred_interruption_min_audio_ms"], "1350", ) self.assertEqual( resolve_vad_overrides(payload)["deferred_interruption_enabled"], "False", ) self.assertEqual(resolve_vad_logging_overrides(payload)["log_decisions"], "True") self.assertEqual(resolve_vad_logging_overrides(payload)["log_activity"], "False") self.assertEqual(resolve_ws_overrides(payload)["output_gain"], "1.2") self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_shed_enabled"], "True") self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_shed_threshold_ms"], "700") self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_shed_keep_ms"], "250") self.assertEqual(resolve_ws_overrides(payload)["audio_in_latency_metrics_enabled"], "True") self.assertEqual(resolve_ws_overrides(payload)["audio_in_latency_alert_ms"], "900") self.assertEqual(resolve_ws_overrides(payload)["audio_in_latency_log_interval_s"], "10.5") self.assertEqual(resolve_ws_overrides(payload)["livekit_audio_source_queue_size_ms"], "400") self.assertEqual(resolve_ws_overrides(payload)["livekit_audio_source_clear_on_shed"], "False") self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_energy_shed_enabled"], "True") self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_energy_shed_max_excess_ms"], "600") self.assertEqual(resolve_ws_overrides(payload)["audio_in_backlog_silence_dbfs"], "-60") fake = resolve_fake_agent_overrides(payload) self.assertEqual(fake["delay_ms"], 2500) self.assertEqual(len(fake["responses"]), 2) def test_empty_payload_falls_back_to_defaults(self) -> None: payload = {} self.assertEqual( build_call_config(payload), { "agent_backend": "", "stt": { "provider": "", "language": "", "api_key": "", "initial_prompt": "", "config_override": "", "min_prob_single_word": "", "disable_vosk": "", }, "tts": { "provider": "", "voice_id": "", "model_id": "", "language": "", }, "vad": { "min_speech_duration": "", "activation_threshold": "", "deactivation_threshold": "", "min_silence_duration": "", "prefix_padding_duration": "", "pre_backend_wait_notice_fast_on_vad_pause": "", "deferred_interruption_min_audio_ms": "", "deferred_interruption_enabled": "", }, "vad_logging": { "log_decisions": "", "log_activity": "", "activity_min_probability": "", }, "ws": { "output_gain": "", "audio_in_backlog_shed_enabled": "", "audio_in_backlog_shed_threshold_ms": "", "audio_in_backlog_shed_keep_ms": "", "audio_in_latency_metrics_enabled": "", "audio_in_latency_alert_ms": "", "audio_in_latency_log_interval_s": "", "livekit_audio_source_queue_size_ms": "", "livekit_audio_source_clear_on_shed": "", "audio_in_backlog_energy_shed_enabled": "", "audio_in_backlog_energy_shed_max_excess_ms": "", "audio_in_backlog_silence_dbfs": "", }, "agent_fake": {"delay_ms": "", "responses": ""}, }, ) self.assertEqual(resolve_agent_backend_name(payload, "remote_ws"), "remote_ws") def test_fake_responses_are_trimmed_and_default_delay_is_applied(self) -> None: payload = { "agentBackend": "remote_ws_fake", "agentFake": { "responses": ( " Primeira resposta simulada com comprimento intermediario ;" " Segunda resposta simulada encerrando corretamente a chamada " ) }, } fake = resolve_fake_agent_overrides(payload) self.assertEqual(fake["delay_ms"], 2500) self.assertEqual( fake["responses"], [ "Primeira resposta simulada com comprimento intermediario", "Segunda resposta simulada encerrando corretamente a chamada", ], ) def test_fake_responses_reject_invalid_contract(self) -> None: valid = "Esta resposta simulada possui tamanho intermediario adequado" invalid_cases = [ {"agentBackend": "remote_ws", "agentFake": {"responses": f"{valid};{valid}"}}, {"agentBackend": "remote_ws_fake", "agentFake": {"responses": valid}}, {"agentBackend": "remote_ws_fake", "agentFake": {"responses": f"{valid};;{valid}"}}, {"agentBackend": "remote_ws_fake", "agentFake": {"responses": f"curta;{valid}"}}, { "agentBackend": "remote_ws_fake", "agentFake": {"responses": f"{valid};{valid}", "delayMs": 180001}, }, ] for payload in invalid_cases: with self.subTest(payload=payload), self.assertRaises(ValueError): resolve_fake_agent_overrides(payload) def test_disable_vosk_and_fake_backend_are_normalized_per_call(self) -> None: payload = { "agentBackend": "remote_ws_fake", "stt": {"provider": "internal_http", "disableVosk": True}, } self.assertEqual(resolve_agent_backend_name(payload, "remote_ws"), "remote_ws_fake") self.assertEqual(resolve_stt_overrides(payload)["disable_vosk"], "True") def test_fast_vad_pause_override_lives_with_vad_overrides(self) -> None: payload = {"vad": {"preBackendWaitNoticeFastOnVadPause": True}} self.assertEqual( resolve_vad_overrides(payload)["pre_backend_wait_notice_fast_on_vad_pause"], "True", ) def test_deferred_interruption_min_audio_override_lives_with_vad_overrides(self) -> None: payload = {"vad": {"deferredInterruptionMinAudioMs": 1250}} self.assertEqual( resolve_vad_overrides(payload)["deferred_interruption_min_audio_ms"], "1250", ) def test_deferred_interruption_min_audio_accepts_root_env_style_key(self) -> None: payload = {"DEFERRED_INTERRUPTION_MIN_AUDIO_MS": 1400} self.assertEqual( resolve_vad_overrides(payload)["deferred_interruption_min_audio_ms"], "1400", ) def test_deferred_interruption_enabled_accepts_root_env_style_key(self) -> None: payload = {"DEFERRED_INTERRUPTION_ENABLED": False} self.assertEqual( resolve_vad_overrides(payload)["deferred_interruption_enabled"], "False", ) if __name__ == "__main__": unittest.main()