feat: expand companion platform, device controls, and memoir workflows
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:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
+249
View File
@@ -0,0 +1,249 @@
package vikingmemory
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"testing"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/callmemory"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/identity"
"data-terminal-cloud/internal/saas"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestManagedUploadPreservesScopeAndOnlyExtractsManagedEvents(t *testing.T) {
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
var p map[string]any
json.NewDecoder(r.Body).Decode(&p)
if p["session_id"] != "c_test" || p["resource_id"] != "mem-test" || p["store_file"] != false {
t.Fatal("wrong upload destination")
}
types := p["extract_memory_type"].([]any)
if len(types) != 1 || types[0] != "event_v1" {
t.Fatal("independent profile extraction enabled")
}
meta := p["metadata"].(map[string]any)
if meta["default_user_id"] != "elder-a" || meta["default_assistant_id"] != ManagedAssistantID {
t.Fatal("wrong elder")
}
fmt.Fprint(w, `{"code":0}`)
})
if e := c.AddManagedSession(context.Background(), "c_test", "elder-a", []Message{{Role: "user", RoleID: "elder-a", Content: "茶"}}, 1); e != nil {
t.Fatal(e)
}
}
func TestManagedTranscriptToMemoryAndNextCall(t *testing.T) {
db := os.Getenv("TEST_DATABASE_URL")
if db == "" {
t.Skip("TEST_DATABASE_URL required")
}
ctx := context.Background()
p, e := pgxpool.New(ctx, db)
if e != nil {
t.Fatal(e)
}
defer p.Close()
exec := func(q string, args ...any) {
t.Helper()
if _, e := p.Exec(ctx, q, args...); e != nil {
t.Fatal(e)
}
}
subject, elder, device, sid := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString()
tenant := "10000000-0000-0000-0000-000000000001"
exec(`INSERT INTO subject_profile(id,public_id,tenant_id,household_id,preferred_name,status) VALUES($1::uuid,$2::uuid,$3::uuid,'10000000-0000-0000-0000-000000000201','managed-test','active')`, subject, elder, tenant)
exec(`INSERT INTO consent_record(tenant_id,subject_id,consent_type,scope,policy_version,notice_digest,actor_user_id,evidence_ref) SELECT tenant_id,$1::uuid,consent_type,scope,policy_version,notice_digest,actor_user_id,evidence_ref FROM consent_record WHERE subject_id='10000000-0000-0000-0000-000000000401' AND withdrawn_at IS NULL`, subject)
exec(`INSERT INTO consent_snapshot(tenant_id,subject_id,snapshot_version,effective_consents,source_consent_ids) VALUES($1::uuid,$2::uuid,1,'{"voice_chat":true}','[]')`, tenant, subject)
exec(`INSERT INTO memory_user_state(subject_id,tenant_id) VALUES($1::uuid,$2::uuid)`, subject, tenant)
exec(`INSERT INTO device(id,tenant_id,serial_digest,model,hardware_revision,status,credential_ref,activation_state) VALUES($1::uuid,$2::uuid,$3,'test','test','registered','local-dev://test','ACTIVE')`, device, tenant, strings.Repeat("a", 28)+strings.ReplaceAll(device, "-", ""))
exec(`INSERT INTO device_binding(tenant_id,device_id,household_id,subject_id,bound_by,reason,generation) VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000201',$3::uuid,'10000000-0000-0000-0000-000000000101','test',1)`, tenant, device, subject)
exec(`INSERT INTO subject_voice_agent(subject_id,bot_name,bot_id,status) VALUES($1::uuid,$2,$2,'READY')`, subject, "test-"+subject)
exec(`INSERT INTO device_activation_job(device_id,tenant_id,provider_device_name,provider_bot_id,status,binding_generation,config_version,sdk_registered_at) VALUES($1::uuid,$2::uuid,$3,'bot','READY',1,1,now())`, device, tenant, "test-"+device)
exec(`INSERT INTO call_session(id,tenant_id,subject_id,device_id,boot_id,hook_event_id,call_generation,binding_generation,call_state,transcript_state,ended_at,memory_mode,transcript_revision,budget_snapshot) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,$1::uuid,$1,1,1,'ENDED','PARTIAL',now(),'HOT_ONLY',2,'{"protocol":"DIRECT-CHAT-1"}')`, sid, tenant, subject, device)
exec(`INSERT INTO call_utterance(session_id,utterance_id,source_revision,speaker,text_content,interrupted,source_at) VALUES($1::uuid,'u',0,'USER','我喜欢淡茶',false,now()),($1::uuid,'a',0,'ASSISTANT','记住了',false,now()+interval '1 second')`, sid)
user := UserID(tenant, subject)
remote := SessionID(sid, 0)
uploads := 0
fail := true
deleted := false
cloudText := "喜欢淡茶"
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
var in map[string]any
json.NewDecoder(r.Body).Decode(&in)
switch {
case strings.HasSuffix(r.URL.Path, "/session/add"):
uploads++
if fail {
w.WriteHeader(503)
fmt.Fprint(w, `{"code":9}`)
return
}
msgs := in["messages"].([]any)
if len(msgs) != 2 || in["session_id"] != remote {
t.Fatal("lost or duplicated transcript")
}
for _, msg := range msgs {
v := msg.(map[string]any)
if v["role"] == "user" && v["role_id"] != user {
t.Fatal("wrong upload elder")
}
}
fmt.Fprint(w, `{"code":0}`)
case strings.HasSuffix(r.URL.Path, "/search"):
f := in["filter"].(map[string]any)
if f["user_id"].([]any)[0] != user || f["session_id"].([]any)[0] != remote {
t.Fatal("unscoped search")
}
items := []any{map[string]any{"id": "foreign", "user_id": []string{"other-elder"}, "assistant_id": []string{ManagedAssistantID}, "session_id": remote, "memory_info": map[string]any{"summary": "不能串人"}}}
if !deleted {
items = append(items, map[string]any{"id": "event-one", "user_id": []string{user}, "assistant_id": []string{ManagedAssistantID}, "session_id": remote, "memory_info": map[string]any{"summary": cloudText}})
}
json.NewEncoder(w).Encode(map[string]any{"code": 0, "data": map[string]any{"result_list": items}})
case strings.HasSuffix(r.URL.Path, "/update"):
if in["event_id"] != "event-one" {
t.Fatal("wrong update target")
}
cloudText = in["memory_info"].(map[string]any)["summary"].(string)
fmt.Fprint(w, `{"code":0}`)
case strings.HasSuffix(r.URL.Path, "/delete"):
if in["event_id"] != "event-one" {
t.Fatal("wrong delete target")
}
deleted = true
fmt.Fprint(w, `{"code":0}`)
case strings.HasSuffix(r.URL.Path, "/batch_delete"):
f := in["filter"].(map[string]any)
if f["user_id"].([]any)[0] != user || f["assistant_id"].([]any)[0] != ManagedAssistantID {
t.Fatal("unscoped withdrawal")
}
deleted = true
fmt.Fprint(w, `{"code":0}`)
default:
t.Fatal("unexpected endpoint", r.URL.Path)
}
})
m := NewManaged(p, c)
s := callmemory.NewService(p, nil)
s.ConfigureManagedSync(m.SyncNext)
s.ConfigureProjection(m)
due := func() { exec(`UPDATE call_memory_sync SET next_attempt_at=now() WHERE session_id=$1::uuid`, sid) }
if e = m.SyncNext(ctx); e == nil {
t.Fatal("failed upload reported successful")
}
fail = false
due()
if e = m.SyncNext(ctx); e != nil {
t.Fatal(e)
}
due()
if e = m.SyncNext(ctx); e != nil {
t.Fatal(e)
}
due()
if e = m.SyncNext(ctx); e != nil {
t.Fatal(e)
}
if uploads != 2 {
t.Fatal("acknowledged upload repeated", uploads)
}
var n int
var id string
if e = p.QueryRow(ctx, `SELECT count(*),min(public_id::text) FROM curated_memory WHERE subject_id=$1::uuid`, subject).Scan(&n, &id); e != nil || n != 1 {
t.Fatal("import duplicate or foreign memory", n, e)
}
if e = s.ProjectNext(ctx); e != nil {
t.Fatal(e)
}
// A provider correction may refresh an untouched auto-memory.
cloudText = "喜欢不加糖的淡茶"
due()
if e = m.SyncNext(ctx); e != nil {
t.Fatal(e)
}
var contextText string
s.ConfigureDirect(callmemory.DirectOptions{CallbackURL: "https://example.com", CallbackSecret: []byte(strings.Repeat("k", 32)), Prepare: func(_ context.Context, gotSubject, bot, session, text string) error {
if gotSubject != subject {
t.Fatal("wrong preparation elder")
}
contextText = text
return nil
}})
next := uuid.NewString()
d := deviceauth.Identity{ID: device, TenantID: tenant, BindingGeneration: 1}
if _, e = s.DirectRequest(ctx, d, "start", callmemory.DirectInput{SessionID: next}); e != nil {
t.Fatal(e)
}
if !strings.Contains(contextText, "淡茶") || strings.Contains(contextText, "串人") {
t.Fatal("next call did not receive scoped memory")
}
if _, e = s.DirectRequest(ctx, d, "end", callmemory.DirectInput{SessionID: next, Reason: "hangup", SDKStopped: true}); e != nil {
t.Fatal(e)
}
principal := identity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000101"}
portal := saas.NewService(p, audit.NewRepository(), nil, nil)
visible, e := portal.ListSubjectMemories(ctx, principal, elder, "")
if e != nil || len(visible) != 1 || visible[0].ConfirmedBy != "" {
t.Fatal("mini-program cannot list automatically extracted memory", e, len(visible))
}
text := "喜欢温开水"
if _, e = portal.UpdateMemory(ctx, principal, id, "managed-test-edit", 2, saas.UpdateMemoryInput{Content: &text, Reason: "更正测试"}); e != nil {
t.Fatal(e)
}
if e = s.ProjectNext(ctx); e != nil {
t.Fatal(e)
}
if cloudText != text {
t.Fatal("edit not projected")
}
if e = s.ProjectNext(ctx); e != nil {
t.Fatal(e)
}
due()
if e = m.SyncNext(ctx); e != nil {
t.Fatal(e)
}
if e = p.QueryRow(ctx, `SELECT count(*) FROM call_utterance WHERE session_id=$1::uuid`, sid).Scan(&n); e != nil || n != 2 {
t.Fatal("chat records lost", e)
}
// Use the actual user deletion API; repeated import cannot resurrect it.
if _, e = portal.RequestMemoryDeletion(ctx, principal, id, "测试删除", "managed-test-delete", 3); e != nil {
t.Fatal(e)
}
due()
if e = m.SyncNext(ctx); e != nil {
t.Fatal(e)
}
var status string
if e = p.QueryRow(ctx, `SELECT status FROM curated_memory WHERE public_id=$1::uuid`, id).Scan(&status); e != nil || status != "PENDING_DELETE" {
t.Fatal("deleted record resurrected", status, e)
}
// Exact deletion projection and its confirmed absence; no second model involved.
r := callmemory.ProjectionRecord{ID: id, Subject: subject, Tenant: tenant, Status: "PENDING_DELETE", Policy: "NEVER"}
if _, e = m.Project(ctx, r, "", "", false); e != nil {
t.Fatal(e)
}
if !deleted {
t.Fatal("remote deletion missing")
}
result, e := m.Project(ctx, r, "", "", true)
if e != nil || !result.Visible {
t.Fatal("cleanup acknowledgement missing", e)
}
deleted = false
exec(`UPDATE consent_record SET withdrawn_at=now() WHERE subject_id=$1::uuid AND consent_type='personalized_memory'`, subject)
due()
if e = m.SyncNext(ctx); e != nil {
t.Fatal(e)
}
if !deleted {
t.Fatal("withdrawal did not clean managed cloud scope")
}
}