168 lines
6.6 KiB
Go
168 lines
6.6 KiB
Go
package callmemory
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/deviceauth"
|
||
|
|
"data-terminal-cloud/internal/volcsubtitle"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestDirectStoryTrackingBoundaries(t *testing.T) {
|
||
|
|
state := storyContext{Version: storyContextVersion}
|
||
|
|
sid := uuid.NewString()
|
||
|
|
state.observe(sid, "q", "ASSISTANT", "您以前在厂里做什么工作呢?", 2000)
|
||
|
|
state.observe(sid, "short", "USER", "嗯", 3000)
|
||
|
|
if state.UserTurns != 0 || state.EarlyInvitations != 1 || state.Questions[0].Reply != nil {
|
||
|
|
t.Fatal(state)
|
||
|
|
}
|
||
|
|
state.observe(sid, "quoted", "USER", "他说不想聊以前的工作", 4000)
|
||
|
|
if len(state.Paused) != 0 {
|
||
|
|
t.Fatal("third-party quotation became a refusal")
|
||
|
|
}
|
||
|
|
state.observe(sid, "q2", "ASSISTANT", "您以前在厂里做什么工作呢?", 190000)
|
||
|
|
state.observe(sid, "no", "USER", "我不想聊以前的工作", 190100)
|
||
|
|
if len(state.Paused) != 1 || state.Paused[0].Topic != "work" || state.Pending != nil {
|
||
|
|
t.Fatal(state)
|
||
|
|
}
|
||
|
|
state.observe(sid, "own", "USER", "我年轻时在老家种过菜", 191000)
|
||
|
|
state.observe(sid, "q3", "ASSISTANT", "老家以前怎么种菜呢?", 192000)
|
||
|
|
if state.Invitations != 2 || len(state.Paused) != 1 {
|
||
|
|
t.Fatal("user-led story counted as invitation or erased prior boundary", state)
|
||
|
|
}
|
||
|
|
raw, _ := json.Marshal(state)
|
||
|
|
if strings.Contains(string(raw), "不想聊") || strings.Contains(string(raw), "种过菜") {
|
||
|
|
t.Fatal("duplicated transcript in snapshot", string(raw))
|
||
|
|
}
|
||
|
|
state.observe(sid, "all", "USER", "我不想聊以前的事", 193000)
|
||
|
|
if state.Paused[len(state.Paused)-1].Topic != "all" {
|
||
|
|
t.Fatal("global boundary not tracked")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func storyFixture(t *testing.T) (fixture, deviceauth.Identity, *string) {
|
||
|
|
f, d := directFixture(t)
|
||
|
|
f.add(t)
|
||
|
|
brief := ""
|
||
|
|
f.s.direct.Prepare = func(_ context.Context, _, _, _, text string) error { brief = text; return nil }
|
||
|
|
return f, d, &brief
|
||
|
|
}
|
||
|
|
func storyStart(t *testing.T, f fixture, d deviceauth.Identity) string {
|
||
|
|
t.Helper()
|
||
|
|
sid := uuid.NewString()
|
||
|
|
if _, e := f.s.DirectRequest(context.Background(), d, "start", DirectInput{SessionID: sid}); e != nil {
|
||
|
|
t.Fatal(e)
|
||
|
|
}
|
||
|
|
return sid
|
||
|
|
}
|
||
|
|
func storyStop(t *testing.T, f fixture, d deviceauth.Identity, sid string) {
|
||
|
|
t.Helper()
|
||
|
|
if _, e := f.s.DirectRequest(context.Background(), d, "end", DirectInput{SessionID: sid, Reason: "hangup", SDKStopped: true}); e != nil {
|
||
|
|
t.Fatal(e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
func storySubtitle(t *testing.T, f fixture, sid, speaker, text string, seq int64) {
|
||
|
|
t.Helper()
|
||
|
|
round := int64(1)
|
||
|
|
if e := f.s.DirectSubtitle(context.Background(), sid, volcsubtitle.Packet{Type: "subtitle", Data: []volcsubtitle.Segment{{UserID: speaker, RoundID: &round, Sequence: &seq, Text: text, Definite: true}}}); e != nil {
|
||
|
|
t.Fatal(e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
func storyRead(t *testing.T, f fixture, sid string) storyContext {
|
||
|
|
t.Helper()
|
||
|
|
var raw []byte
|
||
|
|
if e := f.p.QueryRow(context.Background(), `SELECT budget_snapshot->'story_context' FROM call_session WHERE id=$1::uuid`, sid).Scan(&raw); e != nil {
|
||
|
|
t.Fatal(e)
|
||
|
|
}
|
||
|
|
var state storyContext
|
||
|
|
if e := json.Unmarshal(raw, &state); e != nil {
|
||
|
|
t.Fatal(e)
|
||
|
|
}
|
||
|
|
return state
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDirectStorySourceContextAndRevocation(t *testing.T) {
|
||
|
|
f, d, brief := storyFixture(t)
|
||
|
|
sid := storyStart(t, f, d)
|
||
|
|
storySubtitle(t, f, sid, "output", "您以前在厂里做什么工作呢?", 1)
|
||
|
|
storySubtitle(t, f, sid, "input", "我在厂里负责修机器", 2)
|
||
|
|
storyStop(t, f, d, sid)
|
||
|
|
next := storyStart(t, f, d)
|
||
|
|
if !strings.Contains(*brief, "recently_asked_questions") || !strings.Contains(*brief, "我在厂里负责修机器") {
|
||
|
|
t.Fatal("source-backed question/reply not loaded", *brief)
|
||
|
|
}
|
||
|
|
if len(storyRead(t, f, next).Questions) != 1 {
|
||
|
|
t.Fatal("question history not carried forward")
|
||
|
|
}
|
||
|
|
frozen := *brief
|
||
|
|
if _, e := f.s.DirectRequest(context.Background(), d, "start", DirectInput{SessionID: next}); e != nil {
|
||
|
|
t.Fatal(e)
|
||
|
|
}
|
||
|
|
if *brief != frozen {
|
||
|
|
t.Fatal("renewal changed the frozen background")
|
||
|
|
}
|
||
|
|
storyStop(t, f, d, next)
|
||
|
|
f.exec(t, `UPDATE consent_record SET withdrawn_at=now() WHERE subject_id=$1::uuid AND consent_type='personalized_memory'`, f.subject)
|
||
|
|
storyStart(t, f, d)
|
||
|
|
if *brief != "" {
|
||
|
|
t.Fatal("memory revoked but history sent to provider", *brief)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDirectStoryRefusalPersistsAndDuplicateCallbackIsIdempotent(t *testing.T) {
|
||
|
|
f, d, brief := storyFixture(t)
|
||
|
|
sid := storyStart(t, f, d)
|
||
|
|
storySubtitle(t, f, sid, "output", "您以前在厂里做什么工作呢?", 1)
|
||
|
|
storySubtitle(t, f, sid, "output", "您以前在厂里做什么工作呢?", 1)
|
||
|
|
storySubtitle(t, f, sid, "input", "我不想聊以前的工作", 2)
|
||
|
|
state := storyRead(t, f, sid)
|
||
|
|
if state.Invitations != 1 || len(state.Paused) != 1 || len(state.Questions) != 1 || state.Questions[0].Reply != nil {
|
||
|
|
t.Fatal(state)
|
||
|
|
}
|
||
|
|
storyStop(t, f, d, sid)
|
||
|
|
next := storyStart(t, f, d)
|
||
|
|
if !strings.Contains(*brief, `"topics_not_to_open_proactively":["work"]`) {
|
||
|
|
t.Fatal("explicit boundary missing", *brief)
|
||
|
|
}
|
||
|
|
storySubtitle(t, f, next, "input", "我年轻时在厂里修过机器", 3)
|
||
|
|
storyStop(t, f, d, next)
|
||
|
|
last := storyStart(t, f, d)
|
||
|
|
if len(storyRead(t, f, last).Paused) != 1 || !strings.Contains(*brief, `"work"`) {
|
||
|
|
t.Fatal("voluntary story erased future boundary")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDirectStoryIsolationAndDeletedSource(t *testing.T) {
|
||
|
|
f, d, brief := storyFixture(t)
|
||
|
|
sid := storyStart(t, f, d)
|
||
|
|
storySubtitle(t, f, sid, "output", "您以前在厂里做什么工作呢?", 1)
|
||
|
|
storySubtitle(t, f, sid, "input", "我不想聊以前的工作", 2)
|
||
|
|
storyStop(t, f, d, sid)
|
||
|
|
f.exec(t, `UPDATE call_utterance SET text_content='[deleted]' WHERE session_id=$1::uuid`, sid)
|
||
|
|
next := storyStart(t, f, d)
|
||
|
|
if strings.Contains(*brief, "recently_asked_questions") || len(storyRead(t, f, next).Paused) != 0 {
|
||
|
|
t.Fatal("deleted sources still used", *brief)
|
||
|
|
}
|
||
|
|
storyStop(t, f, d, next)
|
||
|
|
g, gd, _ := storyFixture(t)
|
||
|
|
foreign := storyStart(t, g, gd)
|
||
|
|
storySubtitle(t, g, foreign, "output", "您以前在厂里做什么工作呢?", 1)
|
||
|
|
storySubtitle(t, g, foreign, "input", "我不想聊以前的工作", 2)
|
||
|
|
storyStop(t, g, gd, foreign)
|
||
|
|
forged := storyRead(t, g, foreign)
|
||
|
|
f.exec(t, `UPDATE call_session SET budget_snapshot=jsonb_set(budget_snapshot,'{story_context}',$2::jsonb) WHERE id=$1::uuid`, next, jsonBytes(forged))
|
||
|
|
last := storyStart(t, f, d)
|
||
|
|
if len(storyRead(t, f, last).Paused) != 0 || strings.Contains(*brief, "recently_asked_questions") {
|
||
|
|
t.Fatal("foreign elder source leaked", *brief)
|
||
|
|
}
|
||
|
|
storyStop(t, f, d, last)
|
||
|
|
f.exec(t, `UPDATE memory_user_state SET privacy_epoch=privacy_epoch+1 WHERE subject_id=$1::uuid`, f.subject)
|
||
|
|
epochCall := storyStart(t, f, d)
|
||
|
|
if len(storyRead(t, f, epochCall).Questions) != 0 {
|
||
|
|
t.Fatal("old epoch history survived")
|
||
|
|
}
|
||
|
|
}
|