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
+501
View File
@@ -0,0 +1,501 @@
package callmemory
import (
"context"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/identity"
"data-terminal-cloud/internal/outbox"
"data-terminal-cloud/internal/privacy"
"encoding/json"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/redis/go-redis/v9"
"os"
"strings"
"testing"
"time"
)
type fixture struct {
s *Service
p *pgxpool.Pool
principal identity.Principal
elder, subject, tenant, device, boot string
}
func setup(t *testing.T) fixture {
t.Helper()
url := os.Getenv("TEST_DATABASE_URL")
if url == "" {
t.Skip("TEST_DATABASE_URL required")
}
p, e := pgxpool.New(context.Background(), url)
if e != nil {
t.Fatal(e)
}
t.Cleanup(p.Close)
f := fixture{p: p, s: NewService(p, nil), elder: uuid.NewString(), subject: uuid.NewString(), tenant: "10000000-0000-0000-0000-000000000001", device: uuid.NewString(), boot: uuid.NewString(), principal: identity.Principal{Issuer: "http://localhost:58180/realms/voice-local", Subject: "00000000-0000-0000-0000-000000000101"}}
f.exec(t, `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','call-test','active')`, f.subject, f.elder, f.tenant)
f.exec(t, `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`, f.subject)
f.exec(t, `INSERT INTO consent_snapshot(tenant_id,subject_id,snapshot_version,effective_consents,source_consent_ids) VALUES($1::uuid,$2::uuid,1,'{"voice_chat":true}','[]')`, f.tenant, f.subject)
f.exec(t, `INSERT INTO device(id,tenant_id,serial_digest,model,hardware_revision,status,credential_ref,minimum_security_version,minimum_firmware_version,onboarding_boot_id,binding_generation) VALUES($1::uuid,$2::uuid,$3,'test','test','registered','local-dev://call-test','1','1',$4::uuid,0)`, f.device, f.tenant, hash([]byte(f.device)), f.boot)
f.exec(t, `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)`, f.tenant, f.device, f.subject)
f.exec(t, `INSERT INTO memory_user_state(subject_id,tenant_id,legacy_projection_pending) VALUES($1::uuid,$2::uuid,false)`, f.subject, f.tenant)
return f
}
func (f fixture) exec(t *testing.T, q string, args ...any) {
t.Helper()
if _, e := f.p.Exec(context.Background(), q, args...); e != nil {
t.Fatal(e)
}
}
func (f fixture) request(t *testing.T, op string, in any) map[string]any {
t.Helper()
out, e := f.s.MemoryRequest(context.Background(), f.principal, op, jsonBytes(in))
if e != nil {
t.Fatalf("%s: %v", op, e)
}
var m map[string]any
json.Unmarshal(jsonBytes(out), &m)
return m
}
func (f fixture) add(t *testing.T) (string, map[string]any) {
t.Helper()
content := "喜欢喝淡茶"
out := f.request(t, "mutations", MutationInput{ID: uuid.NewString(), ElderID: f.elder, Action: "ADD", Targets: []string{}, Expected: map[string]int64{}, Patch: MemoryPatch{Content: &content}})
return out["targets"].([]any)[0].(string), out
}
func (f fixture) call(t *testing.T) (Manifest, CallKey, deviceauth.Identity) {
t.Helper()
m := testManifest()
m.BootID = f.boot
m.DeviceID = f.device
m.PrivacyEpoch = 0
b, mac, e := EncodeManifest([]byte(strings.Repeat("k", 32)), m)
if e != nil {
t.Fatal(e)
}
f.exec(t, `INSERT INTO call_session(id,tenant_id,subject_id,device_id,boot_id,hook_event_id,call_generation,binding_generation,manifest_id,manifest_b64,manifest_mac,manifest_status,memory_mode,expires_at,lease_expires_at) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,$5::uuid,$6,1,1,$7::uuid,$8,$9,'ISSUED','HOT_ONLY',$10,$11)`, m.SessionID, f.tenant, f.subject, f.device, f.boot, m.HookID, m.ManifestID, b, mac, time.UnixMilli(m.ExpiresAt), time.UnixMilli(m.LeaseExpiresAt))
return m, CallKey{SessionID: &m.SessionID, BootID: f.boot, HookID: m.HookID, Generation: 1, Binding: 1}, deviceauth.Identity{ID: f.device, TenantID: f.tenant, EnrollmentMode: "physical_confirm", BindingGeneration: 1}
}
func TestMutationVersionFenceAndConfirmationReplay(t *testing.T) {
f := setup(t)
id, _ := f.add(t)
m, _, _ := f.call(t)
out := f.request(t, "mutations", MutationInput{ID: uuid.NewString(), ElderID: f.elder, Action: "DELETE", Targets: []string{}, Expected: map[string]int64{}, Patch: MemoryPatch{}})
if out["mutation_state"] != "NEEDS_CONFIRMATION" {
t.Fatal(out)
}
resolved := f.request(t, "mutations/resolve", ResolveInput{ID: out["mutation_id"].(string), Revision: int64(out["mutation_revision"].(float64)), Targets: []string{id}, Expected: map[string]int64{id: 1}, Patch: MemoryPatch{}})
ci := ConfirmInput{ID: out["mutation_id"].(string), Revision: int64(resolved["mutation_revision"].(float64)), Digest: resolved["proposal_digest"].(string), Token: resolved["confirmation_token"].(string), Decision: "CONFIRM"}
done := f.request(t, "mutations/confirm", ci)
if done["mutation_state"] != "PROPAGATING" {
t.Fatal(done)
}
ci.Decision = "CANCEL"
replay := f.request(t, "mutations/confirm", ci)
if replay["mutation_state"] != "PROPAGATING" {
t.Fatal("cancel rolled back committed mutation")
}
var status string
var epoch int64
if e := f.p.QueryRow(context.Background(), `SELECT manifest_status FROM call_session WHERE id=$1::uuid`, m.SessionID).Scan(&status); e != nil {
t.Fatal(e)
}
if status != "REVOKED" {
t.Fatal("issued manifest survived deletion")
}
f.p.QueryRow(context.Background(), `SELECT privacy_epoch FROM memory_user_state WHERE subject_id=$1::uuid`, f.subject).Scan(&epoch)
if epoch != 1 {
t.Fatal(epoch)
}
if e := f.s.Maintain(context.Background()); e != nil {
t.Fatal(e)
}
progress := f.request(t, "mutations/status", map[string]any{"mutation_id": ci.ID})
stages := progress["stage_states"].(map[string]any)
if stages["active_context"] != "PENDING" || stages["backup_cleanup"] != "PENDING" || progress["mutation_state"] == "COMPLETED" {
t.Fatal(progress)
}
f.exec(t, `UPDATE call_session SET local_audio_off=true,sdk_stopped=true,remote_state='UNKNOWN' WHERE id=$1::uuid`, m.SessionID)
if e := f.s.Maintain(context.Background()); e != nil {
t.Fatal(e)
}
progress = f.request(t, "mutations/status", map[string]any{"mutation_id": ci.ID})
if progress["stage_states"].(map[string]any)["active_context"] != "PENDING" {
t.Fatal("local SDK evidence falsely proved supplier context cleanup")
}
}
func TestLeaseExpiresAndCannotResurrect(t *testing.T) {
f := setup(t)
m, k, d := f.call(t)
in := HeartbeatInput{CallKey: k, ManifestID: m.ManifestID, AttemptID: uuid.NewString(), Runtime: "STARTING"}
req := uuid.NewString()
first, e := f.s.DeviceRequest(context.Background(), d, nil, req, "heartbeat", jsonBytes(in))
if e != nil {
t.Fatal(e)
}
if first.(map[string]any)["allowed"] != true {
var details []byte
f.p.QueryRow(context.Background(), `SELECT jsonb_build_object('device',to_jsonb(d)-'credential_ref'-'serial_digest','binding',to_jsonb(b),'consents',(SELECT jsonb_agg(cr.consent_type) FROM consent_record cr WHERE cr.subject_id=b.subject_id)) FROM device d JOIN device_binding b ON b.device_id=d.id WHERE d.id=$1::uuid`, f.device).Scan(&details)
t.Log(string(details))
t.Fatal(first)
}
if first.(map[string]any)["transcript_upload_allowed"] != true {
t.Fatal("granted transcript upload not advertised")
}
f.exec(t, `UPDATE call_session SET lease_expires_at=now()-interval '1 second' WHERE id=$1::uuid`, m.SessionID)
late, e := f.s.DeviceRequest(context.Background(), d, nil, req, "heartbeat", jsonBytes(in))
if e != nil {
t.Fatal(e)
}
if late.(map[string]any)["allowed"] != false {
t.Fatal("cached heartbeat revived expired lease", late)
}
}
func TestTextRevisionDrainingAndConsent(t *testing.T) {
f := setup(t)
m, k, d := f.call(t)
attempt := uuid.NewString()
send := func(seq int64, rev int64, text string) map[string]any {
ev := Event{ID: uuid.NewString(), Seq: seq, Type: "TRANSCRIPT_REVISION", Occurred: time.Now().UnixMilli(), AttemptID: attempt, Payload: map[string]any{"utterance_id": "u1", "source_revision": rev, "speaker": "USER", "source_final": true, "text": text}}
out, e := f.s.DeviceRequest(context.Background(), d, nil, uuid.NewString(), "events", jsonBytes(EventsInput{CallKey: k, Events: []Event{ev}}))
if e != nil {
t.Fatal(e)
}
return out.(map[string]any)
}
first := send(1, 1, "明天看牙")
if len(first["accepted_event_ids"].([]string)) != 1 {
t.Fatal(first)
}
_, e := f.s.DeviceRequest(context.Background(), d, nil, uuid.NewString(), "end", jsonBytes(EndInput{CallKey: k, Reason: "ON_HOOK", AudioStopped: true, SDKStatus: "STOPPED_OBSERVED"}))
if e != nil {
t.Fatal(e)
}
f.exec(t, `UPDATE call_session SET drain_started_at=now()-interval '20 seconds',last_text_at=now()-interval '20 seconds' WHERE id=$1::uuid`, m.SessionID)
if e = f.s.Maintain(context.Background()); e != nil {
t.Fatal(e)
}
send(2, 2, "明天不去看牙")
var state, text string
var revision int64
e = f.p.QueryRow(context.Background(), `SELECT c.transcript_state,c.transcript_revision,u.text_content FROM call_session c JOIN call_utterance u ON u.session_id=c.id WHERE c.id=$1::uuid`, m.SessionID).Scan(&state, &revision, &text)
if e != nil {
t.Fatal(e)
}
if state != "DRAINING" || revision != 2 || text != "明天不去看牙" {
t.Fatal(state, revision, text)
}
f.exec(t, `UPDATE consent_record SET withdrawn_at=now() WHERE subject_id=$1::uuid AND consent_type='transcript_storage'`, f.subject)
rejected := send(3, 3, "不得保存")
if len(rejected["accepted_event_ids"].([]string)) != 0 {
t.Fatal(rejected)
}
}
func TestMemoryGrantIndependentOfDeviceRole(t *testing.T) {
f := setup(t)
f.principal.Subject = "00000000-0000-0000-0000-000000000102"
_, e := f.s.MemoryRequest(context.Background(), f.principal, "query", jsonBytes(QueryInput{ElderID: f.elder}))
if e == nil {
t.Fatal("editor inherited memory permission")
}
}
func TestPrepareIdempotenceCancelAndFirstHeartbeat(t *testing.T) {
f := setup(t)
ctx := context.Background()
profileID := "test-" + uuid.NewString()
caps, evidence, mapping, template := map[string]bool{}, map[string]string{}, map[string]string{}, map[string]any{}
for _, g := range []string{"H01", "H02", "H04", "H05", "H06", "H07", "H09", "H11"} {
caps[g] = true
evidence[g] = "synthetic integration test, no hardware claim"
}
for _, field := range []string{"memory_enabled", "hot_context", "welcome_text", "user_id", "assistant_id", "dialog_id"} {
mapping[field] = "/" + field
template[field] = ""
}
f.exec(t, `INSERT INTO call_provider_profile(id,protocol_version,sdk_version,bot_id,model_version,transport,audio_profile,vendor_template,field_mapping,capabilities,evidence,published) VALUES($1,'CALL-MEMORY-1.1','test-sdk','test-bot','test-model','RTC','{"id":"test-audio"}',$2::jsonb,$3::jsonb,$4::jsonb,$5::jsonb,true)`, profileID, jsonBytes(template), jsonBytes(mapping), jsonBytes(caps), jsonBytes(evidence))
f.exec(t, `UPDATE device SET activation_state='ACTIVE' WHERE id=$1::uuid`, f.device)
f.exec(t, `INSERT INTO device_activation_job(device_id,tenant_id,provider_device_name,status,binding_generation,config_version,config_ack_at,engine_ready_at,sdk_registered_at,config_expires_at,ready_binding_generation) VALUES($1::uuid,$2::uuid,$3,'READY',1,1,now(),now(),now(),now()+interval '1 hour',1)`, f.device, f.tenant, "test-"+f.device)
d := deviceauth.Identity{ID: f.device, PublicID: f.device, TenantID: f.tenant, EnrollmentMode: "physical_confirm", BindingGeneration: 1, ActivationState: "ACTIVE"}
key := []byte(strings.Repeat("k", 32))
if _, e := f.s.DeviceRequest(ctx, d, key, uuid.NewString(), "warmup", jsonBytes(WarmupInput{Contract: ContractVersion, KnownVersion: 999, BootID: f.boot, Binding: 1, SDK: "test-sdk", Firmware: "test", AudioID: "test-audio"})); e != nil {
t.Fatal(e)
}
k := CallKey{BootID: f.boot, HookID: "cancel-hook", Generation: 1, Binding: 1}
if _, e := f.s.DeviceRequest(ctx, d, key, uuid.NewString(), "end", jsonBytes(EndInput{CallKey: k, Reason: "CANCEL_BEFORE_START", AudioStopped: true, SDKStatus: "NOT_STARTED"})); e != nil {
t.Fatal(e)
}
if _, e := f.s.DeviceRequest(ctx, d, key, uuid.NewString(), "prepare", jsonBytes(PrepareInput{CallKey: k, AudioID: "test-audio", Purpose: "NORMAL"})); e == nil {
t.Fatal("end-before-prepare was resurrected")
}
k.HookID = "new-hook"
k.Generation = 2
input := jsonBytes(PrepareInput{CallKey: k, AudioID: "test-audio", Purpose: "NORMAL"})
first, e := f.s.DeviceRequest(ctx, d, key, uuid.NewString(), "prepare", input)
if e != nil {
t.Fatal(e)
}
second, e := f.s.DeviceRequest(ctx, d, key, uuid.NewString(), "prepare", input)
if e != nil {
t.Fatal(e)
}
if string(jsonBytes(first)) != string(jsonBytes(second)) {
t.Fatal("prepare created a second manifest")
}
signed := first.(map[string]any)
m, e := DecodeManifest(key, signed["manifest_b64"].(string), signed["manifest_mac"].(string))
if e != nil {
t.Fatal(e)
}
k.SessionID = &m.SessionID
attempt := uuid.NewString()
ev := EventsInput{CallKey: k, Events: []Event{{ID: uuid.NewString(), Seq: 1, Type: "START_INTENT", AttemptID: attempt, Occurred: time.Now().UnixMilli(), Payload: map[string]any{"manifest_id": m.ManifestID}}}}
out, e := f.s.DeviceRequest(ctx, d, key, uuid.NewString(), "events", jsonBytes(ev))
if e != nil {
t.Fatal(e)
}
if len(out.(map[string]any)["accepted_event_ids"].([]string)) != 0 {
t.Fatal("start accepted without first heartbeat")
}
if _, e = f.s.DeviceRequest(ctx, d, key, uuid.NewString(), "heartbeat", jsonBytes(HeartbeatInput{CallKey: k, ManifestID: m.ManifestID, AttemptID: attempt, Runtime: "STARTING"})); e != nil {
t.Fatal(e)
}
out, e = f.s.DeviceRequest(ctx, d, key, uuid.NewString(), "events", jsonBytes(ev))
if e != nil {
t.Fatal(e)
}
if len(out.(map[string]any)["accepted_event_ids"].([]string)) != 1 {
t.Fatal(out)
}
}
type projectionFunc func(context.Context, ProjectionRecord, string, string, bool) (ProjectionResult, error)
func (f projectionFunc) Project(c context.Context, r ProjectionRecord, k, id string, reconcile bool) (ProjectionResult, error) {
return f(c, r, k, id, reconcile)
}
func TestProjectionAckIsNotVisibilityAndWatermarkHasNoHoles(t *testing.T) {
f := setup(t)
_, one := f.add(t)
f.add(t)
f.add(t)
f.exec(t, `UPDATE memory_projection_job SET next_attempt_at='2000-01-01',state=CASE WHEN memory_revision=3 THEN 'VISIBLE' ELSE state END WHERE subject_id=$1::uuid`, f.subject)
visible := false
f.s.ConfigureProjection(projectionFunc(func(_ context.Context, r ProjectionRecord, _, _ string, _ bool) (ProjectionResult, error) {
return ProjectionResult{Kind: "event", ID: r.ID, Visible: visible}, nil
}))
if e := f.s.ProjectNext(context.Background()); e != nil {
t.Fatal(e)
}
status := f.request(t, "sync/status", map[string]any{"elder_id": f.elder})
if status["viking_visible_revision"].(float64) != 0 {
t.Fatal("ack counted as visible", status)
}
visible = true
f.exec(t, `UPDATE memory_projection_job SET next_attempt_at='2000-01-01' WHERE subject_id=$1::uuid`, f.subject)
if e := f.s.ProjectNext(context.Background()); e != nil {
t.Fatal(e)
}
status = f.request(t, "sync/status", map[string]any{"elder_id": f.elder})
if status["viking_visible_revision"].(float64) != 1 {
t.Fatal("watermark skipped gap at 2", status)
}
if e := f.s.Maintain(context.Background()); e != nil {
t.Fatal(e)
}
done := f.request(t, "mutations/status", map[string]any{"mutation_id": one["mutation_id"]})
if done["mutation_state"] != "COMPLETED" {
t.Fatal(done)
}
}
func TestSensitiveMutationStatusCannotBypassListGrant(t *testing.T) {
f := setup(t)
content, sensitivity := "需要权限的内容", "sensitive"
out := f.request(t, "mutations", MutationInput{ID: uuid.NewString(), ElderID: f.elder, Action: "ADD", Targets: []string{}, Expected: map[string]int64{}, Patch: MemoryPatch{Content: &content, Sensitivity: &sensitivity}})
f.request(t, "access/grant", AccessInput{Elder: f.elder, User: "10000000-0000-0000-0000-000000000112", Read: true})
f.principal.Subject = "00000000-0000-0000-0000-000000000102"
rows := f.request(t, "query", QueryInput{ElderID: f.elder})
if len(rows["items"].([]any)) != 0 {
t.Fatal(rows)
}
if _, e := f.s.MemoryRequest(context.Background(), f.principal, "mutations/status", jsonBytes(map[string]any{"mutation_id": out["mutation_id"]})); e == nil {
t.Fatal("sensitive status bypass")
}
}
func TestProjectionRechecksConsentAndSensitiveState(t *testing.T) {
f := setup(t)
id, _ := f.add(t)
// An expiry is a clock event, not an UPDATE of withdrawn_at.
f.exec(t, `UPDATE consent_record SET expires_at=now()+interval '1 hour' WHERE subject_id=$1::uuid AND consent_type='personalized_memory'`, f.subject)
var observed string
f.s.ConfigureProjection(projectionFunc(func(_ context.Context, r ProjectionRecord, _, _ string, _ bool) (ProjectionResult, error) {
observed = r.Status
f.exec(t, `UPDATE consent_record SET withdrawn_at=now() WHERE subject_id=$1::uuid AND consent_type='personalized_memory'`, f.subject)
return ProjectionResult{Kind: "event", ID: r.ID, Visible: true}, nil
}))
if e := f.s.ProjectNext(context.Background()); e != nil {
t.Fatal(e)
}
if observed != "ACTIVE" {
t.Fatal(observed)
}
var visible int64
if e := f.p.QueryRow(context.Background(), `SELECT viking_visible_revision FROM memory_user_state WHERE subject_id=$1::uuid`, f.subject).Scan(&visible); e != nil {
t.Fatal(e)
}
if visible != 0 {
t.Fatal("concurrent consent withdrawal advanced visibility")
}
f.s.ConfigureProjection(projectionFunc(func(_ context.Context, r ProjectionRecord, _, _ string, _ bool) (ProjectionResult, error) {
if r.ID == id && r.Status != "DISABLED" {
t.Fatal("withdrawn consent projected as active")
}
return ProjectionResult{Kind: "event", ID: r.ID, Visible: true}, nil
}))
if e := f.s.ProjectNext(context.Background()); e != nil {
t.Fatal(e)
}
// A separate subject with a sensitive ALLOWED record must not publish plaintext.
other := setup(t)
content, sensitivity := "private medical detail", "sensitive"
other.request(t, "mutations", MutationInput{ID: uuid.NewString(), ElderID: other.elder, Action: "ADD", Targets: []string{}, Expected: map[string]int64{}, Patch: MemoryPatch{Content: &content, Sensitivity: &sensitivity}})
f.exec(t, `UPDATE memory_projection_job SET next_attempt_at=now()+interval '1 hour' WHERE subject_id<>$1::uuid`, other.subject)
other.s.ConfigureProjection(projectionFunc(func(_ context.Context, r ProjectionRecord, _, _ string, _ bool) (ProjectionResult, error) {
if r.Status != "DISABLED" {
t.Fatal("sensitive memory eligible for projection")
}
return ProjectionResult{Kind: "event", Visible: true}, nil
}))
if e := other.s.ProjectNext(context.Background()); e != nil {
t.Fatal(e)
}
}
type noObjects struct{}
func (noObjects) Remove(context.Context, string) error { return nil }
func TestPrivacyDeleteIncludesNewCallDataAndWaitsForRuntime(t *testing.T) {
f := setup(t)
ctx := context.Background()
_, mutation := f.add(t)
m, _, _ := f.call(t)
f.exec(t, `INSERT INTO call_utterance(session_id,utterance_id,source_revision,speaker,text_content,interrupted,source_at) VALUES($1::uuid,'u1',1,'USER','private final text',false,now())`, m.SessionID)
story, _ := addCallSourceStory(t, f, m.SessionID, "u1", 1)
f.exec(t, `UPDATE call_session SET budget_snapshot='{"protocol":"DIRECT-CHAT-1","story_context":{"private":"original words"}}'::jsonb WHERE id=$1::uuid`, m.SessionID)
f.exec(t, `INSERT INTO outbox_event(tenant_id,aggregate_type,aggregate_id,event_type,payload) VALUES($1::uuid,'call_session',$2::uuid,'call.story_extraction_requested','{"transcript_revision":1}'::jsonb)`, f.tenant, m.SessionID)
request := uuid.NewString()
f.exec(t, `INSERT INTO privacy_request(id,tenant_id,subject_id,request_type,scope,requested_by,idempotency_key,request_digest) VALUES($1::uuid,$2::uuid,$3::uuid,'DELETE','{}','10000000-0000-0000-0000-000000000101',$1,repeat('a',64))`, request, f.tenant, f.subject)
f.exec(t, `INSERT INTO privacy_subtask(privacy_request_id,subsystem,target_type,target_ref) VALUES($1::uuid,'postgresql','subject',$2)`, request, f.subject)
cache := redis.NewClient(&redis.Options{Addr: "127.0.0.1:1"})
defer cache.Close()
processor, e := privacy.NewDeleteProcessor(f.p, cache, noObjects{})
if e != nil {
t.Fatal(e)
}
if e = processor.Handle(ctx, outbox.Event{AggregateID: request}); e == nil || !outbox.IsRetryable(e) || outbox.RetryDelay(e) < time.Minute {
t.Fatalf("unproved runtime/projection must hold deletion open with a delayed retry: %v", e)
}
var text, patch string
var manifest *string
if e = f.p.QueryRow(ctx, `SELECT text_content FROM call_utterance WHERE session_id=$1::uuid`, m.SessionID).Scan(&text); e != nil {
t.Fatal(e)
}
if text != "[deleted]" {
t.Fatal("new call transcript survived privacy deletion")
}
if e = f.p.QueryRow(ctx, `SELECT manifest_b64 FROM call_session WHERE id=$1::uuid`, m.SessionID).Scan(&manifest); e != nil {
t.Fatal(e)
}
if manifest != nil {
t.Fatal("manifest retained private context")
}
if e = f.p.QueryRow(ctx, `SELECT patch::text FROM memory_mutation WHERE id=$1::uuid`, mutation["mutation_id"]).Scan(&patch); e != nil {
t.Fatal(e)
}
if patch != "{}" {
t.Fatal("mutation retained deleted plaintext")
}
var storyStatus, storyContent, extractionStatus string
var hasContext bool
if e = f.p.QueryRow(ctx, `SELECT sd.status,sr.content FROM story_draft sd JOIN story_revision sr ON sr.id=sd.current_revision_id WHERE sd.id=$1::uuid`, story).Scan(&storyStatus, &storyContent); e != nil {
t.Fatal(e)
}
if storyStatus != "SUPERSEDED" || storyContent != "[已按隐私请求删除]" {
t.Fatal("direct-call story survived privacy deletion")
}
if e = f.p.QueryRow(ctx, `SELECT budget_snapshot ? 'story_context' FROM call_session WHERE id=$1::uuid`, m.SessionID).Scan(&hasContext); e != nil || hasContext {
t.Fatalf("direct-call story context survived privacy deletion: %v", e)
}
if e = f.p.QueryRow(ctx, `SELECT status FROM outbox_event WHERE aggregate_id=$1::uuid AND event_type='call.story_extraction_requested'`, m.SessionID).Scan(&extractionStatus); e != nil || extractionStatus != "completed" {
t.Fatalf("direct-call extraction job survived privacy deletion: status=%s err=%v", extractionStatus, e)
}
// Synthetic local/remote/backup evidence; this is not hardware acceptance.
f.exec(t, `UPDATE memory_projection_job SET state='VISIBLE' WHERE subject_id=$1::uuid`, f.subject)
f.exec(t, `UPDATE memory_deletion_ledger SET backup_cleanup_verified_at=now(),evidence_ref='synthetic test' WHERE subject_id=$1::uuid`, f.subject)
if e = processor.Handle(ctx, outbox.Event{AggregateID: request}); e == nil {
t.Fatal("missing device stop was ignored")
}
f.exec(t, `UPDATE call_session SET local_audio_off=true,sdk_stopped=true,remote_state='STOPPED_OBSERVED' WHERE id=$1::uuid`, m.SessionID)
if e = processor.Handle(ctx, outbox.Event{AggregateID: request}); e != nil {
t.Fatal(e)
}
var state string
if e = f.p.QueryRow(ctx, `SELECT status FROM privacy_request WHERE id=$1::uuid`, request).Scan(&state); e != nil {
t.Fatal(e)
}
if state != "COMPLETED" {
t.Fatal(state)
}
}
func TestLateRevisionCannotRestoreDeletedSource(t *testing.T) {
f := setup(t)
ctx := context.Background()
m, k, d := f.call(t)
id, _ := f.add(t)
f.exec(t, `INSERT INTO memory_source_suppression(subject_id,memory_id,source_refs,privacy_epoch,scope) SELECT subject_id,id,$2::jsonb,0,'SOURCE_AND_DERIVED' FROM curated_memory WHERE public_id=$1::uuid`, id, jsonBytes([]map[string]any{{"session_id": m.SessionID, "utterance_id": "erased-source"}}))
event := Event{ID: uuid.NewString(), Seq: 1, Type: "TRANSCRIPT_REVISION", AttemptID: uuid.NewString(), Occurred: time.Now().UnixMilli(), Payload: map[string]any{"utterance_id": "erased-source", "source_revision": 2, "speaker": "USER", "source_final": true, "text": "must stay erased"}}
result, e := f.s.DeviceRequest(ctx, d, nil, uuid.NewString(), "events", jsonBytes(EventsInput{CallKey: k, Events: []Event{event}}))
if e != nil {
t.Fatal(e)
}
raw := string(jsonBytes(result))
if !strings.Contains(raw, "SOURCE_REDACTED") {
t.Fatal(raw)
}
var count int
if e = f.p.QueryRow(ctx, `SELECT count(*) FROM call_utterance WHERE session_id=$1::uuid`, m.SessionID).Scan(&count); e != nil {
t.Fatal(e)
}
if count != 0 {
t.Fatal("late final recreated source text")
}
}
func TestStatusUsesDocumentKeyWithoutRenewingLease(t *testing.T) {
f := setup(t)
m, k, d := f.call(t)
ctx := context.Background()
var before, after time.Time
if e := f.p.QueryRow(ctx, `SELECT lease_expires_at FROM call_session WHERE id=$1::uuid`, m.SessionID).Scan(&before); e != nil {
t.Fatal(e)
}
input := StatusInput{SessionID: k.SessionID, BootID: k.BootID, HookID: k.HookID}
if _, e := f.s.DeviceRequest(ctx, d, nil, uuid.NewString(), "status", jsonBytes(input)); e != nil {
t.Fatal(e)
}
if e := f.p.QueryRow(ctx, `SELECT lease_expires_at FROM call_session WHERE id=$1::uuid`, m.SessionID).Scan(&after); e != nil {
t.Fatal(e)
}
if !before.Equal(after) {
t.Fatal("status renewed lease")
}
input.BootID = uuid.NewString()
if _, e := f.s.DeviceRequest(ctx, d, nil, uuid.NewString(), "status", jsonBytes(input)); e == nil {
t.Fatal("status accepted another boot identity")
}
}