Files
story/internal/saas/voice_agent_integration_test.go
T

149 lines
5.7 KiB
Go
Raw Normal View History

package saas
import (
"context"
"errors"
"os"
"strings"
"testing"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/elderagent"
"data-terminal-cloud/internal/identity"
"github.com/jackc/pgx/v5/pgxpool"
)
type elderProvider struct {
calls int
fail bool
updates int
config map[string]any
}
func (p *elderProvider) Ensure(ctx context.Context, name string, c, a map[string]any) (elderagent.Bot, error) {
p.calls++
if p.fail {
return elderagent.Bot{}, errors.New("provider unavailable")
}
return elderagent.Bot{ID: "bot-" + name, Name: name, Config: c, AgentConfig: a}, nil
}
func (p *elderProvider) Update(_ context.Context, _ string, c, a map[string]any) error {
p.updates++
p.config = c
if p.fail {
return errors.New("SECRET_PROVIDER_FAILURE")
}
return nil
}
func TestSubjectEditSynchronizesPersonaWithoutLosingSavedProfile(t *testing.T) {
if os.Getenv("TEST_DATABASE_URL") == "" {
t.Skip("integration database required")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, os.Getenv("TEST_DATABASE_URL"))
if err != nil {
t.Fatal(err)
}
defer pool.Close()
s := NewService(pool, audit.NewRepository(), nil, nil)
provider := &elderProvider{}
agents := &elderagent.Service{Pool: pool, Provider: provider, Template: []byte(`{"Config":{"S2SConfig":{"Provider":"volcano","ProviderParams":{"app":{"appid":"test","token":"test"},"dialog":{"system_role":"基础角色"}}}}}`), CallbackURL: "https://example.com/subtitles", CallbackSecret: []byte(strings.Repeat("k", 32))}
s.ConfigureVoiceAgents(agents)
owner := identity.Principal{Issuer: testIssuer, Subject: testOwnerSubject}
h, err := s.CreateHousehold(ctx, owner, testTenantPublicID, "资料同步测试", "persona-save-test")
if err != nil {
t.Fatal(err)
}
sp, err := s.CreateSubject(ctx, owner, h.ID, "persona-save-test", CreateSubjectInput{PreferredName: "原称呼", Language: "zh-CN", Timezone: "Asia/Shanghai", ProfileData: map[string]any{}, ConversationPreferences: map[string]any{}})
if err != nil {
t.Fatal(err)
}
name := "修改后的称呼"
provider.fail = true
sp, err = s.PatchSubject(ctx, owner, sp.ID, "persona-save-test", sp.Version, PatchSubjectInput{PreferredName: &name})
if err != nil || sp.PreferredName != name || sp.VoiceAgent == nil || sp.VoiceAgent.Status != "READY" || sp.VoiceAgent.PersonaSyncStatus != "FAILED" {
t.Fatal("provider failure undid save or lost status", sp, err)
}
loaded, err := s.GetSubject(ctx, owner, sp.ID)
if err != nil || loaded.PreferredName != name {
t.Fatal("save was not durable", err)
}
updates := provider.updates
if _, err = s.PatchSubject(ctx, owner, sp.ID, "persona-save-test-stale", sp.Version-1, PatchSubjectInput{PreferredName: &name}); !errors.Is(err, ErrVersionConflict) || provider.updates != updates {
t.Fatal("stale edit changed provider", err)
}
provider.fail = false
i, err := s.EnsureSubjectVoiceAgent(ctx, owner, sp.ID)
if err != nil || i.PersonaSyncStatus != "SYNCED" {
t.Fatal("manual sync retry failed", i, err)
}
// Corrections share the database trigger, including changes made through privacy.
_, _, err = s.CreatePrivacyRequest(ctx, owner, sp.ID, "persona-correction-test", "persona-correction-test", CreatePrivacyRequestInput{RequestType: "CORRECTION", Scope: map[string]any{"corrections": map[string]any{"preferred_name": "更正后的称呼"}}})
if err != nil {
t.Fatal(err)
}
i, err = agents.Get(ctx, sp.ID)
if err != nil || i.PersonaSyncStatus != "PENDING" {
t.Fatal("correction not queued", i, err)
}
if err = agents.SyncPending(ctx); err != nil {
t.Fatal(err)
}
i, err = agents.Get(ctx, sp.ID)
if err != nil || i.PersonaSyncStatus != "SYNCED" {
t.Fatal("correction not synchronized", i, err)
}
}
func TestCreatingElderCreatesPersistentAgentAndRetry(t *testing.T) {
if os.Getenv("TEST_DATABASE_URL") == "" {
t.Skip("integration database required")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, os.Getenv("TEST_DATABASE_URL"))
if err != nil {
t.Fatal(err)
}
defer pool.Close()
s := NewService(pool, audit.NewRepository(), nil, nil)
provider := &elderProvider{}
agents := &elderagent.Service{Pool: pool, Provider: provider, Template: []byte(`{"Config":{"S2SConfig":{"Provider":"volcano","ProviderParams":{"app":{"appid":"test","token":"test"}}}}}`), CallbackURL: "https://example.com/subtitles", CallbackSecret: []byte(strings.Repeat("k", 32))}
s.ConfigureVoiceAgents(agents)
owner := identity.Principal{Issuer: testIssuer, Subject: testOwnerSubject}
h, err := s.CreateHousehold(ctx, owner, testTenantPublicID, "专属智能体测试", "elder-agent-test")
if err != nil {
t.Fatal(err)
}
create := func(name string) Subject {
t.Helper()
sp, err := s.CreateSubject(ctx, owner, h.ID, "elder-agent-test", CreateSubjectInput{PreferredName: name, Language: "zh-CN", Timezone: "Asia/Shanghai", ProfileData: map[string]any{}, ConversationPreferences: map[string]any{}})
if err != nil {
t.Fatal(err)
}
return sp
}
first := create("第一位长者")
second := create("第二位长者")
if first.VoiceAgent == nil || first.VoiceAgent.Status != "READY" || first.VoiceAgent.BotID == second.VoiceAgent.BotID {
t.Fatal(first, second)
}
if _, err = s.EnsureSubjectVoiceAgent(ctx, owner, first.ID); err != nil || provider.calls != 2 {
t.Fatal("retry created another bot", err, provider.calls)
}
provider.fail = true
failed := create("失败后重试")
if failed.ID == "" || failed.VoiceAgent.Status != "FAILED" {
t.Fatal(failed)
}
loaded, err := s.GetSubject(ctx, owner, failed.ID)
if err != nil || loaded.VoiceAgent.Status != "FAILED" {
t.Fatal(loaded, err)
}
provider.fail = false
fixed, err := s.EnsureSubjectVoiceAgent(ctx, owner, failed.ID)
if err != nil || fixed.Status != "READY" {
t.Fatal(fixed, err)
}
}