chore: establish local platform baseline

This commit is contained in:
Codex Local
2026-08-26 06:41:54 +08:00
commit 29bdf4900f
575 changed files with 95644 additions and 0 deletions
@@ -0,0 +1,890 @@
package orchestration
import (
"context"
"encoding/json"
"errors"
"os"
"strings"
"sync/atomic"
"testing"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/database"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/embeddingprovider"
"data-terminal-cloud/internal/idempotency"
platformidentity "data-terminal-cloud/internal/identity"
"data-terminal-cloud/internal/opsconfig"
"data-terminal-cloud/internal/outbox"
"data-terminal-cloud/internal/ratelimit"
"data-terminal-cloud/internal/rtc"
"data-terminal-cloud/internal/voiceprovider"
"github.com/jackc/pgx/v5/pgxpool"
)
const (
testDeviceID = "90000000-0000-0000-0000-000000000501"
testDevicePublicID = "90000000-0000-0000-0000-000000000511"
testSubjectID = "90000000-0000-0000-0000-000000000401"
testSubjectPublicID = "90000000-0000-0000-0000-000000000411"
testPriceBookID = "90000000-0000-0000-0000-000000000601"
testPersonaConfigID = "90000000-0000-0000-0000-000000000701"
testPersonaConfigPublicID = "90000000-0000-0000-0000-000000000711"
testVoiceConfigID = "90000000-0000-0000-0000-000000000702"
testVoiceConfigPublicID = "90000000-0000-0000-0000-000000000712"
testModelConfigID = "90000000-0000-0000-0000-000000000703"
testModelConfigPublicID = "90000000-0000-0000-0000-000000000713"
testRoutingConfigID = "90000000-0000-0000-0000-000000000704"
testRoutingConfigPublicID = "90000000-0000-0000-0000-000000000714"
testBundleConfigKey = "orchestration-test-agent-bundle"
testBundleCreateKey = "orchestration-agent-bundle-create"
testBundleEvaluationKey = "orchestration-agent-bundle-evaluation"
)
type testLeaseCache struct{}
func (testLeaseCache) Refresh(context.Context, LeaseRecord) error { return nil }
func (testLeaseCache) End(context.Context, string, string, time.Time) error { return nil }
type failingTokenIssuer struct{}
func (failingTokenIssuer) Issue(string, string, time.Time) (string, error) {
return "", errors.New("simulated token signing failure")
}
type countingEmbedder struct{ calls atomic.Int32 }
func (e *countingEmbedder) Embed(ctx context.Context, content string) (embeddingprovider.Result, error) {
e.calls.Add(1)
return (embeddingprovider.Local{}).Embed(ctx, content)
}
type testVoiceProvider struct {
startRequest voiceprovider.StartRequest
stopRequest voiceprovider.StopRequest
startStatus string
stopStatus string
queryStatus string
startErr error
stopErr error
queryErr error
}
func (p *testVoiceProvider) Start(_ context.Context, request voiceprovider.StartRequest) (voiceprovider.StartResponse, error) {
p.startRequest = request
if p.startErr != nil {
return voiceprovider.StartResponse{}, p.startErr
}
status := p.startStatus
if status == "" {
status = "STARTING"
}
return voiceprovider.StartResponse{TaskRef: request.TaskRef, Status: status}, nil
}
func (p *testVoiceProvider) Query(_ context.Context, request voiceprovider.QueryRequest) (voiceprovider.QueryResponse, error) {
if p.queryErr != nil {
return voiceprovider.QueryResponse{}, p.queryErr
}
status := p.queryStatus
if status == "" {
status = "STARTING"
}
return voiceprovider.QueryResponse{TaskRef: request.TaskRef, Status: status}, nil
}
func (p *testVoiceProvider) Stop(_ context.Context, request voiceprovider.StopRequest) (voiceprovider.StopResponse, error) {
p.stopRequest = request
if p.stopErr != nil {
return voiceprovider.StopResponse{}, p.stopErr
}
status := p.stopStatus
if status == "" {
status = "STOPPING"
}
return voiceprovider.StopResponse{TaskRef: request.TaskRef, Status: status}, nil
}
type allowProviderStart struct{}
func (allowProviderStart) Allow(context.Context, string, string) (ratelimit.Result, error) {
return ratelimit.Result{Allowed: true}, nil
}
func TestServiceIdempotentlyCreatesConversationWithoutPersistingToken(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := database.Open(ctx, databaseURL, "orchestration-service-test")
if err != nil {
t.Fatal(err)
}
defer pool.Close()
cleanupOrchestrationFixture(t, ctx, pool)
defer cleanupOrchestrationFixture(t, context.Background(), pool)
fixtureStatements := []struct {
query string
args []any
}{
{`INSERT INTO subject_profile (id, public_id, tenant_id, household_id, preferred_name, profile_data, conversation_preferences, status)
VALUES ($1::uuid, '90000000-0000-0000-0000-000000000411', '10000000-0000-0000-0000-000000000001', '10000000-0000-0000-0000-000000000201', '服务测试长者',
'{"background":"年轻时从事铁路工作","interests":["老照片","家乡变化"],"internal_note":"不得进入语音上下文"}',
'{"conversation_goal":"聊聊第一次参加工作的经历","pace":"slow","preferred_topics":["工作经历"]}', 'active')`, []any{testSubjectID}},
{`INSERT INTO device (id, public_id, tenant_id, serial_digest, model, status, credential_ref)
VALUES ($1::uuid, $2::uuid, '10000000-0000-0000-0000-000000000001', repeat('9',64), 'service-test', 'registered', 'test://service')`, []any{testDeviceID, testDevicePublicID}},
{`INSERT INTO device_binding (tenant_id, device_id, household_id, subject_id, reason)
VALUES ('10000000-0000-0000-0000-000000000001', $1::uuid, '10000000-0000-0000-0000-000000000201', $2::uuid, 'integration test')`, []any{testDeviceID, testSubjectID}},
{`INSERT INTO device_twin (device_id, tenant_id) VALUES ($1::uuid, '10000000-0000-0000-0000-000000000001')`, []any{testDeviceID}},
{`INSERT INTO consent_snapshot (tenant_id, subject_id, snapshot_version, effective_consents)
VALUES ('10000000-0000-0000-0000-000000000001', $1::uuid, 1, '{"voice_chat":true,"transcript":true,"audio_recording":false}'::jsonb)`, []any{testSubjectID}},
{`INSERT INTO consent_record (tenant_id,subject_id,consent_type,policy_version,notice_digest,actor_user_id)
VALUES ('10000000-0000-0000-0000-000000000001',$1::uuid,'personalized_memory','orchestration-service-test',repeat('a',64),'10000000-0000-0000-0000-000000000101')`, []any{testSubjectID}},
{`INSERT INTO runtime_config_version (
id,public_id,tenant_id,config_key,display_name,config_kind,version_no,status,payload,change_summary,
created_by,submitted_by,submitted_at,approved_by,approved_at,published_by,published_at
) VALUES (
$1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001','orchestration-test-persona','编排测试人设','PERSONA',1,'PUBLISHED',
'{"system_prompt":"你是已审核发布的陪伴助手。","tone":"warm"}'::jsonb,'编排集成测试',
'10000000-0000-0000-0000-000000000102','10000000-0000-0000-0000-000000000102','2026-08-23T11:00:00Z',
'10000000-0000-0000-0000-000000000102','2026-08-23T11:01:00Z','10000000-0000-0000-0000-000000000102','2026-08-23T11:02:00Z'
)`, []any{testPersonaConfigID, testPersonaConfigPublicID}},
{`INSERT INTO runtime_config_version (
id,public_id,tenant_id,config_key,display_name,config_kind,version_no,status,payload,change_summary,
created_by,submitted_by,submitted_at,approved_by,approved_at,published_by,published_at
) VALUES (
$1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001','orchestration-test-voice','编排测试音色','VOICE',1,'PUBLISHED',
'{"provider":"mock","voice_resource_ref":"voice://orchestration/approved"}'::jsonb,'编排集成测试',
'10000000-0000-0000-0000-000000000102','10000000-0000-0000-0000-000000000102','2026-08-23T11:00:00Z',
'10000000-0000-0000-0000-000000000102','2026-08-23T11:01:00Z','10000000-0000-0000-0000-000000000102','2026-08-23T11:02:00Z'
)`, []any{testVoiceConfigID, testVoiceConfigPublicID}},
{`INSERT INTO runtime_config_version (
id,public_id,tenant_id,config_key,display_name,config_kind,version_no,status,payload,change_summary,
created_by,submitted_by,submitted_at,approved_by,approved_at,published_by,published_at
) VALUES (
$1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001','orchestration-test-model','编排测试模型','MODEL',1,'PUBLISHED',
'{"provider":"mock","model_id":"model://orchestration/approved"}'::jsonb,'编排集成测试',
'10000000-0000-0000-0000-000000000102','10000000-0000-0000-0000-000000000102','2026-08-23T11:00:00Z',
'10000000-0000-0000-0000-000000000102','2026-08-23T11:01:00Z','10000000-0000-0000-0000-000000000102','2026-08-23T11:02:00Z'
)`, []any{testModelConfigID, testModelConfigPublicID}},
{`INSERT INTO runtime_config_version (
id,public_id,tenant_id,config_key,display_name,config_kind,version_no,status,payload,change_summary,
created_by,submitted_by,submitted_at,approved_by,approved_at,published_by,published_at
) VALUES (
$1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001','orchestration-test-routing','编排测试路由','ROUTING',1,'PUBLISHED',
'{"default_engine_mode":"cascade","routes":[{"priority":1,"provider":"mock"}]}'::jsonb,'编排集成测试',
'10000000-0000-0000-0000-000000000102','10000000-0000-0000-0000-000000000102','2026-08-23T11:00:00Z',
'10000000-0000-0000-0000-000000000102','2026-08-23T11:01:00Z','10000000-0000-0000-0000-000000000102','2026-08-23T11:02:00Z'
)`, []any{testRoutingConfigID, testRoutingConfigPublicID}},
{`INSERT INTO billing_price_book (
id,public_id,tenant_id,provider,currency,version_no,status,source_ref,change_summary,
effective_from,created_by,published_by,published_at
) VALUES (
$1::uuid,'90000000-0000-0000-0000-000000000611','10000000-0000-0000-0000-000000000001',
'mock','CNY',9001,'PUBLISHED','integration-test','provider usage integration test',
'2026-01-01T00:00:00Z','10000000-0000-0000-0000-000000000102','10000000-0000-0000-0000-000000000102','2026-01-01T00:00:00Z'
)`, []any{testPriceBookID}},
{`INSERT INTO billing_price_rate (price_book_id,usage_category,metric_code,unit,unit_price_micros)
VALUES ($1::uuid,'LLM','input_tokens','token',2)`, []any{testPriceBookID}},
}
for _, statement := range fixtureStatements {
if _, err := pool.Exec(ctx, statement.query, statement.args...); err != nil {
t.Fatal(err)
}
}
configService, err := opsconfig.NewService(pool, audit.NewRepository())
if err != nil {
t.Fatal(err)
}
configAdmin := platformidentity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000101"}
configApprover := platformidentity.Principal{Issuer: configAdmin.Issuer, Subject: "00000000-0000-0000-0000-000000000102"}
bundle, created, err := configService.Create(ctx, configAdmin, testBundleCreateKey, "orchestration-agent-bundle-create", opsconfig.CreateInput{
TenantID: "10000000-0000-0000-0000-000000000010", ConfigKey: testBundleConfigKey,
DisplayName: "编排测试智能体包", ConfigKind: "AGENT_BUNDLE", ChangeSummary: "验证运营发布的自定义版本族进入通话运行时",
Payload: map[string]any{
"persona_version_id": testPersonaConfigPublicID, "voice_version_id": testVoiceConfigPublicID,
"model_version_id": testModelConfigPublicID, "routing_version_id": testRoutingConfigPublicID,
}, SecretRefs: map[string]string{},
})
if err != nil || !created {
t.Fatalf("create Agent bundle=%+v created=%v err=%v", bundle, created, err)
}
bundle, err = configService.Submit(ctx, configAdmin, "10000000-0000-0000-0000-000000000010", bundle.ID, "orchestration-agent-bundle-submit", bundle.RowVersion)
if err != nil {
t.Fatal(err)
}
qualityMetrics := map[string]any{
"report_schema": "agent-eval-report-v1", "suite_sha256": "b5cd3b798a01a48ddf0081f2fcc2a917f93681a3ee2fa55bbf4190466de3bef8",
"agent_version": "orchestration-integration-v1", "model_config_ref": "runtime-config-version:" + testModelConfigPublicID,
"config_version_id": bundle.ID, "environment": "mock", "executed_at": "2026-08-24T10:00:00Z",
"cases_total": 10, "cases_passed": 10, "failed_check_count": 0, "conversation_success_rate": 1.0,
"first_response_p95_ms": 1200, "interruption_recovery_rate": 1.0, "average_response_chars": 36.5,
"salutation_accuracy": 1.0, "memory_accuracy": 1.0, "fabricated_facts": 0,
"story_source_completeness": 1.0, "sensitive_information_handling_rate": 1.0,
}
if _, created, err = configService.CreateEvaluation(ctx, configAdmin, "10000000-0000-0000-0000-000000000010", bundle.ID,
testBundleEvaluationKey, "orchestration-agent-bundle-evaluation", opsconfig.EvidenceTypeAgentQualityV1,
"orchestration-agent-quality-v1", "PASSED", "artifact://orchestration-agent-quality.json", qualityMetrics); err != nil || !created {
t.Fatalf("register Agent quality evidence created=%v err=%v", created, err)
}
bundle, err = configService.Publish(ctx, configApprover, "10000000-0000-0000-0000-000000000010", bundle.ID,
"orchestration-agent-bundle-publish", opsconfig.PublishInput{Reason: "标准质量证据通过后灰度到测试长者", Rollout: opsconfig.RolloutInput{
ScopeType: "SUBJECT", ScopeID: testSubjectPublicID, RolloutPercent: 100, MaxConcurrentCalls: 100,
}}, bundle.RowVersion)
if err != nil || bundle.Status != "PUBLISHED" {
t.Fatalf("publish Agent bundle=%+v err=%v", bundle, err)
}
issuer, _ := rtc.NewLocalTokenIssuer([]byte("01234567890123456789012345678901"))
service, err := NewService(
idempotency.NewExecutor(pool), NewRepository(), audit.NewRepository(), outbox.NewStore(pool), issuer,
testLeaseCache{},
ServiceOptions{RTCAppID: "test-app", EngineMode: "s2s_hybrid", AllocationTTL: 30 * time.Second, TokenTTL: 5 * time.Minute, HeartbeatInterval: 15},
)
if err != nil {
t.Fatal(err)
}
embedder := &countingEmbedder{}
if err = service.ConfigureMemoryEmbedding(pool, embedder); err != nil {
t.Fatal(err)
}
service.now = func() time.Time { return time.Date(2026, 8, 23, 12, 0, 0, 0, time.UTC) }
identity := deviceauth.Identity{ID: testDeviceID, PublicID: testDevicePublicID, TenantID: "10000000-0000-0000-0000-000000000001", Status: "registered"}
resume := true
rtcCredentialRenewal := true
input := CreateInput{DeviceTime: service.now(), Capabilities: Capabilities{RTC: true, AudioTracks: []string{"near"}, UploadResume: &resume, RTCCredentialRenewal: &rtcCredentialRenewal}, Network: Network{Type: "test"}, RequestedGoal: "open_conversation"}
body := []byte(`{"device_time":"2026-08-23T12:00:00Z","capabilities":{"rtc":true,"audio_tracks":["near"],"upload_resume":true,"rtc_credential_renewal":true},"network":{"type":"test"},"requested_goal":"open_conversation"}`)
first, replayed, err := service.Create(ctx, identity, "idempotency-test-0001", "request-1", body, input)
if err != nil || replayed || first.RTC.Token == "" {
t.Fatalf("first response=%#v replayed=%v err=%v", first, replayed, err)
}
var frozenContext, frozenBundleVersion string
if err := pool.QueryRow(ctx, `SELECT cs.sanitized_summary,cs.prompt_bundle_version FROM context_snapshot cs JOIN conversation c ON c.context_snapshot_id=cs.id WHERE c.public_id=$1::uuid`, first.ConversationID).Scan(&frozenContext, &frozenBundleVersion); err != nil {
t.Fatal(err)
}
if frozenBundleVersion != "cfg:"+bundle.ID || first.Session.EngineMode != "cascade" {
t.Fatalf("frozen bundle=%q engine=%q", frozenBundleVersion, first.Session.EngineMode)
}
for _, expected := range []string{"服务测试长者", "年轻时从事铁路工作", "老照片", "聊聊第一次参加工作的经历", "放慢语速", "工作经历"} {
if !strings.Contains(frozenContext, expected) {
t.Fatalf("frozen context %q does not contain %q", frozenContext, expected)
}
}
if strings.Contains(frozenContext, "不得进入语音上下文") || strings.Contains(frozenContext, "open_conversation") {
t.Fatalf("frozen context exposed non-whitelisted or technical fields: %q", frozenContext)
}
second, replayed, err := service.Create(ctx, identity, "idempotency-test-0001", "request-2", body, input)
if err != nil || !replayed || second.ConversationID != first.ConversationID || second.RTC.Token != first.RTC.Token {
t.Fatalf("second response=%#v replayed=%v err=%v", second, replayed, err)
}
if _, _, err := service.Create(ctx, identity, "idempotency-test-0001", "request-3", append(body, ' '), input); !errors.Is(err, idempotency.ErrKeyConflict) {
t.Fatalf("conflicting body error = %v", err)
}
if embedder.calls.Load() != 1 {
t.Fatalf("embedding calls=%d want=1; sequential replay and body conflict must not repeat provider billing", embedder.calls.Load())
}
var conversations, histories, audits, events int
var tokenPersisted bool
if err := pool.QueryRow(ctx, "SELECT count(*) FROM conversation WHERE device_id=$1::uuid", testDeviceID).Scan(&conversations); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM conversation_state_history h JOIN conversation c ON c.id=h.conversation_id WHERE c.device_id=$1::uuid`, testDeviceID).Scan(&histories); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, "SELECT count(*) FROM audit_event WHERE actor_public_id=$1::uuid AND action='conversation.create'", testDevicePublicID).Scan(&audits); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM outbox_event o JOIN conversation c ON c.id=o.aggregate_id WHERE c.device_id=$1::uuid AND o.event_type='conversation.room_issued'`, testDeviceID).Scan(&events); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM idempotency_record WHERE actor_key=$1 AND response_body::text LIKE '%dev1.%')`, "device:"+testDevicePublicID).Scan(&tokenPersisted); err != nil {
t.Fatal(err)
}
if conversations != 1 || histories != 2 || audits != 1 || events != 1 || tokenPersisted {
t.Fatalf("conversation=%d history=%d audit=%d event=%d tokenPersisted=%v", conversations, histories, audits, events, tokenPersisted)
}
activateInput := ActivateInput{
JoinedAt: service.now().Add(time.Second), RoomID: first.RTC.RoomID, RTCUserID: first.RTC.UserID,
RTCJoinResult: "success", ConfigRevision: 1,
}
activateBody, _ := json.Marshal(activateInput)
activated, replayed, err := service.Activate(ctx, identity, first.ConversationID, "activate-idempotency-0001", "activation-request", activateBody, activateInput)
if err != nil || replayed || activated.Status != "STARTING_AGENT" {
t.Fatalf("activation=%#v replayed=%v err=%v", activated, replayed, err)
}
replayedActivation, replayed, err := service.Activate(ctx, identity, first.ConversationID, "activate-idempotency-0001", "activation-request-replay", activateBody, activateInput)
if err != nil || !replayed || replayedActivation != activated {
t.Fatalf("replayed activation=%#v replayed=%v err=%v", replayedActivation, replayed, err)
}
var conversationInternalID, providerTaskRef string
if err := pool.QueryRow(ctx, `SELECT id::text, provider_task_ref FROM conversation WHERE public_id=$1::uuid`, first.ConversationID).Scan(&conversationInternalID, &providerTaskRef); err != nil {
t.Fatal(err)
}
providerClient := &testVoiceProvider{}
providerHealth, err := NewProviderHealth(pool, 3, time.Minute)
if err != nil {
t.Fatal(err)
}
startProcessor, err := NewStartProcessor(pool, providerClient, providerHealth, 75*time.Second)
if err != nil {
t.Fatal(err)
}
startProcessor.now = func() time.Time { return service.now().Add(2 * time.Second) }
if err := startProcessor.Handle(ctx, outbox.Event{AggregateID: conversationInternalID}); err != nil {
t.Fatal(err)
}
if providerClient.startRequest.TaskRef != providerTaskRef || providerClient.startRequest.ConversationID != first.ConversationID || providerClient.startRequest.RoomID != first.RTC.RoomID || providerClient.startRequest.EngineMode != first.Session.EngineMode {
t.Fatalf("provider start request=%#v", providerClient.startRequest)
}
if providerClient.startRequest.ContextSummary != frozenContext {
t.Fatalf("provider context=%q want frozen context=%q", providerClient.startRequest.ContextSummary, frozenContext)
}
if providerClient.startRequest.PromptBundleVersion != frozenBundleVersion ||
providerClient.startRequest.AgentBundleVersion != bundle.ID ||
providerClient.startRequest.PersonaVersion != testPersonaConfigPublicID ||
providerClient.startRequest.VoiceVersion != testVoiceConfigPublicID ||
providerClient.startRequest.ModelVersion != testModelConfigPublicID ||
providerClient.startRequest.RoutingVersion != testRoutingConfigPublicID ||
providerClient.startRequest.SystemPrompt != "你是已审核发布的陪伴助手。" ||
providerClient.startRequest.PersonaTone != "warm" ||
providerClient.startRequest.VoiceResourceRef != "voice://orchestration/approved" ||
providerClient.startRequest.ModelResourceRef != "model://orchestration/approved" {
t.Fatalf("provider bundle request=%#v", providerClient.startRequest)
}
processor, _ := NewProviderEventProcessor(pool, 75*time.Second)
processor.now = func() time.Time { return service.now().Add(3 * time.Second) }
callback := voiceprovider.CallbackEvent{
Provider: "mock", EventID: "provider-event-started-1", TaskRef: providerTaskRef,
ConversationID: first.ConversationID, EventType: "voice_chat.started", Status: "ACTIVE",
OccurredAt: service.now().Add(2500 * time.Millisecond),
}
rawCallback, _ := json.Marshal(callback)
processed, err := processor.Process(ctx, callback, rawCallback)
if err != nil || processed.Status != "ACTIVE" || processed.Duplicate || processed.Ignored {
t.Fatalf("processed=%#v err=%v", processed, err)
}
duplicateCallback, err := processor.Process(ctx, callback, rawCallback)
if err != nil || !duplicateCallback.Duplicate || duplicateCallback.Status != "ACTIVE" {
t.Fatalf("duplicate=%#v err=%v", duplicateCallback, err)
}
conflictingCallback := callback
conflictingCallback.Status = "FAILED"
conflictingBody, _ := json.Marshal(conflictingCallback)
if _, err := processor.Process(ctx, conflictingCallback, conflictingBody); !errors.Is(err, ErrProviderEventConflict) {
t.Fatalf("conflicting callback error=%v", err)
}
callback.EventID = "provider-event-started-late"
lateBody, _ := json.Marshal(callback)
lateCallback, err := processor.Process(ctx, callback, lateBody)
if err != nil || !lateCallback.Ignored || lateCallback.Status != "ACTIVE" {
t.Fatalf("late=%#v err=%v", lateCallback, err)
}
var finalStatus, finalTaskStatus string
var providerEvents int
if err := pool.QueryRow(ctx, `SELECT c.status, r.status FROM conversation c JOIN rtc_task r ON r.conversation_id=c.id WHERE c.public_id=$1::uuid`, first.ConversationID).Scan(&finalStatus, &finalTaskStatus); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM provider_event e JOIN conversation c ON c.id=e.conversation_id WHERE c.public_id=$1::uuid`, first.ConversationID).Scan(&providerEvents); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM conversation_state_history h JOIN conversation c ON c.id=h.conversation_id WHERE c.public_id=$1::uuid`, first.ConversationID).Scan(&histories); err != nil {
t.Fatal(err)
}
if finalStatus != "ACTIVE" || finalTaskStatus != "ACTIVE" || providerEvents != 2 || histories != 4 {
t.Fatalf("conversation=%s task=%s events=%d history=%d", finalStatus, finalTaskStatus, providerEvents, histories)
}
heartbeatOptions := HeartbeatServiceOptions{
RTCAppID: "test-app", LeaseTTL: time.Minute, TokenTTL: 5 * time.Minute, CredentialRenewalWindow: 30 * time.Second,
}
heartbeatService, err := NewHeartbeatService(pool, NewRepository(), testLeaseCache{}, issuer, heartbeatOptions)
if err != nil {
t.Fatal(err)
}
heartbeatService.now = func() time.Time { return time.Date(2026, 8, 23, 12, 0, 3, 500*1e6, time.UTC) }
oldCredentialExpiry := heartbeatService.now().Add(20 * time.Second)
if _, err := pool.Exec(ctx, `UPDATE rtc_task SET credential_issued_at=$2,credential_expires_at=$3 WHERE conversation_id=$1::uuid`, conversationInternalID, service.now(), oldCredentialExpiry); err != nil {
t.Fatal(err)
}
monotonicMS := int64(12345)
heartbeatInput := HeartbeatInput{SequenceNo: 1, DeviceTime: heartbeatService.now(), MonotonicMS: &monotonicMS, ConversationStatus: "ACTIVE", RTC: RTCHeartbeat{Joined: true, Uplink: true, Downlink: true}, Network: Network{Type: "wifi"}, FirmwareVersion: "1.0.0", ReportedConfigRevision: 1, RecordingQueue: RecordingQueue{}, RTCCredentialExpiresAt: &oldCredentialExpiry, CommandResults: []json.RawMessage{}}
failingHeartbeatService, err := NewHeartbeatService(pool, NewRepository(), testLeaseCache{}, failingTokenIssuer{}, heartbeatOptions)
if err != nil {
t.Fatal(err)
}
failingHeartbeatService.now = heartbeatService.now
if _, err = pool.Exec(ctx, `UPDATE conversation SET rtc_credential_renewal_supported=false WHERE id=$1::uuid`, conversationInternalID); err != nil {
t.Fatal(err)
}
legacyHeartbeat, err := failingHeartbeatService.Heartbeat(ctx, identity, first.ConversationID, heartbeatInput)
if err != nil || len(legacyHeartbeat.Actions) != 1 || legacyHeartbeat.Actions[0].Type != "continue" {
t.Fatalf("legacy heartbeat=%#v err=%v", legacyHeartbeat, err)
}
var legacyExpiry time.Time
if err = pool.QueryRow(ctx, `SELECT credential_expires_at FROM rtc_task WHERE conversation_id=$1::uuid`, conversationInternalID).Scan(&legacyExpiry); err != nil {
t.Fatal(err)
}
if !legacyExpiry.Equal(oldCredentialExpiry) {
t.Fatalf("legacy client rotated credential metadata: expiry=%s", legacyExpiry)
}
if _, err = pool.Exec(ctx, `UPDATE conversation SET rtc_credential_renewal_supported=true,last_heartbeat_seq=0 WHERE id=$1::uuid`, conversationInternalID); err != nil {
t.Fatal(err)
}
if _, err = failingHeartbeatService.Heartbeat(ctx, identity, first.ConversationID, heartbeatInput); err == nil {
t.Fatal("credential signing failure did not abort heartbeat")
}
var expiryAfterFailure time.Time
var sequenceAfterFailure int64
if err = pool.QueryRow(ctx, `SELECT r.credential_expires_at,c.last_heartbeat_seq FROM rtc_task r JOIN conversation c ON c.id=r.conversation_id WHERE c.id=$1::uuid`, conversationInternalID).Scan(&expiryAfterFailure, &sequenceAfterFailure); err != nil {
t.Fatal(err)
}
if !expiryAfterFailure.Equal(oldCredentialExpiry) || sequenceAfterFailure != 0 {
t.Fatalf("failed renewal committed metadata expiry=%s sequence=%d", expiryAfterFailure, sequenceAfterFailure)
}
heartbeat, err := heartbeatService.Heartbeat(ctx, identity, first.ConversationID, heartbeatInput)
if err != nil || heartbeat.LeaseExpiresAt == nil || len(heartbeat.Actions) != 2 || heartbeat.Actions[0].Type != "continue" || heartbeat.Actions[1].Type != "renew_rtc_credential" || heartbeat.Actions[1].RTC == nil || heartbeat.Actions[1].RTC.Token == "" || !heartbeat.Actions[1].RTC.ExpiresAt.Equal(heartbeatService.now().Add(5*time.Minute)) {
t.Fatalf("heartbeat=%#v err=%v", heartbeat, err)
}
staleHeartbeat, err := heartbeatService.Heartbeat(ctx, identity, first.ConversationID, heartbeatInput)
if err != nil || staleHeartbeat.LeaseExpiresAt == nil || !staleHeartbeat.LeaseExpiresAt.Equal(*heartbeat.LeaseExpiresAt) || len(staleHeartbeat.Actions) != 2 || staleHeartbeat.Actions[1].RTC == nil || staleHeartbeat.Actions[1].RTC.Token != heartbeat.Actions[1].RTC.Token {
t.Fatalf("stale heartbeat=%#v err=%v", staleHeartbeat, err)
}
heartbeatInput.SequenceNo = 2
heartbeatInput.RTCCredentialExpiresAt = &heartbeat.Actions[1].RTC.ExpiresAt
heartbeatInput.ReportedConfigRevision = 2
if _, err := heartbeatService.Heartbeat(ctx, identity, first.ConversationID, heartbeatInput); !errors.Is(err, ErrConfigVersionConflict) {
t.Fatalf("future heartbeat configuration error=%v", err)
}
service.now = func() time.Time { return time.Date(2026, 8, 23, 12, 0, 4, 0, time.UTC) }
stopInput := StopInput{
Reason: "handset_on_hook", DeviceTime: service.now(), LastHeartbeatSequence: 2,
RTCLeft: true, Recording: StopRecording{NearSegments: 1, FarSegments: 1},
}
stopBody := []byte(`{"reason":"handset_on_hook","device_time":"2026-08-23T12:00:04Z","last_heartbeat_sequence":2,"rtc_left":true,"recording":{"near_segments":1,"far_segments":1}}`)
stopped, replayed, err := service.Stop(ctx, identity, first.ConversationID, "stop-idempotency-0001", "stop-request-1", stopBody, stopInput)
if err != nil || replayed || stopped.Status != "STOPPING" {
t.Fatalf("stop=%#v replayed=%v err=%v", stopped, replayed, err)
}
replayedStop, replayed, err := service.Stop(ctx, identity, first.ConversationID, "stop-idempotency-0001", "stop-request-2", stopBody, stopInput)
if err != nil || !replayed || replayedStop.Status != "STOPPING" {
t.Fatalf("replayed stop=%#v replayed=%v err=%v", replayedStop, replayed, err)
}
var stopEvents, stopAudits int
if err := pool.QueryRow(ctx, `SELECT count(*) FROM outbox_event o JOIN conversation c ON c.id=o.aggregate_id
WHERE c.public_id=$1::uuid AND o.event_type='conversation.agent_stop_requested'`, first.ConversationID).Scan(&stopEvents); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM audit_event a JOIN conversation c ON c.id=a.resource_id
WHERE c.public_id=$1::uuid AND a.action='conversation.stop'`, first.ConversationID).Scan(&stopAudits); err != nil {
t.Fatal(err)
}
if stopEvents != 1 || stopAudits != 1 {
t.Fatalf("stop events=%d audits=%d", stopEvents, stopAudits)
}
stopProcessor, err := NewStopProcessor(pool, providerClient, providerHealth)
if err != nil {
t.Fatal(err)
}
stopProcessor.now = func() time.Time { return service.now().Add(500 * time.Millisecond) }
if err := stopProcessor.Handle(ctx, outbox.Event{AggregateID: conversationInternalID}); err != nil {
t.Fatal(err)
}
if providerClient.stopRequest.TaskRef != providerTaskRef || providerClient.stopRequest.ConversationID != first.ConversationID || providerClient.stopRequest.Reason != stopInput.Reason {
t.Fatalf("provider stop request=%#v", providerClient.stopRequest)
}
processor.now = func() time.Time { return time.Date(2026, 8, 23, 12, 0, 5, 0, time.UTC) }
stopCallback := voiceprovider.CallbackEvent{
Provider: "mock", EventID: "provider-event-stopped-1", TaskRef: providerTaskRef,
ConversationID: first.ConversationID, EventType: "voice_chat.stopped", Status: "COMPLETED",
OccurredAt: service.now().Add(time.Second),
}
rawStopCallback, _ := json.Marshal(stopCallback)
completed, err := processor.Process(ctx, stopCallback, rawStopCallback)
if err != nil || completed.Status != "COMPLETED" || completed.Ignored {
t.Fatalf("completed=%#v err=%v", completed, err)
}
duplicateStop, err := processor.Process(ctx, stopCallback, rawStopCallback)
if err != nil || !duplicateStop.Duplicate || duplicateStop.Status != "COMPLETED" {
t.Fatalf("duplicate stop=%#v err=%v", duplicateStop, err)
}
if err := pool.QueryRow(ctx, `SELECT c.status, r.status FROM conversation c JOIN rtc_task r ON r.conversation_id=c.id
WHERE c.public_id=$1::uuid`, first.ConversationID).Scan(&finalStatus, &finalTaskStatus); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM conversation_state_history h JOIN conversation c ON c.id=h.conversation_id
WHERE c.public_id=$1::uuid`, first.ConversationID).Scan(&histories); err != nil {
t.Fatal(err)
}
if finalStatus != "COMPLETED" || finalTaskStatus != "COMPLETED" || histories != 6 {
t.Fatalf("final conversation=%s task=%s history=%d", finalStatus, finalTaskStatus, histories)
}
// Exercise the synchronous ACTIVE/COMPLETED provider responses in addition
// to the asynchronous callback path above. This is the other supported
// provider contract and must preserve the same durable state transitions.
secondConversation, replayed, err := service.Create(ctx, identity, "idempotency-test-0002", "request-4", body, input)
if err != nil || replayed {
t.Fatalf("second conversation=%#v replayed=%v err=%v", secondConversation, replayed, err)
}
secondActivateInput := activateInput
secondActivateInput.RoomID = secondConversation.RTC.RoomID
secondActivateInput.RTCUserID = secondConversation.RTC.UserID
secondActivateBody, _ := json.Marshal(secondActivateInput)
if _, replayed, err := service.Activate(ctx, identity, secondConversation.ConversationID, "activate-idempotency-0002", "activation-request-2", secondActivateBody, secondActivateInput); err != nil || replayed {
t.Fatalf("second activation replayed=%v err=%v", replayed, err)
}
var secondInternalID string
if err := pool.QueryRow(ctx, `SELECT id::text FROM conversation WHERE public_id=$1::uuid`, secondConversation.ConversationID).Scan(&secondInternalID); err != nil {
t.Fatal(err)
}
providerClient.startStatus = "ACTIVE"
secondStartProcessor, err := NewStartProcessor(pool, providerClient, providerHealth, 75*time.Second)
if err != nil {
t.Fatal(err)
}
if err := secondStartProcessor.ConfigureStartProtection(allowProviderStart{}, 100); err != nil {
t.Fatal(err)
}
secondStartProcessor.now = func() time.Time { return service.now().Add(2 * time.Minute) }
if err := secondStartProcessor.Handle(ctx, outbox.Event{AggregateID: secondInternalID}); err != nil {
t.Fatal(err)
}
secondStopInput := stopInput
secondStopInput.DeviceTime = service.now().Add(2*time.Minute + time.Second)
secondStopBody, _ := json.Marshal(secondStopInput)
if _, replayed, err := service.Stop(ctx, identity, secondConversation.ConversationID, "stop-idempotency-0002", "stop-request-3", secondStopBody, secondStopInput); err != nil || replayed {
t.Fatalf("second stop replayed=%v err=%v", replayed, err)
}
providerClient.stopStatus = "COMPLETED"
secondStopProcessor, err := NewStopProcessor(pool, providerClient, providerHealth)
if err != nil {
t.Fatal(err)
}
secondStopProcessor.now = func() time.Time { return service.now().Add(2*time.Minute + 2*time.Second) }
if err := secondStopProcessor.Handle(ctx, outbox.Event{AggregateID: secondInternalID}); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT c.status, r.status FROM conversation c JOIN rtc_task r ON r.conversation_id=c.id WHERE c.id=$1::uuid`, secondInternalID).Scan(&finalStatus, &finalTaskStatus); err != nil {
t.Fatal(err)
}
if finalStatus != "COMPLETED" || finalTaskStatus != "COMPLETED" {
t.Fatalf("second final conversation=%s task=%s", finalStatus, finalTaskStatus)
}
usageCallback := voiceprovider.CallbackEvent{
Provider: "mock", EventID: "provider-event-usage-1", TaskRef: providerTaskRef,
ConversationID: first.ConversationID, EventType: "voice_chat.usage", Status: "FINAL",
OccurredAt: service.now().Add(3 * time.Minute), Usage: []voiceprovider.UsageMetric{{
UsageCategory: "LLM", MetricCode: "input_tokens", Quantity: "12", Unit: "token",
}},
}
usageBody, _ := json.Marshal(usageCallback)
usageResult, err := processor.Process(ctx, usageCallback, usageBody)
if err != nil || usageResult.UsageAccepted != 1 {
t.Fatalf("provider usage result=%#v err=%v", usageResult, err)
}
// A provider can end an active call before a device stop request arrives.
// Verify that callback-only termination creates both durable transitions and
// schedules content extraction.
providerEndedConversation, replayed, err := service.Create(ctx, identity, "idempotency-test-0003", "request-5", body, input)
if err != nil || replayed {
t.Fatalf("provider-ended conversation=%#v replayed=%v err=%v", providerEndedConversation, replayed, err)
}
providerEndedActivate := activateInput
providerEndedActivate.RoomID = providerEndedConversation.RTC.RoomID
providerEndedActivate.RTCUserID = providerEndedConversation.RTC.UserID
providerEndedActivateBody, _ := json.Marshal(providerEndedActivate)
if _, replayed, err := service.Activate(ctx, identity, providerEndedConversation.ConversationID, "activate-idempotency-0003", "activation-request-3", providerEndedActivateBody, providerEndedActivate); err != nil || replayed {
t.Fatalf("provider-ended activation replayed=%v err=%v", replayed, err)
}
var providerEndedInternalID, providerEndedTaskRef string
if err := pool.QueryRow(ctx, `SELECT id::text,provider_task_ref FROM conversation WHERE public_id=$1::uuid`, providerEndedConversation.ConversationID).Scan(&providerEndedInternalID, &providerEndedTaskRef); err != nil {
t.Fatal(err)
}
if err := secondStartProcessor.Handle(ctx, outbox.Event{AggregateID: providerEndedInternalID}); err != nil {
t.Fatal(err)
}
providerEndedCallback := voiceprovider.CallbackEvent{
Provider: "mock", EventID: "provider-event-ended-active", TaskRef: providerEndedTaskRef,
ConversationID: providerEndedConversation.ConversationID, EventType: "voice_chat.stopped", Status: "COMPLETED",
OccurredAt: service.now().Add(4 * time.Minute),
}
providerEndedBody, _ := json.Marshal(providerEndedCallback)
providerEndedResult, err := processor.Process(ctx, providerEndedCallback, providerEndedBody)
if err != nil || providerEndedResult.Status != "COMPLETED" || providerEndedResult.Ignored {
t.Fatalf("provider-ended result=%#v err=%v", providerEndedResult, err)
}
// A runtime failure moves an active conversation to STOPPING and repeated
// failure delivery remains safe while retaining the first failure cause.
failedConversation, replayed, err := service.Create(ctx, identity, "idempotency-test-0004", "request-6", body, input)
if err != nil || replayed {
t.Fatalf("failed conversation=%#v replayed=%v err=%v", failedConversation, replayed, err)
}
failedActivate := activateInput
failedActivate.RoomID = failedConversation.RTC.RoomID
failedActivate.RTCUserID = failedConversation.RTC.UserID
failedActivateBody, _ := json.Marshal(failedActivate)
if _, replayed, err := service.Activate(ctx, identity, failedConversation.ConversationID, "activate-idempotency-0004", "activation-request-4", failedActivateBody, failedActivate); err != nil || replayed {
t.Fatalf("failed activation replayed=%v err=%v", replayed, err)
}
var failedInternalID, failedTaskRef string
if err := pool.QueryRow(ctx, `SELECT id::text,provider_task_ref FROM conversation WHERE public_id=$1::uuid`, failedConversation.ConversationID).Scan(&failedInternalID, &failedTaskRef); err != nil {
t.Fatal(err)
}
if err := secondStartProcessor.Handle(ctx, outbox.Event{AggregateID: failedInternalID}); err != nil {
t.Fatal(err)
}
failedCallback := voiceprovider.CallbackEvent{
Provider: "mock", EventID: "provider-event-runtime-failed-1", TaskRef: failedTaskRef,
ConversationID: failedConversation.ConversationID, EventType: "voice_chat.failed", Status: "FAILED",
OccurredAt: service.now().Add(5 * time.Minute),
}
failedBody, _ := json.Marshal(failedCallback)
failedResult, err := processor.Process(ctx, failedCallback, failedBody)
if err != nil || failedResult.Status != "STOPPING" || failedResult.Ignored {
t.Fatalf("runtime failure result=%#v err=%v", failedResult, err)
}
failedCallback.EventID = "provider-event-runtime-failed-2"
repeatedFailedBody, _ := json.Marshal(failedCallback)
repeatedFailedResult, err := processor.Process(ctx, failedCallback, repeatedFailedBody)
if err != nil || repeatedFailedResult.Status != "STOPPING" || repeatedFailedResult.Ignored {
t.Fatalf("repeated runtime failure result=%#v err=%v", repeatedFailedResult, err)
}
failedStopProcessor, err := NewStopProcessor(pool, providerClient, providerHealth)
if err != nil {
t.Fatal(err)
}
providerClient.stopErr = &voiceprovider.Error{Operation: "stop", Temporary: true, Cause: errors.New("temporary test failure")}
if err := failedStopProcessor.Handle(ctx, outbox.Event{AggregateID: failedInternalID}); err == nil {
t.Fatal("temporary provider stop failure was not retried")
}
providerClient.stopErr = nil
providerClient.stopStatus = "COMPLETED"
if err := failedStopProcessor.Handle(ctx, outbox.Event{AggregateID: failedInternalID}); err != nil {
t.Fatal(err)
}
// Reconciliation after an ambiguous start must query first. A still-starting
// response is retried, a confirmed 404 permits the same stable task to be
// started again, and a later ACTIVE query closes the ambiguity safely.
reconciledConversation, replayed, err := service.Create(ctx, identity, "idempotency-test-0005", "request-7", body, input)
if err != nil || replayed {
t.Fatalf("reconciled conversation=%#v replayed=%v err=%v", reconciledConversation, replayed, err)
}
reconciledActivate := activateInput
reconciledActivate.RoomID = reconciledConversation.RTC.RoomID
reconciledActivate.RTCUserID = reconciledConversation.RTC.UserID
reconciledActivateBody, _ := json.Marshal(reconciledActivate)
if _, replayed, err := service.Activate(ctx, identity, reconciledConversation.ConversationID, "activate-idempotency-0005", "activation-request-5", reconciledActivateBody, reconciledActivate); err != nil || replayed {
t.Fatalf("reconciled activation replayed=%v err=%v", replayed, err)
}
var reconciledInternalID string
if err := pool.QueryRow(ctx, `SELECT id::text FROM conversation WHERE public_id=$1::uuid`, reconciledConversation.ConversationID).Scan(&reconciledInternalID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `UPDATE rtc_task SET status='UNKNOWN' WHERE conversation_id=$1::uuid`, reconciledInternalID); err != nil {
t.Fatal(err)
}
providerClient.queryStatus = "STARTING"
if err := secondStartProcessor.Handle(ctx, outbox.Event{AggregateID: reconciledInternalID}); err == nil {
t.Fatal("still-starting reconciliation was not retried")
}
if _, err := pool.Exec(ctx, `UPDATE rtc_task SET status='UNKNOWN' WHERE conversation_id=$1::uuid`, reconciledInternalID); err != nil {
t.Fatal(err)
}
providerClient.queryErr = &voiceprovider.Error{Operation: "query", StatusCode: 404}
providerClient.startErr = &voiceprovider.Error{Operation: "start", Temporary: true, Cause: errors.New("ambiguous start")}
if err := secondStartProcessor.Handle(ctx, outbox.Event{AggregateID: reconciledInternalID}); err == nil {
t.Fatal("temporary provider start failure was not retried")
}
providerClient.queryErr = nil
providerClient.startErr = nil
providerClient.queryStatus = "ACTIVE"
if err := secondStartProcessor.Handle(ctx, outbox.Event{AggregateID: reconciledInternalID}); err != nil {
t.Fatal(err)
}
reconciledStopInput := stopInput
reconciledStopInput.DeviceTime = service.now().Add(6*time.Minute + time.Second)
reconciledStopBody, _ := json.Marshal(reconciledStopInput)
if _, replayed, err := service.Stop(ctx, identity, reconciledConversation.ConversationID, "stop-idempotency-0005", "stop-request-5", reconciledStopBody, reconciledStopInput); err != nil || replayed {
t.Fatalf("reconciled stop replayed=%v err=%v", replayed, err)
}
if err := failedStopProcessor.Handle(ctx, outbox.Event{AggregateID: reconciledInternalID}); err != nil {
t.Fatal(err)
}
// A permanent provider start rejection is not retryable. It must close the
// authoritative state immediately so the device can leave the RTC room with
// a stable, non-sensitive failure code instead of timing out in STARTING_AGENT.
rejectedConversation, replayed, err := service.Create(ctx, identity, "idempotency-test-0006", "request-8", body, input)
if err != nil || replayed {
t.Fatalf("rejected conversation=%#v replayed=%v err=%v", rejectedConversation, replayed, err)
}
rejectedActivate := activateInput
rejectedActivate.RoomID = rejectedConversation.RTC.RoomID
rejectedActivate.RTCUserID = rejectedConversation.RTC.UserID
rejectedActivateBody, _ := json.Marshal(rejectedActivate)
if _, replayed, err = service.Activate(ctx, identity, rejectedConversation.ConversationID, "activate-idempotency-0006", "activation-request-6", rejectedActivateBody, rejectedActivate); err != nil || replayed {
t.Fatalf("rejected activation replayed=%v err=%v", replayed, err)
}
var rejectedInternalID string
if err = pool.QueryRow(ctx, `SELECT id::text FROM conversation WHERE public_id=$1::uuid`, rejectedConversation.ConversationID).Scan(&rejectedInternalID); err != nil {
t.Fatal(err)
}
providerClient.startErr = &voiceprovider.Error{Operation: "start", StatusCode: 400, Cause: errors.New("invalid provider request")}
if err = secondStartProcessor.Handle(ctx, outbox.Event{AggregateID: rejectedInternalID}); err != nil {
t.Fatalf("permanent provider rejection was not durably consumed: %v", err)
}
providerClient.startErr = nil
rejectedState, err := service.GetDeviceState(ctx, identity, rejectedConversation.ConversationID)
if err != nil || rejectedState.Status != "FAILED" || rejectedState.Action != "leave_room" || rejectedState.FailureCode != "provider_start_rejected" {
t.Fatalf("rejected device state=%#v err=%v", rejectedState, err)
}
var rejectedTaskStatus string
var rejectedHistory, rejectedAudits int
if err = pool.QueryRow(ctx, `SELECT status FROM rtc_task WHERE conversation_id=$1::uuid`, rejectedInternalID).Scan(&rejectedTaskStatus); err != nil {
t.Fatal(err)
}
if err = pool.QueryRow(ctx, `SELECT count(*) FROM conversation_state_history WHERE conversation_id=$1::uuid AND from_status='STARTING_AGENT' AND to_status='FAILED'`, rejectedInternalID).Scan(&rejectedHistory); err != nil {
t.Fatal(err)
}
if err = pool.QueryRow(ctx, `SELECT count(*) FROM audit_event WHERE resource_id=$1::uuid AND action='conversation.agent_start_failed'`, rejectedInternalID).Scan(&rejectedAudits); err != nil {
t.Fatal(err)
}
if rejectedTaskStatus != "FAILED" || rejectedHistory != 1 || rejectedAudits != 1 {
t.Fatalf("rejected task=%s history=%d audits=%d", rejectedTaskStatus, rejectedHistory, rejectedAudits)
}
transcriptService, err := NewTranscriptBatchService(pool, idempotency.NewExecutor(pool), audit.NewRepository(), outbox.NewStore(pool))
if err != nil {
t.Fatal(err)
}
providerTurn := TranscriptTurnInput{
TurnKey: "provider-final-1", SequenceNo: 1, Speaker: "subject",
StartAt: time.Date(2026, 8, 23, 12, 0, 1, 0, time.UTC), EndAt: time.Date(2026, 8, 23, 12, 0, 2, 0, time.UTC),
FinalText: "供应商已经返回的终稿", Interrupted: false,
}
lateTranscript := voiceprovider.CallbackEvent{
Provider: "mock", EventID: "provider-event-transcript-late-1", TaskRef: providerTaskRef,
ConversationID: first.ConversationID, EventType: "voice_chat.transcript.final", Status: "FINAL",
OccurredAt: service.now().Add(2 * time.Second), Turns: []voiceprovider.FinalTurn{{
TurnKey: providerTurn.TurnKey, SequenceNo: providerTurn.SequenceNo, Speaker: providerTurn.Speaker,
StartAt: providerTurn.StartAt, EndAt: providerTurn.EndAt, FinalText: providerTurn.FinalText,
Interrupted: providerTurn.Interrupted, ProviderRef: "provider-turn-1",
}},
}
lateTranscriptBody, _ := json.Marshal(lateTranscript)
lateTranscriptResult, err := processor.Process(ctx, lateTranscript, lateTranscriptBody)
if err != nil || lateTranscriptResult.TurnsAccepted != 1 || lateTranscriptResult.Status != "COMPLETED" {
t.Fatalf("late transcript=%#v err=%v", lateTranscriptResult, err)
}
deviceTurn := TranscriptTurnInput{
TurnKey: "device-final-2", SequenceNo: 2, Speaker: "agent",
StartAt: time.Date(2026, 8, 23, 12, 0, 2, 0, time.UTC), EndAt: time.Date(2026, 8, 23, 12, 0, 3, 0, time.UTC),
FinalText: "终端补传的缺失终稿", Interrupted: true,
}
transcriptInput := TranscriptBatchInput{Turns: []TranscriptTurnInput{providerTurn, deviceTurn}}
transcriptBody, _ := json.Marshal(transcriptInput)
transcriptResponse, replayed, err := transcriptService.Submit(ctx, identity, first.ConversationID, "transcript-idempotency-0001", "transcript-request-1", transcriptBody, transcriptInput)
if err != nil || replayed || transcriptResponse.AcceptedCount != 1 || transcriptResponse.DuplicateCount != 1 {
t.Fatalf("transcript response=%#v replayed=%v err=%v", transcriptResponse, replayed, err)
}
replayedTranscript, replayed, err := transcriptService.Submit(ctx, identity, first.ConversationID, "transcript-idempotency-0001", "transcript-request-2", transcriptBody, transcriptInput)
if err != nil || !replayed || replayedTranscript != transcriptResponse {
t.Fatalf("replayed transcript=%#v replayed=%v err=%v", replayedTranscript, replayed, err)
}
duplicateTranscript, replayed, err := transcriptService.Submit(ctx, identity, first.ConversationID, "transcript-idempotency-0002", "transcript-request-3", transcriptBody, transcriptInput)
if err != nil || replayed || duplicateTranscript.AcceptedCount != 0 || duplicateTranscript.DuplicateCount != 2 {
t.Fatalf("duplicate transcript=%#v replayed=%v err=%v", duplicateTranscript, replayed, err)
}
conflictingInput := TranscriptBatchInput{Turns: []TranscriptTurnInput{deviceTurn}}
conflictingInput.Turns[0].FinalText = "不一致的终稿"
conflictingBody, _ = json.Marshal(conflictingInput)
if _, _, err := transcriptService.Submit(ctx, identity, first.ConversationID, "transcript-idempotency-0003", "transcript-request-4", conflictingBody, conflictingInput); err == nil {
t.Fatal("conflicting transcript was accepted")
} else {
var conflict *TranscriptConflictError
if !errors.As(err, &conflict) || len(conflict.Conflicts) != 1 {
t.Fatalf("conflicting transcript error=%v", err)
}
}
wrongDevice := identity
wrongDevice.ID = "90000000-0000-0000-0000-000000000599"
if _, _, err := transcriptService.Submit(ctx, wrongDevice, first.ConversationID, "transcript-idempotency-0004", "transcript-request-5", transcriptBody, transcriptInput); !errors.Is(err, ErrConversationNotFound) {
t.Fatalf("wrong device transcript error=%v", err)
}
if _, err := pool.Exec(ctx, `UPDATE conversation SET retention_mode='none' WHERE public_id=$1::uuid`, first.ConversationID); err != nil {
t.Fatal(err)
}
if _, _, err := transcriptService.Submit(ctx, identity, first.ConversationID, "transcript-idempotency-0005", "transcript-request-6", transcriptBody, transcriptInput); !errors.Is(err, ErrTranscriptStorageNotAllowed) {
t.Fatalf("retention-disabled transcript error=%v", err)
}
var turnCount, deviceTurnCount, transcriptAudits, extractionEvents, providerExtractionEvents int
var extractionGeneration int64
if err := pool.QueryRow(ctx, `SELECT count(*),count(*) FILTER (WHERE source_type='device') FROM conversation_turn
WHERE conversation_id=(SELECT id FROM conversation WHERE public_id=$1::uuid)`, first.ConversationID).Scan(&turnCount, &deviceTurnCount); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM audit_event WHERE actor_public_id=$1::uuid AND action='conversation.transcript_batch.submit'`, testDevicePublicID).Scan(&transcriptAudits); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*),max((payload->>'generation_version')::bigint) FROM outbox_event WHERE aggregate_id=(SELECT id FROM conversation WHERE public_id=$1::uuid)
AND event_type='conversation.content_extraction_requested'
AND deduplication_key LIKE 'content-extraction-v1:%:device:%'`, first.ConversationID).Scan(&extractionEvents, &extractionGeneration); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM outbox_event WHERE aggregate_id=(SELECT id FROM conversation WHERE public_id=$1::uuid)
AND event_type='conversation.content_extraction_requested'
AND deduplication_key LIKE 'content-extraction-v1:%:provider:%'`, first.ConversationID).Scan(&providerExtractionEvents); err != nil {
t.Fatal(err)
}
if turnCount != 2 || deviceTurnCount != 1 || transcriptAudits != 2 || extractionEvents != 1 || providerExtractionEvents != 1 || extractionGeneration != 2 {
t.Fatalf("turns=%d device=%d audits=%d device_extraction=%d provider_extraction=%d generation=%d", turnCount, deviceTurnCount, transcriptAudits, extractionEvents, providerExtractionEvents, extractionGeneration)
}
}
func cleanupOrchestrationFixture(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
t.Helper()
statements := []struct {
query string
arg string
}{
{`DELETE FROM runtime_config_rollout WHERE config_key=$1`, testBundleConfigKey},
{`DELETE FROM ops_config_evaluation WHERE config_version_id IN (SELECT id FROM runtime_config_version WHERE config_key=$1)`, testBundleConfigKey},
{`DELETE FROM runtime_config_version WHERE config_key=$1`, testBundleConfigKey},
{`DELETE FROM audit_event WHERE request_id LIKE $1`, "orchestration-agent-bundle-%"},
{`DELETE FROM idempotency_record WHERE idempotency_key=$1`, testBundleCreateKey},
{`DELETE FROM idempotency_record WHERE idempotency_key=$1`, testBundleEvaluationKey},
{`DELETE FROM runtime_config_version WHERE id=$1::uuid`, testPersonaConfigID},
{`DELETE FROM runtime_config_version WHERE id=$1::uuid`, testVoiceConfigID},
{`DELETE FROM runtime_config_version WHERE id=$1::uuid`, testModelConfigID},
{`DELETE FROM runtime_config_version WHERE id=$1::uuid`, testRoutingConfigID},
{`DELETE FROM conversation_turn WHERE conversation_id IN (SELECT id FROM conversation WHERE device_id=$1::uuid)`, testDeviceID},
{`DELETE FROM provider_event WHERE conversation_id IN (SELECT id FROM conversation WHERE device_id=$1::uuid)`, testDeviceID},
{`DELETE FROM conversation_state_history WHERE conversation_id IN (SELECT id FROM conversation WHERE device_id=$1::uuid)`, testDeviceID},
{`DELETE FROM rtc_task WHERE conversation_id IN (SELECT id FROM conversation WHERE device_id=$1::uuid)`, testDeviceID},
{`DELETE FROM outbox_event WHERE aggregate_id IN (SELECT id FROM conversation WHERE device_id=$1::uuid)`, testDeviceID},
{`DELETE FROM audit_event WHERE resource_id IN (SELECT id FROM conversation WHERE device_id=$1::uuid)`, testDeviceID},
{`DELETE FROM audit_event WHERE actor_public_id=$1::uuid`, testDevicePublicID},
{`DELETE FROM idempotency_record WHERE actor_key=$1`, "device:" + testDevicePublicID},
{`DELETE FROM conversation WHERE device_id=$1::uuid`, testDeviceID},
{`DELETE FROM context_snapshot WHERE subject_id=$1::uuid`, testSubjectID},
{`DELETE FROM consent_snapshot WHERE subject_id=$1::uuid`, testSubjectID},
{`DELETE FROM consent_record WHERE subject_id=$1::uuid`, testSubjectID},
{`DELETE FROM device_binding WHERE device_id=$1::uuid`, testDeviceID},
{`DELETE FROM device_presence_state WHERE device_id=$1::uuid`, testDeviceID},
{`DELETE FROM device_twin WHERE device_id=$1::uuid`, testDeviceID},
{`DELETE FROM device WHERE id=$1::uuid`, testDeviceID},
{`DELETE FROM subject_profile WHERE id=$1::uuid`, testSubjectID},
{`DELETE FROM billing_price_book WHERE id=$1::uuid`, testPriceBookID},
}
for _, statement := range statements {
if _, err := pool.Exec(ctx, statement.query, statement.arg); err != nil {
t.Fatalf("cleanup fixture: %v", err)
}
}
}