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, }, 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, 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") } }