Files

176 lines
7.4 KiB
Go
Raw Permalink Normal View History

2026-08-26 06:41:07 +08:00
package mockprovider
import (
"bytes"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
"data-terminal-cloud/internal/voiceprovider"
)
func TestStartIsIdempotentAndEmitsDuplicateSignedCallbacks(t *testing.T) {
secret := "01234567890123456789012345678901"
var callbacks atomic.Int32
var stoppedCallbacks atomic.Int32
var transcriptCallbacks atomic.Int32
callbackServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
body, _ := io.ReadAll(request.Body)
if err := voiceprovider.VerifyCallback([]byte(secret), request.Header.Get(voiceprovider.HeaderCallbackTimestamp), request.Header.Get(voiceprovider.HeaderCallbackSignature), body, time.Now(), time.Minute); err != nil {
t.Errorf("callback signature: %v", err)
}
callbacks.Add(1)
if bytes.Contains(body, []byte(`"voice_chat.stopped"`)) {
stoppedCallbacks.Add(1)
}
if bytes.Contains(body, []byte(`"voice_chat.transcript.final"`)) {
transcriptCallbacks.Add(1)
}
writer.WriteHeader(http.StatusNoContent)
}))
defer callbackServer.Close()
server, err := New(Options{
APIKey: "mock-provider-key", CallbackURL: callbackServer.URL, CallbackSecret: secret,
StartDelay: time.Millisecond, DuplicateCallbacks: 2,
}, slog.Default())
if err != nil {
t.Fatal(err)
}
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
client, _ := voiceprovider.NewHTTPClient(httpServer.URL, "mock-provider-key", time.Second)
input := voiceprovider.StartRequest{TaskRef: "task-1", RoomID: "room-1", DeviceUserID: "device-1", AgentUserID: "agent-1", ConversationID: "conversation-1", EngineMode: "s2s_hybrid"}
for index := 0; index < 2; index++ {
response, err := client.Start(t.Context(), input)
if err != nil || response.TaskRef != input.TaskRef {
t.Fatalf("start %d response=%#v err=%v", index, response, err)
}
}
deadline := time.Now().Add(time.Second)
for callbacks.Load() < 2 && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if callbacks.Load() != 2 {
t.Fatalf("callbacks = %d", callbacks.Load())
}
stopInput := voiceprovider.StopRequest{TaskRef: input.TaskRef, ConversationID: input.ConversationID, Reason: "handset_on_hook"}
for index := 0; index < 2; index++ {
response, err := client.Stop(t.Context(), stopInput)
if err != nil || response.TaskRef != input.TaskRef {
t.Fatalf("stop %d response=%#v err=%v", index, response, err)
}
}
deadline = time.Now().Add(time.Second)
for stoppedCallbacks.Load() < 2 && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if stoppedCallbacks.Load() != 2 || transcriptCallbacks.Load() != 2 || callbacks.Load() != 6 {
t.Fatalf("callbacks=%d transcript=%d stopped=%d", callbacks.Load(), transcriptCallbacks.Load(), stoppedCallbacks.Load())
}
request, _ := http.NewRequest(http.MethodPost, httpServer.URL+"/mock/v1/voice-chats:start", strings.NewReader(`{}`))
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != http.StatusUnauthorized {
t.Fatalf("unauthorized status = %d", response.StatusCode)
}
availability, _ := http.NewRequest(http.MethodPost, httpServer.URL+"/mock/v1/admin/availability", strings.NewReader(`{"available":false}`))
availability.Header.Set("Content-Type", "application/json")
availability.Header.Set("X-Mock-Provider-Key", "mock-provider-key")
response, err = http.DefaultClient.Do(availability)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("availability status = %d", response.StatusCode)
}
_, err = client.Start(t.Context(), voiceprovider.StartRequest{TaskRef: "task-unavailable", RoomID: "room-2", DeviceUserID: "device-2", AgentUserID: "agent-2", ConversationID: "conversation-2", EngineMode: "cascade"})
if !voiceprovider.IsTemporary(err) {
t.Fatalf("unavailable error = %v", err)
}
stateRequest, _ := http.NewRequest(http.MethodGet, httpServer.URL+"/mock/v1/admin/state", nil)
stateRequest.Header.Set("X-Mock-Provider-Key", "mock-provider-key")
response, err = http.DefaultClient.Do(stateRequest)
if err != nil {
t.Fatal(err)
}
stateBody, _ := io.ReadAll(response.Body)
response.Body.Close()
if response.StatusCode != http.StatusOK || !bytes.Contains(stateBody, []byte(`"available":false`)) || !bytes.Contains(stateBody, []byte(`"start_calls":3`)) {
t.Fatalf("state status=%d body=%s", response.StatusCode, stateBody)
}
}
func TestServerBuildsAllDeidentifiedReplaySamples(t *testing.T) {
server, err := New(Options{
APIKey: "mock-provider-key", CallbackURL: "http://127.0.0.1:1", CallbackSecret: "01234567890123456789012345678901",
StartDelay: 0, DuplicateCallbacks: 1,
}, slog.Default())
if err != nil {
t.Fatal(err)
}
request := voiceprovider.StartRequest{TaskRef: "sample-task", ConversationID: "sample-conversation"}
for _, sample := range []string{"started", "final_transcript", "agent_answering", "interrupted", "usage", "error", "stopped"} {
event, err := server.sampleEvent(request, sample)
if err != nil {
t.Fatalf("sample %s: %v", sample, err)
}
if event.TaskRef != request.TaskRef || event.ConversationID != request.ConversationID || event.EventID == "" || event.EventType == "" || event.Status == "" {
t.Fatalf("sample %s event=%#v", sample, event)
}
if sample == "interrupted" && (len(event.Turns) != 1 || !event.Turns[0].Interrupted) {
t.Fatalf("interrupted sample=%#v", event)
}
if sample == "usage" && len(event.Usage) != 1 {
t.Fatalf("usage sample=%#v", event)
}
}
if _, err := server.sampleEvent(request, "unknown"); err == nil {
t.Fatal("unknown replay sample was accepted")
}
}
func TestAdminReplayDeliversAnIdenticalSampleMultipleTimes(t *testing.T) {
secret := "01234567890123456789012345678901"
var callbacks atomic.Int32
callbackServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
body, _ := io.ReadAll(request.Body)
if err := voiceprovider.VerifyCallback([]byte(secret), request.Header.Get(voiceprovider.HeaderCallbackTimestamp), request.Header.Get(voiceprovider.HeaderCallbackSignature), body, time.Now(), time.Minute); err != nil {
t.Errorf("callback signature: %v", err)
}
callbacks.Add(1)
writer.WriteHeader(http.StatusNoContent)
}))
defer callbackServer.Close()
server, err := New(Options{APIKey: "mock-provider-key", CallbackURL: callbackServer.URL, CallbackSecret: secret, StartDelay: time.Hour, DuplicateCallbacks: 1}, slog.Default())
if err != nil {
t.Fatal(err)
}
httpServer := httptest.NewServer(server.Handler())
defer httpServer.Close()
client, _ := voiceprovider.NewHTTPClient(httpServer.URL, "mock-provider-key", time.Second)
input := voiceprovider.StartRequest{TaskRef: "replay-task", RoomID: "room", DeviceUserID: "device", AgentUserID: "agent", ConversationID: "conversation", EngineMode: "s2s_pure"}
if _, err := client.Start(t.Context(), input); err != nil {
t.Fatal(err)
}
replay, _ := http.NewRequest(http.MethodPost, httpServer.URL+"/mock/v1/admin/replay", strings.NewReader(`{"task_ref":"replay-task","sample":"interrupted","duplicate_count":2}`))
replay.Header.Set("Content-Type", "application/json")
replay.Header.Set("X-Mock-Provider-Key", "mock-provider-key")
response, err := http.DefaultClient.Do(replay)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != http.StatusAccepted || callbacks.Load() != 2 {
t.Fatalf("status=%d callbacks=%d", response.StatusCode, callbacks.Load())
}
}