ci / verify (push) Canceled after 0s
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
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)
|
|
}
|
|
}
|