package callmemory import ( "context" "testing" "github.com/google/uuid" ) func addCallSourceStory(t *testing.T, f fixture, session, utterance string, generation int) (string, string) { t.Helper() story, revision, fact := uuid.NewString(), uuid.NewString(), uuid.NewString() f.exec(t, `INSERT INTO story_draft(id,public_id,tenant_id,subject_id,call_session_id,call_privacy_epoch,title,generation_version) VALUES($1::uuid,$1::uuid,$2::uuid,$3::uuid,$4::uuid,0,'private story',$5)`, story, f.tenant, f.subject, session, generation) f.exec(t, `INSERT INTO story_revision(id,story_id,revision_no,content,author_type,edit_reason) VALUES($1::uuid,$2::uuid,1,'private story content','model','private edit reason')`, revision, story) f.exec(t, `INSERT INTO story_source(story_revision_id,call_session_id,call_utterance_id,call_source_revision,source_role) VALUES($1::uuid,$2::uuid,$3,1,'subject')`, revision, session, utterance) f.exec(t, `UPDATE story_draft SET current_revision_id=$2::uuid WHERE id=$1::uuid`, story, revision) f.exec(t, `INSERT INTO fact_candidate(id,tenant_id,subject_id,story_id,fact_type,content,statement_basis,source_turn_ids,generation_version) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,'event','private fact','subject_statement',jsonb_build_array($5::text),$6)`, fact, f.tenant, f.subject, story, utterance, generation) f.exec(t, `INSERT INTO fact_candidate_revision(fact_candidate_id,version_no,fact_type,content,sensitivity,status,edit_reason) VALUES($1::uuid,1,'event','private historical fact','normal','CANDIDATE','private edit reason')`, fact) return story, revision } func TestCallSourceDeletionRedactsLinkedStoryAndPreservesUnrelatedTurns(t *testing.T) { for _, refType := range []string{"utterance", "story", "session"} { t.Run(refType, func(t *testing.T) { f := setup(t) ctx := context.Background() memory, _ := f.add(t) call, _, _ := f.call(t) for _, utterance := range []string{"removed", "kept"} { f.exec(t, `INSERT INTO call_utterance(session_id,utterance_id,source_revision,speaker,text_content,interrupted,source_at) VALUES($1::uuid,$2,1,'USER',$2,false,now())`, call.SessionID, utterance) } f.exec(t, `INSERT INTO call_text_fragment(session_id,utterance_id,source_revision,part_index,part_count,speaker,text_content,content_sha256,is_final,source_at) VALUES($1::uuid,'fragment-only',1,0,2,'USER','private partial text',repeat('a',64),true,now())`, call.SessionID) f.exec(t, `INSERT INTO call_event(session_id,event_id,seq,event_type,digest,payload) VALUES($1::uuid,'source-test-event',1,'TRANSCRIPT_FINAL',repeat('a',64),'{"payload":{"utterance_id":"removed","text":"private event text"}}'::jsonb)`, call.SessionID) story, revision := addCallSourceStory(t, f, call.SessionID, "removed", 1) unrelatedStory, _ := addCallSourceStory(t, f, call.SessionID, "kept", 2) // Both utterances occur in the first story. Deleting just one source // invalidates that story, without erasing its other original source. if refType == "utterance" { f.exec(t, `INSERT INTO story_source(story_revision_id,call_session_id,call_utterance_id,call_source_revision,source_role) VALUES($1::uuid,$2::uuid,'kept',1,'subject')`, revision, call.SessionID) } refs := []map[string]any{{"session_id": call.SessionID, "utterance_id": "removed"}} if refType == "story" { refs = []map[string]any{{"type": "story", "story_id": story, "revision_no": 1}} } else if refType == "session" { refs = []map[string]any{{"session_id": call.SessionID}} } 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`, memory, jsonBytes(refs)) tx, err := f.p.Begin(ctx) if err != nil { t.Fatal(err) } defer tx.Rollback(ctx) if err = f.s.cleanSources(ctx, tx, f.subject, jsonBytes([]string{memory}), "SOURCE_AND_DERIVED"); err != nil { t.Fatal(err) } if err = tx.Commit(ctx); err != nil { t.Fatal(err) } var removed, kept, title, status, content, fact, historicalFact string var editReason *string if err = f.p.QueryRow(ctx, `SELECT text_content FROM call_utterance WHERE session_id=$1::uuid AND utterance_id='removed'`, call.SessionID).Scan(&removed); err != nil { t.Fatal(err) } if err = f.p.QueryRow(ctx, `SELECT text_content FROM call_utterance WHERE session_id=$1::uuid AND utterance_id='kept'`, call.SessionID).Scan(&kept); err != nil { t.Fatal(err) } if err = f.p.QueryRow(ctx, `SELECT sd.title,sd.status,sr.content,fc.content,fcr.content,sr.edit_reason FROM story_draft sd JOIN story_revision sr ON sr.id=sd.current_revision_id JOIN fact_candidate fc ON fc.story_id=sd.id JOIN fact_candidate_revision fcr ON fcr.fact_candidate_id=fc.id WHERE sd.id=$1::uuid`, story).Scan(&title, &status, &content, &fact, &historicalFact, &editReason); err != nil { t.Fatal(err) } if removed != "[deleted]" || title != "[deleted]" || status != "SUPERSEDED" || content != "[deleted]" || fact != "[deleted]" || historicalFact != "[deleted]" || editReason != nil { t.Fatalf("source or derived content survived: %q %q %q %q %q %q %v", removed, title, status, content, fact, historicalFact, editReason) } var unrelatedStatus, fragment, event string if err = f.p.QueryRow(ctx, `SELECT status FROM story_draft WHERE id=$1::uuid`, unrelatedStory).Scan(&unrelatedStatus); err != nil { t.Fatal(err) } if err = f.p.QueryRow(ctx, `SELECT text_content FROM call_text_fragment WHERE session_id=$1::uuid`, call.SessionID).Scan(&fragment); err != nil { t.Fatal(err) } if err = f.p.QueryRow(ctx, `SELECT payload::text FROM call_event WHERE session_id=$1::uuid`, call.SessionID).Scan(&event); err != nil || event != `{"redacted": true}` { t.Fatalf("source event survived deletion: %q %v", event, err) } if refType == "session" { if kept != "[deleted]" || unrelatedStatus != "SUPERSEDED" || fragment != "[deleted]" { t.Fatalf("session deletion left a source, story or partial transcript: %q %q %q", kept, unrelatedStatus, fragment) } } else if kept != "kept" || unrelatedStatus != "CANDIDATE" || fragment != "private partial text" { t.Fatalf("unrelated source or story changed: %q %q %q", kept, unrelatedStatus, fragment) } }) } }