package ai import ( "encoding/json" "os" "strings" "testing" "git.ipao.vip/rogee/go-sip/internal/configread" ) func currentFixture(t *testing.T, mode string) (configread.Task, map[string]configread.Provider) { t.Helper() name := "config-read-task-full.json" if mode == "asr_only" { name = "config-read-task-asr.json" } raw, err := os.ReadFile("../../contracts/schema/examples/" + name) if err != nil { t.Fatal(err) } var task configread.Task if err := json.Unmarshal(raw, &task); err != nil { t.Fatal(err) } providers := map[string]configread.Provider{} for _, code := range []string{"volcengine", "openai_compatible", "ali_bailian"} { providers[code] = configread.Provider{ProviderRef: "catalog-" + code, Code: code, Name: "Example only", Endpoint: "https://example.invalid", WSEndpoint: "wss://example.invalid", Credential: "example-only-not-a-real-secret"} } task = changeCurrentAgent(t, task, func(a map[string]any) { a["asr"] = map[string]any{"provider_ref": "volcengine", "model": "example-asr", "params": map[string]any{"result_type": "full", "enable_nonstream": false, "enable_itn": false, "sample_rate": 16000}} if mode == "full_ai" { a["llm"] = map[string]any{"provider_ref": "openai_compatible", "model": "example-chat", "params": map[string]any{"temperature": 0, "max_tokens": 256}} a["tts"] = map[string]any{"provider_ref": "ali_bailian", "model": "qwen3-tts-flash", "voice": "Cherry", "params": map[string]any{"format": "pcm", "sample_rate": 16000, "rate": 1, "language": "Chinese"}} a["prompt"].(map[string]any)["text"] = "Example only" } }) return task, providers } func changeCurrentAgent(t *testing.T, task configread.Task, change func(map[string]any)) configread.Task { t.Helper() var body map[string]any if err := json.Unmarshal(task.Raw, &body); err != nil { t.Fatal(err) } change(body["agent"].(map[string]any)) raw, err := json.Marshal(body) if err != nil { t.Fatal(err) } var updated configread.Task if err := json.Unmarshal(raw, &updated); err != nil { t.Fatal(err) } return updated } func TestBindCurrentFullAIUsesApprovedSDKFields(t *testing.T) { task, ps := currentFixture(t, "full_ai") b, err := Bind(task, ps) if err != nil { t.Fatal(err) } if b.LLM == nil || b.TTS == nil || b.ASR.Provider.Code != "volcengine" || b.LLM.Model != "example-chat" || b.TTS.Voice != "Cherry" || b.TTS.Protocol != TTSProtocolDashScopeTask || string(b.LLM.Params["temperature"]) != "0" { t.Fatal("approved settings changed") } if len(b.HangupKeywords) != 1 || b.HangupKeywords[0].ClosingRemark != "好的,祝您生活愉快。" || b.Prompt != "Example only" || b.Opening != "Example greeting" { t.Fatal("conversation settings changed") } } func TestBindCurrentASROnlyDoesNotBindOtherProviders(t *testing.T) { task, ps := currentFixture(t, "asr_only") delete(ps, "openai_compatible") delete(ps, "ali_bailian") b, err := Bind(task, ps) if err != nil || b.LLM != nil || b.TTS != nil { t.Fatal("ASR-only inherited another provider", err) } } func TestBindBailianTTSOutputSettings(t *testing.T) { tests := []struct { name string params map[string]any wantErr bool wantFormat string wantRate int }{ {name: "missing format defaults to PCM at configured rate", params: map[string]any{"sample_rate": 16000}, wantFormat: "pcm", wantRate: 16000}, {name: "missing settings defaults to internal PCM16", params: map[string]any{}, wantFormat: "pcm", wantRate: internalPCM16SampleRate}, {name: "PCM missing sample rate defaults to internal rate", params: map[string]any{"format": "pcm"}, wantFormat: "pcm", wantRate: internalPCM16SampleRate}, {name: "unsupported format", params: map[string]any{"format": "ogg"}, wantErr: true}, {name: "invalid PCM rate", params: map[string]any{"format": "pcm", "sample_rate": 1600}, wantErr: true}, {name: "PCM", params: map[string]any{"format": "pcm", "sample_rate": 16000}, wantFormat: "pcm", wantRate: 16000}, {name: "MP3", params: map[string]any{"format": "mp3", "sample_rate": 16000}, wantFormat: "mp3", wantRate: 16000}, {name: "MP3 rate from bitstream", params: map[string]any{"format": "mp3"}, wantFormat: "mp3"}, {name: "WAV", params: map[string]any{"format": "wav", "sample_rate": 8000}, wantFormat: "wav", wantRate: 8000}, {name: "WAV rate from container", params: map[string]any{"format": "wav"}, wantFormat: "wav"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { task, providers := currentFixture(t, "full_ai") task = changeCurrentAgent(t, task, func(agent map[string]any) { agent["tts"].(map[string]any)["params"] = tt.params }) binding, err := Bind(task, providers) if tt.wantErr && err == nil { t.Fatal("invalid TTS output settings were accepted") } if !tt.wantErr && err != nil { t.Fatalf("valid TTS output settings were rejected: %v", err) } if !tt.wantErr && (binding.TTS.OutputFormat != tt.wantFormat || binding.TTS.OutputSampleRate != tt.wantRate) { t.Fatalf("TTS output settings = %q/%d, want %q/%d", binding.TTS.OutputFormat, binding.TTS.OutputSampleRate, tt.wantFormat, tt.wantRate) } }) } } func TestBindCurrentRejectsUnauthorizedProviderBeforeCall(t *testing.T) { for _, kind := range []string{"missing", "code", "endpoint", "credential"} { t.Run(kind, func(t *testing.T) { task, ps := currentFixture(t, "full_ai") p := ps["volcengine"] switch kind { case "missing": delete(ps, "volcengine") case "code": p.Code = "unknown" ps["volcengine"] = p case "endpoint": p.WSEndpoint = "" ps["volcengine"] = p case "credential": p.Credential = "" ps["volcengine"] = p } if _, err := Bind(task, ps); err == nil || strings.Contains(err.Error(), "example-only-not-a-real-secret") { t.Fatal("missing/redacted provider validation", err) } }) } }