feat(config): Provider-Pin, Provider-Endpunkte und Voice-Mapping

config.toml bekommt provider = "…" (Pin, Env-Override TALK_PROVIDER),
[providers.<name>] endpoint/model (Endpoints fake-bar für Tests; Keys
bleiben ausschließlich Env) und voice.<provider>.<lang> = id. TOML-Key ist
bewusst providers (Mehrzahl), damit Pin ( Singular) und Einstellungs-
Tabelle koexistieren können. Env-Overrides der Endpunkte liest die
Provider-Schicht (TALK_ELEVENLABS_ENDPOINT etc.) — das config-Paket
bleibt frei von Provider-Namen.
This commit is contained in:
m3ta-chiron committed 2026-10-18 16:34:24 +02:00
1 parent 7a843ae441
commit 88eddc63d2
2 files changed
+118 -2

No files matched your search

+84
View File
@@ -204,3 +204,87 @@ func TestLoadMerge(t *testing.T) {
}
})
}
// TestLoadProviderKette deckt die Task-03-Felder ab: Provider-Pin per
// Datei/Env, Provider-Endpunkte/-Modelle und das Voice-Mapping.
func TestLoadProviderKette(t *testing.T) {
writeTOML := func(t *testing.T, content string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "config.toml")
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return path
}
toml := `provider = "openai"
[providers.elevenlabs]
endpoint = "https://eleven.example.com"
[providers.openai]
endpoint = "https://openai.example.com"
model = "gpt-4o-mini-tts"
[providers.edge]
endpoint = "wss://edge.example.com/synth"
[voice.elevenlabs]
de = "MeineStimme"
en = "MyVoice"
[voice.edge]
de = "de-DE-KatjaNeural"
`
t.Run("TOML liefert Pin, Endpunkte und Voice-Mapping", func(t *testing.T) {
cfg, err := Load(Options{File: writeTOML(t, toml), Getenv: env(nil)})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Provider != "openai" {
t.Errorf("Provider = %q, want %q", cfg.Provider, "openai")
}
if got := cfg.Providers["elevenlabs"].Endpoint; got != "https://eleven.example.com" {
t.Errorf("Providers[elevenlabs].Endpoint = %q", got)
}
if got := cfg.Providers["openai"].Model; got != "gpt-4o-mini-tts" {
t.Errorf("Providers[openai].Model = %q", got)
}
if got := cfg.Providers["edge"].Endpoint; got != "wss://edge.example.com/synth" {
t.Errorf("Providers[edge].Endpoint = %q", got)
}
if got := cfg.Voice["elevenlabs"]["de"]; got != "MeineStimme" {
t.Errorf("Voice[elevenlabs][de] = %q", got)
}
if got := cfg.Voice["elevenlabs"]["en"]; got != "MyVoice" {
t.Errorf("Voice[elevenlabs][en] = %q", got)
}
})
t.Run("TALK_PROVIDER schlägt Datei-Pin", func(t *testing.T) {
cfg, err := Load(Options{
File: writeTOML(t, toml),
Getenv: env(map[string]string{"TALK_PROVIDER": "edge"}),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Provider != "edge" {
t.Errorf("Provider = %q, want Env-Override %q", cfg.Provider, "edge")
}
})
t.Run("ohne alles: keine Pins, keine Endpunkte", func(t *testing.T) {
cfg, err := Load(Options{
File: filepath.Join(t.TempDir(), "gibt-es-nicht.toml"),
Getenv: env(nil),
})
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Provider != "" || cfg.Providers != nil || cfg.Voice != nil {
t.Errorf("Provider-Konfiguration = %+v, will leer", cfg)
}
})
}