chore: establish local platform baseline

This commit is contained in:
Codex Local
2026-08-26 06:41:54 +08:00
commit 29bdf4900f
575 changed files with 95644 additions and 0 deletions
+431
View File
@@ -0,0 +1,431 @@
package mockprovider
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"sync"
"time"
"data-terminal-cloud/internal/voiceprovider"
)
type Options struct {
APIKey string
CallbackURL string
CallbackSecret string
StartDelay time.Duration
DuplicateCallbacks int
}
type task struct {
Request voiceprovider.StartRequest
Status string
}
type Server struct {
options Options
logger *slog.Logger
client *http.Client
mu sync.Mutex
tasks map[string]*task
available bool
startCalls int
stopCalls int
now func() time.Time
}
func New(options Options, logger *slog.Logger) (*Server, error) {
if len(options.APIKey) < 16 || options.CallbackURL == "" || len(options.CallbackSecret) < 32 || options.StartDelay < 0 || options.DuplicateCallbacks < 1 {
return nil, errors.New("valid mock provider options are required")
}
if logger == nil {
logger = slog.Default()
}
return &Server{options: options, logger: logger, client: &http.Client{Timeout: 5 * time.Second}, tasks: map[string]*task{}, available: true, now: time.Now}, nil
}
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /health/live", func(writer http.ResponseWriter, _ *http.Request) {
writer.Header().Set("Content-Type", "application/json")
_, _ = writer.Write([]byte(`{"status":"alive","service":"mock-provider"}`))
})
mux.HandleFunc("POST /mock/v1/voice-chats:start", s.start)
mux.HandleFunc("POST /mock/v1/voice-chats:query", s.query)
mux.HandleFunc("POST /mock/v1/voice-chats:stop", s.stop)
mux.HandleFunc("GET /mock/v1/admin/state", s.adminState)
mux.HandleFunc("POST /mock/v1/admin/availability", s.adminAvailability)
mux.HandleFunc("POST /mock/v1/admin/reset", s.adminReset)
mux.HandleFunc("POST /mock/v1/admin/replay", s.adminReplay)
return mux
}
func (s *Server) query(writer http.ResponseWriter, request *http.Request) {
if !s.authorized(request) {
http.Error(writer, "unauthorized", http.StatusUnauthorized)
return
}
var input voiceprovider.QueryRequest
decoder := json.NewDecoder(io.LimitReader(request.Body, 1<<20))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&input); err != nil || input.TaskRef == "" || input.ConversationID == "" {
http.Error(writer, "invalid request", http.StatusBadRequest)
return
}
s.mu.Lock()
available := s.available
existing := s.tasks[input.TaskRef]
s.mu.Unlock()
if !available {
http.Error(writer, "provider temporarily unavailable", http.StatusServiceUnavailable)
return
}
if existing == nil {
http.Error(writer, "task not found", http.StatusNotFound)
return
}
if existing.Request.ConversationID != input.ConversationID {
http.Error(writer, "task reference conflict", http.StatusConflict)
return
}
writer.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(writer).Encode(voiceprovider.QueryResponse{TaskRef: input.TaskRef, Status: existing.Status})
}
func (s *Server) start(writer http.ResponseWriter, request *http.Request) {
if !hmac.Equal([]byte(request.Header.Get("X-Mock-Provider-Key")), []byte(s.options.APIKey)) {
http.Error(writer, "unauthorized", http.StatusUnauthorized)
return
}
s.mu.Lock()
s.startCalls++
available := s.available
s.mu.Unlock()
if !available {
http.Error(writer, "provider temporarily unavailable", http.StatusServiceUnavailable)
return
}
decoder := json.NewDecoder(io.LimitReader(request.Body, 1<<20))
decoder.DisallowUnknownFields()
var input voiceprovider.StartRequest
if err := decoder.Decode(&input); err != nil || input.TaskRef == "" || input.RoomID == "" || input.DeviceUserID == "" || input.AgentUserID == "" || input.ConversationID == "" || !supportedEngineMode(input.EngineMode) {
http.Error(writer, "invalid request", http.StatusBadRequest)
return
}
s.mu.Lock()
existing, found := s.tasks[input.TaskRef]
if found && existing.Request != input {
s.mu.Unlock()
http.Error(writer, "task reference conflict", http.StatusConflict)
return
}
if !found {
existing = &task{Request: input, Status: "STARTING"}
s.tasks[input.TaskRef] = existing
}
status := existing.Status
s.mu.Unlock()
if !found {
go s.completeStart(input)
}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusAccepted)
_ = json.NewEncoder(writer).Encode(voiceprovider.StartResponse{TaskRef: input.TaskRef, Status: status})
}
func supportedEngineMode(mode string) bool {
return mode == "s2s_pure" || mode == "s2s_hybrid" || mode == "cascade"
}
func (s *Server) completeStart(input voiceprovider.StartRequest) {
timer := time.NewTimer(s.options.StartDelay)
defer timer.Stop()
<-timer.C
s.mu.Lock()
if existing := s.tasks[input.TaskRef]; existing != nil {
if existing.Status != "STARTING" {
s.mu.Unlock()
return
}
existing.Status = "ACTIVE"
}
s.mu.Unlock()
digest := sha256.Sum256([]byte(input.TaskRef))
event := voiceprovider.CallbackEvent{
Provider: "mock", EventID: "mock-started-" + hex.EncodeToString(digest[:12]),
TaskRef: input.TaskRef, ConversationID: input.ConversationID,
EventType: "voice_chat.started", Status: "ACTIVE", OccurredAt: s.now().UTC(),
}
body, err := json.Marshal(event)
if err != nil {
return
}
for index := 0; index < s.options.DuplicateCallbacks; index++ {
if err := s.sendCallback(body); err != nil {
s.logger.Warn("mock provider callback failed", "task_ref", input.TaskRef, "attempt", index+1, "error", err)
}
}
}
func (s *Server) stop(writer http.ResponseWriter, request *http.Request) {
if !hmac.Equal([]byte(request.Header.Get("X-Mock-Provider-Key")), []byte(s.options.APIKey)) {
http.Error(writer, "unauthorized", http.StatusUnauthorized)
return
}
s.mu.Lock()
s.stopCalls++
available := s.available
s.mu.Unlock()
if !available {
http.Error(writer, "provider temporarily unavailable", http.StatusServiceUnavailable)
return
}
decoder := json.NewDecoder(io.LimitReader(request.Body, 1<<20))
decoder.DisallowUnknownFields()
var input voiceprovider.StopRequest
if err := decoder.Decode(&input); err != nil || input.TaskRef == "" || input.ConversationID == "" || input.Reason == "" {
http.Error(writer, "invalid request", http.StatusBadRequest)
return
}
s.mu.Lock()
existing, found := s.tasks[input.TaskRef]
if !found {
s.mu.Unlock()
http.Error(writer, "task not found", http.StatusNotFound)
return
}
if existing.Request.ConversationID != input.ConversationID {
s.mu.Unlock()
http.Error(writer, "task reference conflict", http.StatusConflict)
return
}
shouldComplete := existing.Status != "STOPPING" && existing.Status != "COMPLETED"
if shouldComplete {
existing.Status = "STOPPING"
}
status := existing.Status
s.mu.Unlock()
if shouldComplete {
go s.completeStop(existing.Request)
}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusAccepted)
_ = json.NewEncoder(writer).Encode(voiceprovider.StopResponse{TaskRef: input.TaskRef, Status: status})
}
func (s *Server) adminState(writer http.ResponseWriter, request *http.Request) {
if !s.authorized(request) {
http.Error(writer, "unauthorized", http.StatusUnauthorized)
return
}
s.mu.Lock()
state := struct {
Available bool `json:"available"`
StartCalls int `json:"start_calls"`
StopCalls int `json:"stop_calls"`
Tasks int `json:"tasks"`
}{Available: s.available, StartCalls: s.startCalls, StopCalls: s.stopCalls, Tasks: len(s.tasks)}
s.mu.Unlock()
writer.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(writer).Encode(state)
}
func (s *Server) adminAvailability(writer http.ResponseWriter, request *http.Request) {
if !s.authorized(request) {
http.Error(writer, "unauthorized", http.StatusUnauthorized)
return
}
var input struct {
Available *bool `json:"available"`
}
decoder := json.NewDecoder(io.LimitReader(request.Body, 1024))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&input); err != nil || input.Available == nil {
http.Error(writer, "invalid request", http.StatusBadRequest)
return
}
s.mu.Lock()
s.available = *input.Available
s.mu.Unlock()
writer.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(writer).Encode(map[string]bool{"available": *input.Available})
}
func (s *Server) adminReset(writer http.ResponseWriter, request *http.Request) {
if !s.authorized(request) {
http.Error(writer, "unauthorized", http.StatusUnauthorized)
return
}
s.mu.Lock()
s.available = true
s.startCalls = 0
s.stopCalls = 0
s.tasks = map[string]*task{}
s.mu.Unlock()
writer.WriteHeader(http.StatusNoContent)
}
func (s *Server) adminReplay(writer http.ResponseWriter, request *http.Request) {
if !s.authorized(request) {
http.Error(writer, "unauthorized", http.StatusUnauthorized)
return
}
var input struct {
TaskRef string `json:"task_ref"`
Sample string `json:"sample"`
DuplicateCount int `json:"duplicate_count"`
}
decoder := json.NewDecoder(io.LimitReader(request.Body, 1<<20))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&input); err != nil || input.TaskRef == "" || input.Sample == "" || input.DuplicateCount < 0 || input.DuplicateCount > 10 {
http.Error(writer, "invalid request", http.StatusBadRequest)
return
}
if input.DuplicateCount == 0 {
input.DuplicateCount = 1
}
s.mu.Lock()
existing := s.tasks[input.TaskRef]
s.mu.Unlock()
if existing == nil {
http.Error(writer, "task not found", http.StatusNotFound)
return
}
event, err := s.sampleEvent(existing.Request, input.Sample)
if err != nil {
http.Error(writer, err.Error(), http.StatusBadRequest)
return
}
body, err := json.Marshal(event)
if err != nil {
http.Error(writer, "encode sample", http.StatusInternalServerError)
return
}
for attempt := 0; attempt < input.DuplicateCount; attempt++ {
if err := s.sendCallback(body); err != nil {
http.Error(writer, "callback delivery failed", http.StatusBadGateway)
return
}
}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusAccepted)
_ = json.NewEncoder(writer).Encode(map[string]any{"sample": input.Sample, "deliveries": input.DuplicateCount, "event_id": event.EventID})
}
func (s *Server) sampleEvent(input voiceprovider.StartRequest, sample string) (voiceprovider.CallbackEvent, error) {
now := s.now().UTC()
digest := sha256.Sum256([]byte("sample:" + sample + ":" + input.TaskRef))
event := voiceprovider.CallbackEvent{
Provider: "mock", EventID: "mock-sample-" + sample + "-" + hex.EncodeToString(digest[:8]),
TaskRef: input.TaskRef, ConversationID: input.ConversationID, OccurredAt: now,
}
switch sample {
case "started":
event.EventType, event.Status = "voice_chat.started", "ACTIVE"
case "final_transcript":
event.EventType, event.Status = "voice_chat.transcript.final", "FINAL"
event.Turns = []voiceprovider.FinalTurn{{TurnKey: "sample-subject-101", SequenceNo: 101, Speaker: "subject", StartAt: now.Add(-3 * time.Second), EndAt: now.Add(-2 * time.Second), FinalText: "这是脱敏的长者最终字幕样例。", ProviderRef: "sample-subject-101"}}
case "agent_answering":
event.EventType, event.Status = "voice_chat.transcript.final", "FINAL"
event.Turns = []voiceprovider.FinalTurn{{TurnKey: "sample-agent-102", SequenceNo: 102, Speaker: "agent", StartAt: now.Add(-2 * time.Second), EndAt: now.Add(-time.Second), FinalText: "这是智能体开始回答的脱敏样例。", ProviderRef: "sample-agent-102"}}
case "interrupted":
event.EventType, event.Status = "voice_chat.transcript.final", "FINAL"
event.Turns = []voiceprovider.FinalTurn{{TurnKey: "sample-agent-103", SequenceNo: 103, Speaker: "agent", StartAt: now.Add(-time.Second), EndAt: now, FinalText: "这是被打断的智能体回答样例。", Interrupted: true, ProviderRef: "sample-agent-103"}}
case "usage":
event.EventType, event.Status = "voice_chat.usage", "FINAL"
event.Usage = []voiceprovider.UsageMetric{{UsageCategory: "RTC", MetricCode: "rtc.minutes", Quantity: "1", Unit: "minutes"}}
case "error":
event.EventType, event.Status = "voice_chat.failed", "FAILED"
case "stopped":
event.EventType, event.Status = "voice_chat.stopped", "COMPLETED"
default:
return voiceprovider.CallbackEvent{}, errors.New("unknown sample")
}
return event, nil
}
func (s *Server) authorized(request *http.Request) bool {
return hmac.Equal([]byte(request.Header.Get("X-Mock-Provider-Key")), []byte(s.options.APIKey))
}
func (s *Server) completeStop(input voiceprovider.StartRequest) {
timer := time.NewTimer(s.options.StartDelay)
defer timer.Stop()
<-timer.C
s.mu.Lock()
if existing := s.tasks[input.TaskRef]; existing != nil {
existing.Status = "COMPLETED"
}
s.mu.Unlock()
s.sendFinalTranscript(input)
digest := sha256.Sum256([]byte("stop:" + input.TaskRef))
event := voiceprovider.CallbackEvent{
Provider: "mock", EventID: "mock-stopped-" + hex.EncodeToString(digest[:12]),
TaskRef: input.TaskRef, ConversationID: input.ConversationID,
EventType: "voice_chat.stopped", Status: "COMPLETED", OccurredAt: s.now().UTC(),
}
body, err := json.Marshal(event)
if err != nil {
return
}
for index := 0; index < s.options.DuplicateCallbacks; index++ {
if err := s.sendCallback(body); err != nil {
s.logger.Warn("mock provider stop callback failed", "task_ref", input.TaskRef, "attempt", index+1, "error", err)
}
}
}
func (s *Server) sendFinalTranscript(input voiceprovider.StartRequest) {
base := s.now().UTC().Add(-20 * time.Second)
digest := sha256.Sum256([]byte("transcript:" + input.TaskRef))
event := voiceprovider.CallbackEvent{
Provider: "mock", EventID: "mock-transcript-" + hex.EncodeToString(digest[:12]),
TaskRef: input.TaskRef, ConversationID: input.ConversationID,
EventType: "voice_chat.transcript.final", Status: "FINAL", OccurredAt: s.now().UTC(),
Turns: []voiceprovider.FinalTurn{
{TurnKey: "turn-1", SequenceNo: 1, Speaker: "subject", StartAt: base, EndAt: base.Add(6 * time.Second), FinalText: "我年轻时常在家乡河边散步,春天的柳树特别好看。", ProviderRef: "mock-turn-1"},
{TurnKey: "turn-2", SequenceNo: 2, Speaker: "agent", StartAt: base.Add(7 * time.Second), EndAt: base.Add(13 * time.Second), FinalText: "听起来那是一段很温暖的回忆,下次我们可以继续聊聊家乡的春天。", ProviderRef: "mock-turn-2"},
},
}
body, err := json.Marshal(event)
if err != nil {
return
}
for index := 0; index < s.options.DuplicateCallbacks; index++ {
if err := s.sendCallback(body); err != nil {
s.logger.Warn("mock provider transcript callback failed", "task_ref", input.TaskRef, "attempt", index+1, "error", err)
}
}
}
func (s *Server) sendCallback(body []byte) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
request, err := http.NewRequestWithContext(ctx, http.MethodPost, s.options.CallbackURL, bytes.NewReader(body))
if err != nil {
return err
}
timestamp := s.now().UTC().Format(time.RFC3339)
request.Header.Set("Content-Type", "application/json")
request.Header.Set(voiceprovider.HeaderCallbackTimestamp, timestamp)
request.Header.Set(voiceprovider.HeaderCallbackSignature,
voiceprovider.SignCallback([]byte(s.options.CallbackSecret), timestamp, body))
response, err := s.client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 1<<20))
if response.StatusCode < 200 || response.StatusCode >= 300 {
return errors.New("callback endpoint rejected event")
}
return nil
}
+175
View File
@@ -0,0 +1,175 @@
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())
}
}