Files
talk/internal/provider/openai_test.go
T

164 lines
4.9 KiB
Go
Raw Normal View History

package provider
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
)
// TestOpenAISynthesize200: 200 with bytes → bytes unchanged; POST
// /v1/audio/speech, bearer key, body with model/voice/input/response_format
// per the API contract (default model tts-1, default voice alloy).
func TestOpenAISynthesize200(t *testing.T) {
ts, rec := fakeTTSServer(t, 200, fakeMP3)
p := NewOpenAI(ChainConfig{
Getenv: envMap(map[string]string{
EnvOpenAIKey: "sk-test",
EnvOpenAIEndpoint: ts.URL,
}),
})
data, err := p.Synthesize(context.Background(), "Build finished", "en")
if err != nil {
t.Fatalf("Synthesize: %v", err)
}
if string(data) != string(fakeMP3) {
t.Errorf("Synthesize = %q, want fake bytes", data)
}
if rec.method != http.MethodPost {
t.Errorf("method = %s, want POST", rec.method)
}
if want := "/v1/audio/speech"; rec.path != want {
t.Errorf("path = %q, want %q", rec.path, want)
}
if got := rec.header.Get("Authorization"); got != "Bearer sk-test" {
t.Errorf("Authorization = %q, want Bearer sk-test", got)
}
var body struct {
Model string `json:"model"`
Voice string `json:"voice"`
Input string `json:"input"`
ResponseFormat string `json:"response_format"`
}
if err := json.Unmarshal(rec.body, &body); err != nil {
t.Fatalf("body is not JSON: %v (%s)", err, rec.body)
}
if body.Model != "tts-1" {
t.Errorf("model = %q, want default tts-1", body.Model)
}
if body.Voice != "alloy" {
t.Errorf("voice = %q, want default alloy", body.Voice)
}
if body.Input != "Build finished" {
t.Errorf("input = %q", body.Input)
}
if body.ResponseFormat != "mp3" {
t.Errorf("response_format = %q, want mp3", body.ResponseFormat)
}
}
// TestOpenAIHTTPError: 500 and 401 are clear errors with the status.
func TestOpenAIHTTPError(t *testing.T) {
for _, status := range []int{500, 401} {
t.Run(http.StatusText(status), func(t *testing.T) {
ts, _ := fakeTTSServer(t, status, []byte("boom"))
p := NewOpenAI(ChainConfig{Getenv: envMap(map[string]string{
EnvOpenAIKey: "sk-test",
EnvOpenAIEndpoint: ts.URL,
})})
_, err := p.Synthesize(context.Background(), "Hello", "de")
if err == nil || !strings.Contains(err.Error(), fmt.Sprintf("%d", status)) {
t.Fatalf("Synthesize error = %v, want HTTP %d", err, status)
}
if !strings.Contains(err.Error(), "openai") {
t.Errorf("error should name the provider: %v", err)
}
})
}
}
// TestOpenAIModelConfigurable: [providers.openai].model changes the model
// string in the body (decision note: default tts-1, alternatives such as
// gpt-4o-mini-tts allowed).
func TestOpenAIModelConfigurable(t *testing.T) {
ts, rec := fakeTTSServer(t, 200, fakeMP3)
p := NewOpenAI(ChainConfig{
Getenv: envMap(map[string]string{
EnvOpenAIKey: "sk-test",
EnvOpenAIEndpoint: ts.URL,
}),
Model: "gpt-4o-mini-tts",
})
if _, err := p.Synthesize(context.Background(), "Hello", "de"); err != nil {
t.Fatalf("Synthesize: %v", err)
}
var body struct {
Model string `json:"model"`
}
if err := json.Unmarshal(rec.body, &body); err != nil {
t.Fatal(err)
}
if body.Model != "gpt-4o-mini-tts" {
t.Errorf("model = %q, want gpt-4o-mini-tts", body.Model)
}
}
// TestOpenAIAvailable: the key decides; without a network call.
func TestOpenAIAvailable(t *testing.T) {
if NewOpenAI(ChainConfig{Getenv: envMap(nil)}).Available(context.Background()) {
t.Error("Available without key = true, want false")
}
p := NewOpenAI(ChainConfig{Getenv: envMap(map[string]string{EnvOpenAIKey: "sk"})})
if !p.Available(context.Background()) {
t.Error("Available with key = false, want true")
}
}
// TestOpenAIVoicePrecedence: pin beats mapping beats default.
func TestOpenAIVoicePrecedence(t *testing.T) {
t.Run("mapping beats default", func(t *testing.T) {
ts, rec := fakeTTSServer(t, 200, fakeMP3)
p := NewOpenAI(ChainConfig{
Getenv: envMap(map[string]string{
EnvOpenAIKey: "sk",
EnvOpenAIEndpoint: ts.URL,
}),
Voices: map[string]map[string]string{"openai": {"en": "nova"}},
})
if _, err := p.Synthesize(context.Background(), "Hi", "en"); err != nil {
t.Fatalf("Synthesize: %v", err)
}
var body struct {
Voice string `json:"voice"`
}
json.Unmarshal(rec.body, &body)
if body.Voice != "nova" {
t.Errorf("voice = %q, want nova (config)", body.Voice)
}
})
t.Run("pin beats mapping", func(t *testing.T) {
ts, rec := fakeTTSServer(t, 200, fakeMP3)
p := NewOpenAI(ChainConfig{
Getenv: envMap(map[string]string{
EnvOpenAIKey: "sk",
EnvOpenAIEndpoint: ts.URL,
}),
Voices: map[string]map[string]string{"openai": {"en": "nova"}},
})
p.PinVoice("echo")
if _, err := p.Synthesize(context.Background(), "Hi", "en"); err != nil {
t.Fatalf("Synthesize: %v", err)
}
var body struct {
Voice string `json:"voice"`
}
json.Unmarshal(rec.body, &body)
if body.Voice != "echo" {
t.Errorf("voice = %q, want echo (--voice)", body.Voice)
}
})
}