104 lines
6.9 KiB
Go
104 lines
6.9 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"data-terminal-cloud/internal/orchestration"
|
|
"data-terminal-cloud/internal/voiceprovider"
|
|
)
|
|
|
|
type fakeProviderCallbackProcessor struct{ calls int }
|
|
|
|
func (p *fakeProviderCallbackProcessor) Process(context.Context, voiceprovider.CallbackEvent, []byte) (orchestration.ProviderEventResult, error) {
|
|
p.calls++
|
|
return orchestration.ProviderEventResult{ConversationID: "conversation-1", Status: "ACTIVE"}, nil
|
|
}
|
|
|
|
func TestProviderCallbackRequiresSignatureAndProcessesEvent(t *testing.T) {
|
|
secret := []byte("01234567890123456789012345678901")
|
|
processor := &fakeProviderCallbackProcessor{}
|
|
handler := VoiceProviderCallbackHandler(secret, time.Minute, processor)
|
|
event := voiceprovider.CallbackEvent{
|
|
Provider: "mock", EventID: "event-1", TaskRef: "task-1", ConversationID: "conversation-1",
|
|
EventType: "voice_chat.started", Status: "ACTIVE", OccurredAt: time.Now().UTC(),
|
|
}
|
|
body, _ := json.Marshal(event)
|
|
unsigned := httptest.NewRequest(http.MethodPost, "/callbacks/v1/vertc/voice-chat", strings.NewReader(string(body)))
|
|
unsignedRecorder := httptest.NewRecorder()
|
|
handler.ServeHTTP(unsignedRecorder, unsigned)
|
|
if unsignedRecorder.Code != http.StatusUnauthorized {
|
|
t.Fatalf("unsigned status=%d", unsignedRecorder.Code)
|
|
}
|
|
timestamp := time.Now().UTC().Format(time.RFC3339)
|
|
signed := httptest.NewRequest(http.MethodPost, "/callbacks/v1/vertc/voice-chat", strings.NewReader(string(body)))
|
|
signed.Header.Set(voiceprovider.HeaderCallbackTimestamp, timestamp)
|
|
signed.Header.Set(voiceprovider.HeaderCallbackSignature, voiceprovider.SignCallback(secret, timestamp, body))
|
|
recorder := httptest.NewRecorder()
|
|
handler.ServeHTTP(recorder, signed)
|
|
if recorder.Code != http.StatusOK || processor.calls != 1 || !containsText(recorder.Body.String(), "ACTIVE") {
|
|
t.Fatalf("status=%d calls=%d body=%s", recorder.Code, processor.calls, recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestProviderCallbackValidatesUsageMetrics(t *testing.T) {
|
|
validMetric := voiceprovider.UsageMetric{UsageCategory: "LLM", MetricCode: "input_tokens", Quantity: "123.456", Unit: "token"}
|
|
tests := []struct {
|
|
name string
|
|
event voiceprovider.CallbackEvent
|
|
valid bool
|
|
}{
|
|
{"non usage without metrics", voiceprovider.CallbackEvent{EventType: "voice_chat.started"}, true},
|
|
{"non usage with metrics", voiceprovider.CallbackEvent{EventType: "voice_chat.started", Usage: []voiceprovider.UsageMetric{validMetric}}, false},
|
|
{"valid usage", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{validMetric}}, true},
|
|
{"wrong status", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "ACTIVE", Usage: []voiceprovider.UsageMetric{validMetric}}, false},
|
|
{"empty metrics", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL"}, false},
|
|
{"unknown category", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{{UsageCategory: "UNKNOWN", MetricCode: "input_tokens", Quantity: "1", Unit: "token"}}}, false},
|
|
{"invalid code", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{{UsageCategory: "LLM", MetricCode: "Input", Quantity: "1", Unit: "token"}}}, false},
|
|
{"invalid unit", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{{UsageCategory: "LLM", MetricCode: "input_tokens", Quantity: "1", Unit: "Token"}}}, false},
|
|
{"invalid quantity", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{{UsageCategory: "LLM", MetricCode: "input_tokens", Quantity: "01", Unit: "token"}}}, false},
|
|
{"duplicate metric", voiceprovider.CallbackEvent{EventType: "voice_chat.usage", Status: "FINAL", Usage: []voiceprovider.UsageMetric{validMetric, validMetric}}, false},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if actual := validProviderUsage(test.event); actual != test.valid {
|
|
t.Fatalf("valid=%v want=%v event=%+v", actual, test.valid, test.event)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProviderCallbackValidatesFinalTurns(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
validTurn := voiceprovider.FinalTurn{TurnKey: "turn-1", SequenceNo: 1, Speaker: "subject", StartAt: now, EndAt: now.Add(time.Second), FinalText: "测试终稿", ProviderRef: "provider-turn-1"}
|
|
tests := []struct {
|
|
name string
|
|
event voiceprovider.CallbackEvent
|
|
valid bool
|
|
}{
|
|
{"non transcript without turns", voiceprovider.CallbackEvent{EventType: "voice_chat.started"}, true},
|
|
{"non transcript with turns", voiceprovider.CallbackEvent{EventType: "voice_chat.started", Turns: []voiceprovider.FinalTurn{validTurn}}, false},
|
|
{"valid transcript", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{validTurn}}, true},
|
|
{"wrong status", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "ACTIVE", Turns: []voiceprovider.FinalTurn{validTurn}}, false},
|
|
{"empty turns", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL"}, false},
|
|
{"missing key", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{{SequenceNo: 1, Speaker: "subject", StartAt: now, EndAt: now, FinalText: "text"}}}, false},
|
|
{"invalid sequence", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{{TurnKey: "turn", Speaker: "subject", StartAt: now, EndAt: now, FinalText: "text"}}}, false},
|
|
{"invalid speaker", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{{TurnKey: "turn", SequenceNo: 1, Speaker: "system", StartAt: now, EndAt: now, FinalText: "text"}}}, false},
|
|
{"end before start", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{{TurnKey: "turn", SequenceNo: 1, Speaker: "agent", StartAt: now, EndAt: now.Add(-time.Second), FinalText: "text"}}}, false},
|
|
{"duplicate key", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{validTurn, {TurnKey: "turn-1", SequenceNo: 2, Speaker: "agent", StartAt: now, EndAt: now, FinalText: "text"}}}, false},
|
|
{"duplicate sequence", voiceprovider.CallbackEvent{EventType: "voice_chat.transcript.final", Status: "FINAL", Turns: []voiceprovider.FinalTurn{validTurn, {TurnKey: "turn-2", SequenceNo: 1, Speaker: "agent", StartAt: now, EndAt: now, FinalText: "text"}}}, false},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if actual := validProviderTurns(test.event); actual != test.valid {
|
|
t.Fatalf("valid=%v want=%v event=%+v", actual, test.valid, test.event)
|
|
}
|
|
})
|
|
}
|
|
}
|