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