Files
story/internal/httpapi/provider_callbacks_test.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)
}
})
}
}