Files

502 lines
25 KiB
Go
Raw Permalink Normal View History

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