134 lines
5.9 KiB
Go
134 lines
5.9 KiB
Go
package callmemory
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/volcsubtitle"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
)
|
||
|
|
|
||
|
|
func openingWelcome(out any) string {
|
||
|
|
return out.(map[string]any)["params"].(map[string]any)["AgentConfig"].(map[string]any)["WelcomeMessage"].(string)
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertNoOpeningActivity(t *testing.T, text string) {
|
||
|
|
t.Helper()
|
||
|
|
for _, invitation := range []string{"谜语", "猜猜", "猜个", "您猜", "编个", "编故事", "往下编", "故事可以这样开头"} {
|
||
|
|
if strings.Contains(text, invitation) {
|
||
|
|
t.Fatalf("opening starts an unrequested activity %q: %q", invitation, text)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDirectDynamicOpeningFrozenAndRotation(t *testing.T) {
|
||
|
|
f, d := directFixture(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
f.exec(t, `UPDATE subject_profile SET timezone='Asia/Shanghai',conversation_preferences='{"dynamic_opening":true}' WHERE id=$1::uuid`, f.subject)
|
||
|
|
var prepared []string
|
||
|
|
f.s.direct.PrepareWithWelcome = func(_ context.Context, subject, bot, sid, contextText, welcome string) error {
|
||
|
|
if subject != f.subject || bot != "elder-bot-"+f.subject || !validUUID(sid) {
|
||
|
|
t.Fatal("wrong preparation identity")
|
||
|
|
}
|
||
|
|
prepared = append(prepared, welcome)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
seenIDs := map[string]bool{}
|
||
|
|
for i := 0; i < 7; i++ {
|
||
|
|
sid := uuid.NewString()
|
||
|
|
first, err := f.s.DirectRequest(ctx, d, "start", DirectInput{SessionID: sid})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
assertNoOpeningActivity(t, openingWelcome(first))
|
||
|
|
if openingWelcome(first) != prepared[i] {
|
||
|
|
t.Fatal("supplier and device welcome differ")
|
||
|
|
}
|
||
|
|
retry, err := f.s.DirectRequest(ctx, d, "start", DirectInput{SessionID: sid})
|
||
|
|
if err != nil || len(prepared) != i+1 || openingWelcome(first) != openingWelcome(retry) {
|
||
|
|
t.Fatal("renewal regenerated opening", err)
|
||
|
|
}
|
||
|
|
var meta directOpeningMetadata
|
||
|
|
var raw []byte
|
||
|
|
if err = f.p.QueryRow(ctx, `SELECT budget_snapshot->'opening' FROM call_session WHERE id=$1::uuid`, sid).Scan(&raw); err != nil || StrictJSON(raw, &meta) != nil {
|
||
|
|
t.Fatal("missing opening metadata", err, string(raw))
|
||
|
|
}
|
||
|
|
if i < 6 && seenIDs[meta.ID] {
|
||
|
|
t.Fatal("recent opening repeated", meta)
|
||
|
|
}
|
||
|
|
seenIDs[meta.ID] = true
|
||
|
|
if strings.Contains(string(raw), openingWelcome(first)) || strings.Contains(string(raw), "interests") {
|
||
|
|
t.Fatal("opening metadata stores personal text")
|
||
|
|
}
|
||
|
|
if _, err = f.s.DirectRequest(ctx, d, "end", DirectInput{SessionID: sid, Reason: "hangup", SDKStopped: true}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
other, otherDevice := directFixture(t)
|
||
|
|
out, err := other.s.DirectRequest(ctx, otherDevice, "start", DirectInput{SessionID: uuid.NewString()})
|
||
|
|
if err != nil || openingWelcome(out) != "你好,今天想聊些什么?" {
|
||
|
|
t.Fatal("pilot changed another elder", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDirectDynamicOpeningReopenAndRevocation(t *testing.T) {
|
||
|
|
f, d := directFixture(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
f.exec(t, `UPDATE subject_profile SET profile_data='{"interests":["京剧"]}',conversation_preferences='{"dynamic_opening":true}' WHERE id=$1::uuid`, f.subject)
|
||
|
|
f.s.direct.PrepareWithWelcome = func(context.Context, string, string, string, string, string) error { return nil }
|
||
|
|
first := uuid.NewString()
|
||
|
|
if _, err := f.s.DirectRequest(ctx, d, "start", DirectInput{SessionID: first}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
n := int64(1)
|
||
|
|
for i, speaker := range []string{"input", "output"} {
|
||
|
|
seq := int64(i)
|
||
|
|
if err := f.s.DirectSubtitle(ctx, first, volcsubtitle.Packet{Type: "subtitle", Data: []volcsubtitle.Segment{{UserID: speaker, RoundID: &n, Sequence: &seq, Text: "测试真实字幕", Definite: true}}}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if _, err := f.s.DirectRequest(ctx, d, "end", DirectInput{SessionID: first, Reason: "hangup", SDKStopped: true}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
next := uuid.NewString()
|
||
|
|
if _, err := f.s.DirectRequest(ctx, d, "start", DirectInput{SessionID: next}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
var category string
|
||
|
|
if err := f.p.QueryRow(ctx, `SELECT budget_snapshot->'opening'->>'category' FROM call_session WHERE id=$1::uuid`, next).Scan(&category); err != nil || category != "reconnect" {
|
||
|
|
t.Fatal("real recent exchange did not reconnect", category, err)
|
||
|
|
}
|
||
|
|
if _, err := f.s.DirectRequest(ctx, d, "end", DirectInput{SessionID: next, Reason: "hangup", SDKStopped: true}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
f.exec(t, `UPDATE consent_record SET withdrawn_at=now() WHERE subject_id=$1::uuid AND consent_type='personalized_memory'`, f.subject)
|
||
|
|
// Retained transcript evidence must not re-enable personalized reconnect.
|
||
|
|
f.exec(t, `INSERT INTO call_utterance(session_id,utterance_id,source_revision,speaker,text_content,interrupted,source_at) VALUES($1::uuid,'test-user',0,'USER','测试',false,now()),($1::uuid,'test-assistant',0,'ASSISTANT','测试',false,now())`, next)
|
||
|
|
sid := uuid.NewString()
|
||
|
|
if _, err := f.s.DirectRequest(ctx, d, "start", DirectInput{SessionID: sid}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := f.p.QueryRow(ctx, `SELECT budget_snapshot->'opening'->>'category' FROM call_session WHERE id=$1::uuid`, sid).Scan(&category); err != nil || category == "reconnect" || category == "interest" {
|
||
|
|
t.Fatal("revoked memory used for opening", category, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDirectDynamicOpeningFailedPrepareDoesNotCommit(t *testing.T) {
|
||
|
|
f, d := directFixture(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
f.exec(t, `UPDATE subject_profile SET conversation_preferences='{"dynamic_opening":true}' WHERE id=$1::uuid`, f.subject)
|
||
|
|
f.s.direct.PrepareWithWelcome = func(context.Context, string, string, string, string, string) error {
|
||
|
|
return errors.New("provider unavailable")
|
||
|
|
}
|
||
|
|
sid := uuid.NewString()
|
||
|
|
if _, err := f.s.DirectRequest(ctx, d, "start", DirectInput{SessionID: sid}); err == nil || err.Error() != "VOICE_AGENT_CONFIG_FAILED" {
|
||
|
|
t.Fatal("failed configuration was accepted", err)
|
||
|
|
}
|
||
|
|
var count int
|
||
|
|
if err := f.p.QueryRow(ctx, `SELECT count(*) FROM call_session WHERE id=$1::uuid`, sid).Scan(&count); err != nil || count != 0 {
|
||
|
|
t.Fatal("failed preparation consumed a session/opening", count, err)
|
||
|
|
}
|
||
|
|
}
|