158 lines
6.6 KiB
Go
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")
|
|
}
|
|
}
|