package ai import ( "context" "encoding/base64" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" ) func TestCurrentCallOpeningUsesApprovedTTSOnce(t *testing.T) { task, providers := currentFixture(t, "full_ai") var calls atomic.Int32 text := make(chan string, 1) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) var payload struct { Params struct { Text string `json:"text"` } `json:"req_params"` } if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { http.Error(w, "invalid SDK request", http.StatusBadRequest) return } text <- payload.Params.Text _, _ = fmt.Fprintf(w, `{"code":0,"data":%q}`+"\n", base64.StdEncoding.EncodeToString([]byte{1, 0, 2, 0})) _, _ = fmt.Fprintln(w, `{"code":20000000,"message":"ok","data":null}`) })) defer server.Close() p := providers["tts-example"] p.Endpoint = server.URL providers[p.ProviderRef] = p bound, err := BindCurrent(task, providers) if err != nil { t.Fatal(err) } call, err := NewCurrentCall(bound, func(context.Context) error { return nil }) if err != nil { t.Fatal(err) } audio, err := call.Open(context.Background()) if err != nil || len(audio) != 4 || <-text != bound.Opening { t.Fatalf("approved opening was not synthesized once: len=%d err=%v", len(audio), err) } if _, err := call.Open(context.Background()); err == nil || !strings.Contains(err.Error(), "already") || calls.Load() != 1 { t.Fatalf("opening cannot be replayed: requests=%d err=%v", calls.Load(), err) } } func TestCurrentCallOpeningFailureIsVisibleAndNeverRetried(t *testing.T) { task, providers := currentFixture(t, "full_ai") var calls atomic.Int32 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) http.Error(w, "mock provider unavailable", http.StatusServiceUnavailable) })) defer server.Close() p := providers["tts-example"] p.Endpoint = server.URL providers[p.ProviderRef] = p bound, err := BindCurrent(task, providers) if err != nil { t.Fatal(err) } call, err := NewCurrentCall(bound, func(context.Context) error { return nil }) if err != nil { t.Fatal(err) } if _, err := call.Open(context.Background()); err == nil { t.Fatal("provider failure must be visible") } if _, err := call.Open(context.Background()); err == nil || calls.Load() != 1 { t.Fatalf("uncertain opening must not replay: requests=%d err=%v", calls.Load(), err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := call.RunTurn(ctx, []byte{1, 0}); err == nil || !strings.Contains(err.Error(), "opening") { t.Fatalf("an unsuccessful opening cannot be silently skipped: %v", err) } } func TestCurrentCallASROnlyNeverRequestsOpeningTTS(t *testing.T) { task, providers := currentFixture(t, "asr_only") bound, err := BindCurrent(task, providers) if err != nil { t.Fatal(err) } call, err := NewCurrentCall(bound, func(context.Context) error { return nil }) if err != nil { t.Fatal(err) } if _, err := call.Open(context.Background()); err == nil { t.Fatal("ASR-only call cannot synthesize an opening") } } func TestCurrentCallOptionalEmptyOpeningMakesNoTTSRequest(t *testing.T) { task, providers := currentFixture(t, "full_ai") task = changeCurrentAgent(t, task, func(agent map[string]any) { agent["conversation"].(map[string]any)["opening"] = "" }) bound, err := BindCurrent(task, providers) if err != nil { t.Fatal(err) } call, err := NewCurrentCall(bound, func(context.Context) error { return nil }) if err != nil { t.Fatal(err) } if audio, err := call.Open(context.Background()); err != nil || len(audio) != 0 { t.Fatalf("an approved empty opening must not call TTS: len=%d err=%v", len(audio), err) } if _, err := call.Open(context.Background()); err == nil { t.Fatal("opening stage cannot run twice even if no audio was configured") } } func TestCurrentCallRequiresApprovedOpeningBeforeFullAITurn(t *testing.T) { task, providers := currentFixture(t, "full_ai") bound, err := BindCurrent(task, providers) if err != nil { t.Fatal(err) } call, err := NewCurrentCall(bound, func(context.Context) error { return nil }) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := call.RunTurn(ctx, []byte{1, 0}); err == nil || !strings.Contains(err.Error(), "opening") { t.Fatalf("a full-AI turn cannot skip its configured opening: %v", err) } }