149 lines
5.7 KiB
Go
149 lines
5.7 KiB
Go
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)
|
||
|
|
}
|
||
|
|
}
|