feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
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.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user