2026-04-08 19:20:31 +05:30
|
|
|
"""
|
|
|
|
|
TDD tests for resolve_effective_config().
|
|
|
|
|
|
|
|
|
|
This function deep-merges workflow-level model_overrides onto the global
|
|
|
|
|
UserConfiguration. Fields not overridden inherit from global.
|
|
|
|
|
|
|
|
|
|
Module under test: api.services.configuration.resolve
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from api.schemas.user_configuration import UserConfiguration
|
2026-05-27 01:31:14 -07:00
|
|
|
from api.services.configuration.masking import (
|
|
|
|
|
contains_masked_key,
|
|
|
|
|
mask_workflow_configurations,
|
|
|
|
|
)
|
|
|
|
|
from api.services.configuration.merge import merge_workflow_configuration_secrets
|
2026-04-08 19:20:31 +05:30
|
|
|
from api.services.configuration.registry import (
|
|
|
|
|
DeepgramSTTConfiguration,
|
|
|
|
|
ElevenlabsTTSConfiguration,
|
|
|
|
|
GoogleRealtimeLLMConfiguration,
|
2026-05-22 18:04:59 +05:30
|
|
|
GoogleVertexLLMConfiguration,
|
|
|
|
|
GrokRealtimeLLMConfiguration,
|
2026-04-08 19:20:31 +05:30
|
|
|
OpenAILLMService,
|
2026-05-23 12:51:55 +05:30
|
|
|
UltravoxRealtimeLLMConfiguration,
|
2026-04-08 19:20:31 +05:30
|
|
|
)
|
2026-05-27 01:31:14 -07:00
|
|
|
from api.services.configuration.resolve import (
|
|
|
|
|
enrich_overrides_with_api_keys,
|
|
|
|
|
resolve_effective_config,
|
|
|
|
|
)
|
2026-04-08 19:20:31 +05:30
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Fixtures
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def global_config() -> UserConfiguration:
|
|
|
|
|
"""A realistic global user configuration."""
|
|
|
|
|
return UserConfiguration(
|
|
|
|
|
llm=OpenAILLMService(
|
|
|
|
|
provider="openai", api_key="sk-global-llm", model="gpt-4.1"
|
|
|
|
|
),
|
|
|
|
|
tts=ElevenlabsTTSConfiguration(
|
|
|
|
|
provider="elevenlabs",
|
|
|
|
|
api_key="el-global-tts",
|
|
|
|
|
voice="Rachel",
|
|
|
|
|
model="eleven_flash_v2_5",
|
|
|
|
|
),
|
|
|
|
|
stt=DeepgramSTTConfiguration(
|
|
|
|
|
provider="deepgram",
|
|
|
|
|
api_key="dg-global-stt",
|
|
|
|
|
model="nova-3-general",
|
|
|
|
|
language="multi",
|
|
|
|
|
),
|
|
|
|
|
is_realtime=False,
|
|
|
|
|
realtime=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def global_config_realtime() -> UserConfiguration:
|
|
|
|
|
"""Global config with realtime enabled."""
|
|
|
|
|
return UserConfiguration(
|
|
|
|
|
llm=OpenAILLMService(
|
|
|
|
|
provider="openai", api_key="sk-global-llm", model="gpt-4.1"
|
|
|
|
|
),
|
|
|
|
|
tts=ElevenlabsTTSConfiguration(
|
|
|
|
|
provider="elevenlabs",
|
|
|
|
|
api_key="el-global-tts",
|
|
|
|
|
voice="Rachel",
|
|
|
|
|
model="eleven_flash_v2_5",
|
|
|
|
|
),
|
|
|
|
|
stt=DeepgramSTTConfiguration(
|
|
|
|
|
provider="deepgram",
|
|
|
|
|
api_key="dg-global-stt",
|
|
|
|
|
model="nova-3-general",
|
|
|
|
|
language="multi",
|
|
|
|
|
),
|
|
|
|
|
is_realtime=True,
|
|
|
|
|
realtime=GoogleRealtimeLLMConfiguration(
|
|
|
|
|
provider="google_realtime",
|
|
|
|
|
api_key="goog-global-rt",
|
|
|
|
|
model="gemini-3.1-flash-live-preview",
|
|
|
|
|
voice="Puck",
|
|
|
|
|
language="en",
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# No overrides → global returned unchanged
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestNoOverrides:
|
|
|
|
|
def test_none_overrides_returns_global(self, global_config):
|
|
|
|
|
result = resolve_effective_config(global_config, None)
|
|
|
|
|
assert result.llm.model == "gpt-4.1"
|
|
|
|
|
assert result.tts.voice == "Rachel"
|
|
|
|
|
assert result.stt.model == "nova-3-general"
|
|
|
|
|
assert result.is_realtime is False
|
|
|
|
|
|
|
|
|
|
def test_empty_dict_overrides_returns_global(self, global_config):
|
|
|
|
|
result = resolve_effective_config(global_config, {})
|
|
|
|
|
assert result.llm.model == "gpt-4.1"
|
|
|
|
|
assert result.tts.voice == "Rachel"
|
|
|
|
|
|
|
|
|
|
def test_does_not_mutate_original(self, global_config):
|
|
|
|
|
"""The original config object must not be modified."""
|
|
|
|
|
resolve_effective_config(global_config, {"llm": {"model": "gpt-4.1-mini"}})
|
|
|
|
|
assert global_config.llm.model == "gpt-4.1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Single-field overrides within a section (same provider)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestSingleFieldOverride:
|
|
|
|
|
def test_override_llm_model_only(self, global_config):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config, {"llm": {"model": "gpt-4.1-mini"}}
|
|
|
|
|
)
|
|
|
|
|
assert result.llm.model == "gpt-4.1-mini"
|
|
|
|
|
assert result.llm.provider == "openai" # inherited
|
|
|
|
|
assert result.llm.api_key == "sk-global-llm" # inherited
|
|
|
|
|
|
|
|
|
|
def test_override_tts_voice_only(self, global_config):
|
|
|
|
|
result = resolve_effective_config(global_config, {"tts": {"voice": "shimmer"}})
|
|
|
|
|
assert result.tts.voice == "shimmer"
|
|
|
|
|
assert result.tts.provider == "elevenlabs" # inherited
|
|
|
|
|
assert result.tts.api_key == "el-global-tts" # inherited
|
|
|
|
|
|
|
|
|
|
def test_override_stt_language_only(self, global_config):
|
|
|
|
|
result = resolve_effective_config(global_config, {"stt": {"language": "en"}})
|
|
|
|
|
assert result.stt.language == "en"
|
|
|
|
|
assert result.stt.model == "nova-3-general" # inherited
|
|
|
|
|
assert result.stt.provider == "deepgram" # inherited
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Provider change (requires full section replacement)
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestProviderChange:
|
|
|
|
|
def test_override_llm_to_different_provider(self, global_config):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config,
|
|
|
|
|
{
|
|
|
|
|
"llm": {
|
|
|
|
|
"provider": "groq",
|
|
|
|
|
"api_key": "groq-key",
|
|
|
|
|
"model": "llama-3.3-70b-versatile",
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert result.llm.provider == "groq"
|
|
|
|
|
assert result.llm.model == "llama-3.3-70b-versatile"
|
|
|
|
|
assert result.llm.api_key == "groq-key"
|
|
|
|
|
|
|
|
|
|
def test_provider_change_does_not_affect_other_sections(self, global_config):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config,
|
|
|
|
|
{
|
|
|
|
|
"llm": {
|
|
|
|
|
"provider": "groq",
|
|
|
|
|
"api_key": "groq-key",
|
|
|
|
|
"model": "llama-3.3-70b-versatile",
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
# TTS and STT unchanged
|
|
|
|
|
assert result.tts.provider == "elevenlabs"
|
|
|
|
|
assert result.stt.provider == "deepgram"
|
|
|
|
|
|
2026-05-22 18:04:59 +05:30
|
|
|
def test_override_llm_to_google_vertex(self, global_config):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config,
|
|
|
|
|
{
|
|
|
|
|
"llm": {
|
|
|
|
|
"provider": "google_vertex",
|
|
|
|
|
"model": "gemini-2.5-flash",
|
|
|
|
|
"project_id": "demo-project",
|
|
|
|
|
"location": "us-east4",
|
|
|
|
|
"credentials": '{"type":"service_account"}',
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert isinstance(result.llm, GoogleVertexLLMConfiguration)
|
|
|
|
|
assert result.llm.provider == "google_vertex"
|
|
|
|
|
assert result.llm.project_id == "demo-project"
|
|
|
|
|
|
2026-04-08 19:20:31 +05:30
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# API key inheritance
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAPIKeyInheritance:
|
|
|
|
|
def test_no_api_key_in_override_inherits_global(self, global_config):
|
|
|
|
|
"""When override omits api_key, global key is used."""
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config, {"llm": {"model": "gpt-4.1-mini"}}
|
|
|
|
|
)
|
|
|
|
|
assert result.llm.api_key == "sk-global-llm"
|
|
|
|
|
|
|
|
|
|
def test_explicit_api_key_in_override_wins(self, global_config):
|
|
|
|
|
"""When override includes api_key, it takes precedence."""
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config,
|
|
|
|
|
{"llm": {"model": "gpt-4.1-mini", "api_key": "sk-override-key"}},
|
|
|
|
|
)
|
|
|
|
|
assert result.llm.api_key == "sk-override-key"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# is_realtime override
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestRealtimeOverride:
|
|
|
|
|
def test_enable_realtime_on_non_realtime_global(self, global_config):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config,
|
|
|
|
|
{
|
|
|
|
|
"is_realtime": True,
|
|
|
|
|
"realtime": {
|
|
|
|
|
"provider": "google_realtime",
|
|
|
|
|
"api_key": "goog-override",
|
|
|
|
|
"model": "gemini-3.1-flash-live-preview",
|
|
|
|
|
"voice": "Charon",
|
|
|
|
|
"language": "en",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert result.is_realtime is True
|
|
|
|
|
assert result.realtime.provider == "google_realtime"
|
|
|
|
|
assert result.realtime.voice == "Charon"
|
|
|
|
|
|
|
|
|
|
def test_disable_realtime_on_realtime_global(self, global_config_realtime):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config_realtime, {"is_realtime": False}
|
|
|
|
|
)
|
|
|
|
|
assert result.is_realtime is False
|
|
|
|
|
# Realtime config may still be present but is_realtime flag controls usage
|
|
|
|
|
|
|
|
|
|
def test_override_realtime_voice_only(self, global_config_realtime):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config_realtime, {"realtime": {"voice": "Kore"}}
|
|
|
|
|
)
|
|
|
|
|
assert result.realtime.voice == "Kore"
|
|
|
|
|
assert result.realtime.provider == "google_realtime" # inherited
|
|
|
|
|
assert result.realtime.api_key == "goog-global-rt" # inherited
|
|
|
|
|
|
2026-05-22 18:04:59 +05:30
|
|
|
def test_switch_realtime_provider_to_grok(self, global_config_realtime):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config_realtime,
|
|
|
|
|
{
|
|
|
|
|
"realtime": {
|
|
|
|
|
"provider": "grok_realtime",
|
|
|
|
|
"api_key": "xai-key",
|
|
|
|
|
"model": "grok-voice-think-fast-1.0",
|
|
|
|
|
"voice": "Sal",
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert isinstance(result.realtime, GrokRealtimeLLMConfiguration)
|
|
|
|
|
assert result.realtime.provider == "grok_realtime"
|
|
|
|
|
assert result.realtime.voice == "Sal"
|
|
|
|
|
|
2026-05-23 12:51:55 +05:30
|
|
|
def test_switch_realtime_provider_to_ultravox(self, global_config_realtime):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config_realtime,
|
|
|
|
|
{
|
|
|
|
|
"realtime": {
|
|
|
|
|
"provider": "ultravox_realtime",
|
|
|
|
|
"api_key": "ultra-key",
|
|
|
|
|
"model": "ultravox-v0.7",
|
|
|
|
|
"voice": "Mark",
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert isinstance(result.realtime, UltravoxRealtimeLLMConfiguration)
|
|
|
|
|
assert result.realtime.provider == "ultravox_realtime"
|
|
|
|
|
assert result.realtime.voice == "Mark"
|
|
|
|
|
|
2026-04-08 19:20:31 +05:30
|
|
|
def test_override_is_realtime_only_without_realtime_section(self, global_config):
|
|
|
|
|
"""Override is_realtime=True but provide no realtime config.
|
|
|
|
|
Should set the flag; realtime section stays None from global."""
|
|
|
|
|
result = resolve_effective_config(global_config, {"is_realtime": True})
|
|
|
|
|
assert result.is_realtime is True
|
|
|
|
|
assert result.realtime is None # no config provided
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Section override when global has None for that section
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestOverrideOnNullGlobal:
|
|
|
|
|
def test_override_stt_when_global_is_none(self):
|
|
|
|
|
"""When global has no STT config, override creates one from scratch."""
|
|
|
|
|
config = UserConfiguration(
|
|
|
|
|
llm=OpenAILLMService(provider="openai", api_key="sk-key", model="gpt-4.1"),
|
|
|
|
|
stt=None,
|
|
|
|
|
tts=None,
|
|
|
|
|
is_realtime=False,
|
|
|
|
|
)
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
config,
|
|
|
|
|
{
|
|
|
|
|
"stt": {
|
|
|
|
|
"provider": "deepgram",
|
|
|
|
|
"api_key": "dg-new",
|
|
|
|
|
"model": "nova-3-general",
|
|
|
|
|
"language": "en",
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert result.stt is not None
|
|
|
|
|
assert result.stt.provider == "deepgram"
|
|
|
|
|
assert result.stt.model == "nova-3-general"
|
|
|
|
|
|
|
|
|
|
def test_override_realtime_when_global_is_none(self):
|
|
|
|
|
"""Realtime section can be created from override even if global has none."""
|
|
|
|
|
config = UserConfiguration(
|
|
|
|
|
llm=OpenAILLMService(provider="openai", api_key="sk-key", model="gpt-4.1"),
|
|
|
|
|
is_realtime=False,
|
|
|
|
|
realtime=None,
|
|
|
|
|
)
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
config,
|
|
|
|
|
{
|
|
|
|
|
"is_realtime": True,
|
|
|
|
|
"realtime": {
|
|
|
|
|
"provider": "google_realtime",
|
|
|
|
|
"api_key": "goog-new",
|
|
|
|
|
"model": "gemini-3.1-flash-live-preview",
|
|
|
|
|
"voice": "Puck",
|
|
|
|
|
"language": "en",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert result.is_realtime is True
|
|
|
|
|
assert result.realtime.provider == "google_realtime"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Multi-section overrides
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMultiSectionOverride:
|
|
|
|
|
def test_override_llm_and_tts_not_stt(self, global_config):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config,
|
|
|
|
|
{
|
|
|
|
|
"llm": {"model": "gpt-4.1-mini"},
|
|
|
|
|
"tts": {"voice": "shimmer"},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert result.llm.model == "gpt-4.1-mini"
|
|
|
|
|
assert result.tts.voice == "shimmer"
|
|
|
|
|
# STT untouched
|
|
|
|
|
assert result.stt.model == "nova-3-general"
|
|
|
|
|
assert result.stt.language == "multi"
|
|
|
|
|
|
|
|
|
|
def test_override_all_sections(self, global_config):
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config,
|
|
|
|
|
{
|
|
|
|
|
"llm": {"model": "gpt-4.1-mini"},
|
|
|
|
|
"tts": {"voice": "shimmer"},
|
|
|
|
|
"stt": {"language": "en"},
|
|
|
|
|
"is_realtime": True,
|
|
|
|
|
"realtime": {
|
|
|
|
|
"provider": "google_realtime",
|
|
|
|
|
"api_key": "goog-key",
|
|
|
|
|
"model": "gemini-3.1-flash-live-preview",
|
|
|
|
|
"voice": "Fenrir",
|
|
|
|
|
"language": "en",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert result.llm.model == "gpt-4.1-mini"
|
|
|
|
|
assert result.tts.voice == "shimmer"
|
|
|
|
|
assert result.stt.language == "en"
|
|
|
|
|
assert result.is_realtime is True
|
|
|
|
|
assert result.realtime.voice == "Fenrir"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# Ignored / unknown keys
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestUnknownKeys:
|
|
|
|
|
def test_unknown_section_in_overrides_is_ignored(self, global_config):
|
|
|
|
|
"""Override with a key that doesn't map to any section should not crash."""
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config, {"unknown_section": {"foo": "bar"}}
|
|
|
|
|
)
|
|
|
|
|
assert result.llm.model == "gpt-4.1"
|
|
|
|
|
|
|
|
|
|
def test_embeddings_not_overridable(self, global_config):
|
|
|
|
|
"""Embeddings stay global — overrides for embeddings should be ignored."""
|
|
|
|
|
result = resolve_effective_config(
|
|
|
|
|
global_config,
|
|
|
|
|
{"embeddings": {"provider": "openai", "model": "text-embedding-3-small"}},
|
|
|
|
|
)
|
|
|
|
|
assert result.embeddings is None # was None in global, stays None
|
2026-05-27 01:31:14 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
# enrich_overrides_with_api_keys
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestEnrichOverridesWithApiKeys:
|
|
|
|
|
def test_injects_api_key_when_same_provider(self, global_config):
|
|
|
|
|
"""Override matching the global provider gets the global API key stamped in."""
|
|
|
|
|
overrides = {
|
|
|
|
|
"tts": {
|
|
|
|
|
"provider": "elevenlabs",
|
|
|
|
|
"voice": "Bella",
|
|
|
|
|
"model": "eleven_flash_v2_5",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
enriched = enrich_overrides_with_api_keys(overrides, global_config)
|
|
|
|
|
assert enriched["tts"]["api_key"] == "el-global-tts"
|
|
|
|
|
|
|
|
|
|
def test_injects_all_api_keys_when_global_has_multiple(self, global_config):
|
|
|
|
|
"""Override matching a multi-key global provider gets every global key."""
|
|
|
|
|
global_config.llm.api_key = ["sk-global-1", "sk-global-2"]
|
|
|
|
|
overrides = {"llm": {"provider": "openai", "model": "gpt-4.1-mini"}}
|
|
|
|
|
|
|
|
|
|
enriched = enrich_overrides_with_api_keys(overrides, global_config)
|
|
|
|
|
|
|
|
|
|
assert enriched["llm"]["api_key"] == ["sk-global-1", "sk-global-2"]
|
|
|
|
|
|
|
|
|
|
def test_does_not_overwrite_existing_api_key(self, global_config):
|
|
|
|
|
"""Override that already has an api_key keeps its own key."""
|
|
|
|
|
overrides = {
|
|
|
|
|
"tts": {
|
|
|
|
|
"provider": "elevenlabs",
|
|
|
|
|
"api_key": "my-own-key",
|
|
|
|
|
"voice": "Bella",
|
|
|
|
|
"model": "eleven_flash_v2_5",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
enriched = enrich_overrides_with_api_keys(overrides, global_config)
|
|
|
|
|
assert enriched["tts"]["api_key"] == "my-own-key"
|
|
|
|
|
|
|
|
|
|
def test_skips_when_provider_differs(self, global_config):
|
|
|
|
|
"""Override for a different provider is not enriched with the global key."""
|
|
|
|
|
overrides = {
|
|
|
|
|
"tts": {"provider": "cartesia", "voice": "some-voice", "model": "sonic-3"}
|
|
|
|
|
}
|
|
|
|
|
enriched = enrich_overrides_with_api_keys(overrides, global_config)
|
|
|
|
|
assert "api_key" not in enriched["tts"]
|
|
|
|
|
|
|
|
|
|
def test_does_not_mutate_original(self, global_config):
|
|
|
|
|
"""The input overrides dict must not be modified."""
|
|
|
|
|
overrides = {
|
|
|
|
|
"tts": {
|
|
|
|
|
"provider": "elevenlabs",
|
|
|
|
|
"voice": "Bella",
|
|
|
|
|
"model": "eleven_flash_v2_5",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
original_copy = {
|
|
|
|
|
"tts": {
|
|
|
|
|
"provider": "elevenlabs",
|
|
|
|
|
"voice": "Bella",
|
|
|
|
|
"model": "eleven_flash_v2_5",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
enrich_overrides_with_api_keys(overrides, global_config)
|
|
|
|
|
assert overrides == original_copy
|
|
|
|
|
|
|
|
|
|
def test_regression_override_survives_global_provider_change(self, global_config):
|
|
|
|
|
"""Core bug: override for provider A still works after global switches to B.
|
|
|
|
|
|
|
|
|
|
Steps:
|
|
|
|
|
1. Global TTS = ElevenLabs, Override TTS = ElevenLabs (different voice)
|
|
|
|
|
2. enrich_overrides_with_api_keys stamps ElevenLabs API key into override
|
|
|
|
|
3. Global TTS changes to Deepgram (simulate by building a new config)
|
|
|
|
|
4. resolve_effective_config must still return a valid ElevenLabs config
|
|
|
|
|
"""
|
|
|
|
|
override_at_save_time = {
|
|
|
|
|
"tts": {
|
|
|
|
|
"provider": "elevenlabs",
|
|
|
|
|
"voice": "Bella",
|
|
|
|
|
"model": "eleven_flash_v2_5",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
enriched = enrich_overrides_with_api_keys(override_at_save_time, global_config)
|
|
|
|
|
assert enriched["tts"]["api_key"] == "el-global-tts"
|
|
|
|
|
|
|
|
|
|
# Simulate global config switching to Deepgram
|
|
|
|
|
from api.services.configuration.registry import DeepgramTTSConfiguration
|
|
|
|
|
|
|
|
|
|
new_global = global_config.model_copy(
|
|
|
|
|
update={
|
|
|
|
|
"tts": DeepgramTTSConfiguration(
|
|
|
|
|
provider="deepgram", api_key="dg-new", voice="aura-2-helena-en"
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# The enriched override should resolve correctly against the new global
|
|
|
|
|
result = resolve_effective_config(new_global, enriched)
|
|
|
|
|
assert result.tts.provider == "elevenlabs"
|
|
|
|
|
assert result.tts.voice == "Bella"
|
|
|
|
|
assert result.tts.api_key == "el-global-tts"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestWorkflowConfigurationSecrets:
|
|
|
|
|
def test_masks_model_override_secrets(self):
|
|
|
|
|
configs = {
|
|
|
|
|
"model_overrides": {
|
|
|
|
|
"llm": {
|
|
|
|
|
"provider": "openai",
|
|
|
|
|
"api_key": "sk-real-llm-key",
|
|
|
|
|
"model": "gpt-4.1-mini",
|
|
|
|
|
},
|
|
|
|
|
"tts": {
|
|
|
|
|
"provider": "elevenlabs",
|
|
|
|
|
"api_key": "el-real-tts-key",
|
|
|
|
|
"voice": "Bella",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"ambient_noise_configuration": {"enabled": True},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
masked = mask_workflow_configurations(configs)
|
|
|
|
|
|
|
|
|
|
assert masked["model_overrides"]["llm"]["api_key"] != "sk-real-llm-key"
|
|
|
|
|
assert contains_masked_key(masked["model_overrides"]["llm"]["api_key"])
|
|
|
|
|
assert masked["model_overrides"]["llm"]["api_key"].endswith("-key")
|
|
|
|
|
assert masked["model_overrides"]["tts"]["api_key"] != "el-real-tts-key"
|
|
|
|
|
assert masked["ambient_noise_configuration"] == {"enabled": True}
|
|
|
|
|
assert configs["model_overrides"]["llm"]["api_key"] == "sk-real-llm-key"
|
|
|
|
|
|
|
|
|
|
def test_restores_masked_model_override_secrets_from_existing_config(self):
|
|
|
|
|
existing = {
|
|
|
|
|
"model_overrides": {
|
|
|
|
|
"tts": {
|
|
|
|
|
"provider": "elevenlabs",
|
|
|
|
|
"api_key": "el-real-tts-key",
|
|
|
|
|
"voice": "Rachel",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
incoming = mask_workflow_configurations(existing)
|
|
|
|
|
incoming["model_overrides"]["tts"]["voice"] = "Bella"
|
|
|
|
|
|
|
|
|
|
merged = merge_workflow_configuration_secrets(incoming, existing)
|
|
|
|
|
|
|
|
|
|
assert merged["model_overrides"]["tts"]["api_key"] == "el-real-tts-key"
|
|
|
|
|
assert merged["model_overrides"]["tts"]["voice"] == "Bella"
|
|
|
|
|
assert incoming["model_overrides"]["tts"]["api_key"] != "el-real-tts-key"
|
|
|
|
|
|
|
|
|
|
def test_single_masked_key_preserves_existing_multi_key_override(self):
|
|
|
|
|
existing = {
|
|
|
|
|
"model_overrides": {
|
|
|
|
|
"llm": {
|
|
|
|
|
"provider": "openai",
|
|
|
|
|
"api_key": ["sk-workflow-1", "sk-workflow-2"],
|
|
|
|
|
"model": "gpt-4.1-mini",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
incoming = mask_workflow_configurations(existing)
|
|
|
|
|
incoming["model_overrides"]["llm"]["api_key"] = incoming["model_overrides"][
|
|
|
|
|
"llm"
|
|
|
|
|
]["api_key"][0]
|
|
|
|
|
|
|
|
|
|
merged = merge_workflow_configuration_secrets(incoming, existing)
|
|
|
|
|
|
|
|
|
|
assert merged["model_overrides"]["llm"]["api_key"] == [
|
|
|
|
|
"sk-workflow-1",
|
|
|
|
|
"sk-workflow-2",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def test_missing_secret_copies_current_global_key_instead_of_existing_workflow_key(
|
|
|
|
|
self, global_config
|
|
|
|
|
):
|
|
|
|
|
global_config.stt.api_key = ["dg-global-1", "dg-global-2"]
|
|
|
|
|
existing = {
|
|
|
|
|
"model_overrides": {
|
|
|
|
|
"stt": {
|
|
|
|
|
"provider": "deepgram",
|
|
|
|
|
"api_key": "dg-workflow-key",
|
|
|
|
|
"model": "nova-3-general",
|
|
|
|
|
"language": "multi",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
incoming = {
|
|
|
|
|
"model_overrides": {
|
|
|
|
|
"stt": {
|
|
|
|
|
"provider": "deepgram",
|
|
|
|
|
"model": "nova-3-general",
|
|
|
|
|
"language": "en",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
merged = merge_workflow_configuration_secrets(incoming, existing)
|
|
|
|
|
enriched = enrich_overrides_with_api_keys(
|
|
|
|
|
merged["model_overrides"],
|
|
|
|
|
global_config,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert enriched["stt"]["api_key"] == ["dg-global-1", "dg-global-2"]
|
|
|
|
|
assert enriched["stt"]["language"] == "en"
|