Files
story/internal/voiceprovider/volcengine_test.go
T

168 lines
8.0 KiB
Go

package voiceprovider
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
var testVoiceChatTemplate = []byte(`{
"AgentConfig":{"TargetUserId":[],"UserId":"","WelcomeMessage":"你好"},
"Config":{"LLMConfig":{"Mode":"ArkV3","EndPointId":"{{MODEL_RESOURCE_REF}}","SystemMessages":["基础人设"]},"ASRConfig":{"Provider":"volcano"},"TTSConfig":{"Provider":"volcano","VoiceType":"{{VOICE_RESOURCE_REF}}"}},
"TraceConversation":"{{CONVERSATION_ID}}","TraceBundle":"{{AGENT_BUNDLE_VERSION}}"
}`)
func TestVolcengineClientSignsAndMapsStartStop(t *testing.T) {
var calls []string
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if !strings.HasPrefix(request.Header.Get("Authorization"), "HMAC-SHA256 ") || request.Header.Get("X-Date") == "" || request.Header.Get("X-Content-Sha256") == "" {
t.Fatalf("request is not signed: headers=%v", request.Header)
}
action := request.URL.Query().Get("Action")
if request.URL.Query().Get("Version") != "2024-12-01" {
t.Fatalf("unexpected API version: %s", request.URL.RawQuery)
}
calls = append(calls, action)
var body map[string]any
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
t.Fatal(err)
}
if body["AppId"] != "661e7ae5543cf00000000000" || body["RoomId"] != "room-1" || body["TaskId"] != "task-1" {
t.Fatalf("unexpected provider identity mapping: %+v", body)
}
if action == "StartVoiceChat" {
agent := body["AgentConfig"].(map[string]any)
targets := agent["TargetUserId"].([]any)
if len(targets) != 1 || targets[0] != "device-1" || agent["UserId"] != "agent-1" || body["TraceConversation"] != "conversation-1" || body["TraceBundle"] != "bundle-v1" {
t.Fatalf("unexpected agent mapping: %+v", body)
}
config := body["Config"].(map[string]any)
llm := config["LLMConfig"].(map[string]any)
messages := llm["SystemMessages"].([]any)
if len(messages) != 4 || messages[1] != "发布人设" || !strings.Contains(messages[2].(string), "warm") || !strings.Contains(messages[3].(string), "已授权的家庭背景") || llm["EndPointId"] != "model-approved" || config["TTSConfig"].(map[string]any)["VoiceType"] != "voice-approved" {
t.Fatalf("context was not appended: %+v", messages)
}
}
_ = json.NewEncoder(writer).Encode(map[string]any{"Result": "ok", "ResponseMetadata": map[string]any{"RequestId": "request-1"}})
}))
defer server.Close()
client := newTestVolcengineClient(t, server.URL)
start, err := client.Start(context.Background(), StartRequest{TaskRef: "task-1", RoomID: "room-1", DeviceUserID: "device-1", AgentUserID: "agent-1", ConversationID: "conversation-1", EngineMode: "s2s_hybrid", ContextSummary: "已授权的家庭背景", AgentBundleVersion: "bundle-v1", SystemPrompt: "发布人设", PersonaTone: "warm", ModelResourceRef: "model-approved", VoiceResourceRef: "voice-approved"})
if err != nil || start.Status != "ACTIVE" {
t.Fatalf("start=%+v err=%v", start, err)
}
stop, err := client.Stop(context.Background(), StopRequest{TaskRef: "task-1", RoomID: "room-1"})
if err != nil || stop.Status != "COMPLETED" {
t.Fatalf("stop=%+v err=%v", stop, err)
}
if strings.Join(calls, ",") != "StartVoiceChat,StopVoiceChat" {
t.Fatalf("calls=%v", calls)
}
}
func TestVolcengineClientClassifiesRateLimitAndDuplicateStart(t *testing.T) {
calls := 0
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
calls++
code, message := "FlowLimitExceeded.QPSOverLimit", "too many requests"
if calls == 2 {
code, message = "InvalidParameter.TaskAlreadyStarted", "The task has been started."
}
_ = json.NewEncoder(writer).Encode(map[string]any{"ResponseMetadata": map[string]any{"Error": map[string]any{"Code": code, "Message": message}}})
}))
defer server.Close()
client := newTestVolcengineClient(t, server.URL)
request := StartRequest{TaskRef: "task-1", RoomID: "room-1", DeviceUserID: "device-1", AgentUserID: "agent-1", EngineMode: "s2s_hybrid"}
if _, err := client.Start(context.Background(), request); err == nil || !IsTemporary(err) {
t.Fatalf("expected temporary rate-limit error, got %v", err)
}
result, err := client.Start(context.Background(), request)
if err != nil || result.Status != "ACTIVE" {
t.Fatalf("duplicate start should reconcile: result=%+v err=%v", result, err)
}
if _, err = client.Query(context.Background(), QueryRequest{TaskRef: "task-1"}); !IsNotFound(err) {
t.Fatalf("unsupported query must trigger stable-id start reconciliation: %v", err)
}
}
func TestVolcengineClientRejectsUneditedExampleTemplate(t *testing.T) {
_, err := NewVolcengineClient(VolcengineOptions{
BaseURL: "https://rtc.volcengineapi.com", AccessKeyID: strings.Repeat("a", 32), SecretAccessKey: strings.Repeat("s", 40),
AppID: "661e7ae5543cf00000000000", Template: []byte(`{"AgentConfig":{},"Config":{"ASRConfig":{"AppId":"replace-with-asr-app-id"}}}`), Timeout: time.Second,
})
if err == nil || !strings.Contains(err.Error(), "placeholder") {
t.Fatalf("expected placeholder template to be rejected, got %v", err)
}
}
func TestVolcengineClientSelectsFrozenEngineModeTemplate(t *testing.T) {
var markers []string
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
var body map[string]any
if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
t.Fatal(err)
}
markers = append(markers, body["Config"].(map[string]any)["ModeMarker"].(string))
_ = json.NewEncoder(writer).Encode(map[string]any{"Result": "ok", "ResponseMetadata": map[string]any{"RequestId": "request-mode"}})
}))
defer server.Close()
bundle := map[string]any{
"schema_version": voiceChatTemplateBundleSchema,
"templates": map[string]any{
"s2s_pure": map[string]any{"AgentConfig": map[string]any{}, "Config": map[string]any{"ModeMarker": "pure"}},
"s2s_hybrid": map[string]any{"AgentConfig": map[string]any{}, "Config": map[string]any{"ModeMarker": "hybrid"}},
"cascade": map[string]any{"AgentConfig": map[string]any{}, "Config": map[string]any{"ModeMarker": "cascade"}},
},
}
encoded, _ := json.Marshal(bundle)
client, err := NewVolcengineClient(VolcengineOptions{
BaseURL: server.URL, AccessKeyID: strings.Repeat("a", 32), SecretAccessKey: strings.Repeat("s", 40),
Region: "cn-north-1", AppID: "661e7ae5543cf00000000000", APIVersion: "2024-12-01",
Template: encoded, Timeout: time.Second, RequireAllEngineModes: true,
})
if err != nil {
t.Fatal(err)
}
expectedMarkers := map[string]string{"s2s_pure": "pure", "s2s_hybrid": "hybrid", "cascade": "cascade"}
for index, mode := range supportedEngineModes {
_, err = client.Start(context.Background(), StartRequest{
TaskRef: "task-" + mode, RoomID: "room", DeviceUserID: "device", AgentUserID: "agent",
ConversationID: "conversation", EngineMode: mode,
})
if err != nil {
t.Fatalf("Start(%s): %v", mode, err)
}
if markers[index] != expectedMarkers[mode] {
t.Fatalf("mode %s selected marker %s", mode, markers[index])
}
}
}
func TestProductionVolcengineClientRequiresAllEngineModes(t *testing.T) {
bundle := []byte(`{"schema_version":"voice-chat-template-bundle-v1","templates":{"s2s_hybrid":{"AgentConfig":{},"Config":{}}}}`)
_, err := NewVolcengineClient(VolcengineOptions{
BaseURL: "https://rtc.volcengineapi.com", AccessKeyID: strings.Repeat("a", 32), SecretAccessKey: strings.Repeat("s", 40),
AppID: "661e7ae5543cf00000000000", Template: bundle, Timeout: time.Second, RequireAllEngineModes: true,
})
if err == nil || !strings.Contains(err.Error(), "missing engine mode") {
t.Fatalf("expected incomplete production bundle rejection, got %v", err)
}
}
func newTestVolcengineClient(t *testing.T, baseURL string) *VolcengineClient {
t.Helper()
client, err := NewVolcengineClient(VolcengineOptions{
BaseURL: baseURL, AccessKeyID: strings.Repeat("a", 32), SecretAccessKey: strings.Repeat("s", 40),
Region: "cn-north-1", AppID: "661e7ae5543cf00000000000", APIVersion: "2024-12-01",
Template: testVoiceChatTemplate, Timeout: time.Second,
})
if err != nil {
t.Fatal(err)
}
return client
}