Files
story/internal/callmemory/direct_opening_test.go
T

134 lines
5.9 KiB
Go
Raw Normal View History

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)
}
}