Files
go-sip/internal/rpc/recording_delivery_integration_test.go
T

158 lines
6.6 KiB
Go

package rpc
import (
"bytes"
"context"
"encoding/json"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync/atomic"
"testing"
"time"
agentpb "git.ipao.vip/rogee/go-sip/gen/agent"
"git.ipao.vip/rogee/go-sip/internal/agent"
"git.ipao.vip/rogee/go-sip/internal/ai"
"git.ipao.vip/rogee/go-sip/internal/callflow"
"git.ipao.vip/rogee/go-sip/internal/oss"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/test/bufconn"
)
func TestRecordingDeliveryRealMutualTLSAndLocalOSSCommitsOneSQLiteResult(t *testing.T) {
server, database, _, request, snapshot := recordingRPCFixture(t)
mediaSession, err := callflow.NewRecordingSession(callflow.NewMemorySession(bytes.Repeat([]byte{1, 0}, 1600)), 4096)
if err != nil {
t.Fatal(err)
}
bound := ai.Binding{Mode: "asr_only", Conversation: ai.ConversationConfig{SilenceTimeout: 100 * time.Millisecond, MaxDuration: time.Second}}
hangup := func(context.Context) error { return nil }
mock, err := ai.NewApprovedMockPipeline(bound, ai.ApprovedMockScript{Turns: []ai.ApprovedMockTurn{{Transcript: "隔离 Mock 最终识别文本"}}}, hangup)
if err != nil {
t.Fatal(err)
}
startedAt := time.Now().UTC()
callResult, err := RunApprovedCall(context.Background(), ApprovedExecution{
AI: bound, MaxCallDuration: time.Second, TaskID: snapshot.Task.TaskID, CallerProfileID: snapshot.Task.CallerProfileID,
}, mediaSession, hangup, mock)
endedAt := time.Now().UTC()
if err != nil || len(callResult.Turns) != 1 || len(callResult.OutboundTurns) != 0 || callResult.Turns[0].Transcript != "隔离 Mock 最终识别文本" {
t.Fatalf("approved ASR-only capture changed live media or generated unwanted audio: turns=%d outbound=%d err=%v", len(callResult.Turns), len(callResult.OutboundTurns), err)
}
wav, durationMS, err := mediaSession.WAV()
if err != nil {
t.Fatal(err)
}
var puts atomic.Int32
localOSS := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
puts.Add(1)
body, err := io.ReadAll(io.LimitReader(r.Body, 4097))
if err != nil || r.Method != http.MethodPut || !strings.HasPrefix(r.URL.Path, "/mock-bucket/approved/") || !bytes.Equal(body, wav) {
t.Errorf("local OSS received an incorrect PUT: method=%q size=%d path=%q err=%v", r.Method, len(body), r.URL.Path, err)
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusCreated)
}))
defer localOSS.Close()
server.OSS, err = oss.NewClient(oss.Config{
Endpoint: localOSS.URL, Region: "cn-test", Bucket: "mock-bucket", KeyPrefix: "approved",
AccessKeyID: "isolated-test-key", AccessKeySecret: "isolated-test-secret",
GrantTTL: 15 * time.Minute, MaxAssetBytes: 4096,
})
if err != nil {
t.Fatal(err)
}
caPEM, caCert, caKey := testCertificate(t, nil, nil, true, nil, nil)
serverPEM, _, _ := testCertificate(t, caCert, caKey, false, []string{"dispatcher.local"}, nil)
clientPEM, clientCert, _ := testCertificate(t, caCert, caKey, false, []string{"agent.local"}, nil)
serverTLS, err := NewServerTLSConfig(caPEM.certPEM, serverPEM.certPEM, serverPEM.keyPEM)
if err != nil {
t.Fatal(err)
}
clientTLS, err := NewClientTLSConfig(caPEM.certPEM, clientPEM.certPEM, clientPEM.keyPEM, "dispatcher.local")
if err != nil {
t.Fatal(err)
}
server.TrustedFingerprints = map[string]struct{}{CertificateFingerprint(clientCert): {}}
listener := bufconn.Listen(1 << 20)
grpcServer := grpc.NewServer(grpc.Creds(credentials.NewTLS(serverTLS)))
agentpb.RegisterAgentControlServiceServer(grpcServer, server)
go func() { _ = grpcServer.Serve(listener) }()
t.Cleanup(func() { grpcServer.Stop(); _ = listener.Close() })
conn, err := grpc.NewClient("bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return listener.Dial() }),
grpc.WithTransportCredentials(credentials.NewTLS(clientTLS)),
)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
client := agent.RecordingClient{
Client: agentpb.NewAgentControlServiceClient(conn), DispatcherID: request.DispatcherId,
TenantID: request.TenantId, SourceEventID: request.SourceEventId,
Session: func(context.Context) (*agentpb.RequestMeta, error) { return request.Meta, nil },
}
payload, err := callflow.FinalResultPayload(callflow.FinalCallFacts{
TaskID: snapshot.Task.TaskID, CallerProfileID: snapshot.Task.CallerProfileID,
Callee: "15003164745", TrunkID: "trunk-mock", StartedAt: startedAt, EndedAt: endedAt,
Outcome: "answered", ReasonMessage: "isolated Mock media completed",
}, callResult)
if err != nil {
t.Fatal(err)
}
var result struct {
Transcript []struct {
Text string `json:"text"`
} `json:"transcript"`
}
if err := json.Unmarshal(payload, &result); err != nil || len(result.Transcript) != 1 || result.Transcript[0].Text != callResult.Turns[0].Transcript {
t.Fatalf("final result lost actual final ASR text: segments=%d err=%v", len(result.Transcript), err)
}
root := t.TempDir()
if err := os.Chmod(root, 0700); err != nil {
t.Fatal(err)
}
delivery := &agent.RecordingDelivery{
Call: client,
Recovery: &agent.RecordingRecovery{
Root: root, Now: server.Now,
Upload: agent.UploadClient{HTTPClient: localOSS.Client(), Now: server.Now},
},
}
if err := delivery.Complete(ctx, agent.CompletedRecording{
ResultPayload: payload, Expected: true, RecordingID: request.Asset.AssetId,
UploadID: request.UploadId, WAV: wav, DurationMS: durationMS,
}); err != nil {
t.Fatalf("local mutual-TLS Agent→D→OSS→SQLite recording flow failed: %v", err)
}
if puts.Load() != 1 {
t.Fatalf("approved recording PUT occurred %d times", puts.Load())
}
stored, err := database.LoadRecordingUpload(request.DispatcherId, request.SourceEventId)
if err != nil || stored.ConfirmedAt == "" || stored.SizeBytes != int64(len(wav)) || stored.Bucket != "mock-bucket" {
t.Fatalf("the confirmed original upload and result did not commit together: confirmed=%t size=%d bucket=%q err=%v", stored.ConfirmedAt != "", stored.SizeBytes, stored.Bucket, err)
}
outbox, err := database.ListPendingOutbox(request.DispatcherId)
if err != nil || len(outbox) != 2 || outbox[0].EventType != "call.execute" || outbox[1].EventType != "call.execute.result" {
t.Fatalf("execution created multiple/missing outbound facts: count=%d err=%v", len(outbox), err)
}
entries, err := os.ReadDir(root)
if err != nil || len(entries) != 0 {
t.Fatalf("successful direct upload created local business files: files=%d err=%v", len(entries), err)
}
if request.Meta.OperationId != "request-1" || request.Meta.IdempotencyKey != "request-1" {
t.Fatal("Agent session metadata was mutated by the recording flow")
}
}