Files
story/internal/callmemory/direct_opening_test.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
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.
2026-10-08 10:06:44 +08:00

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