package dataexport import ( "context" "encoding/json" "os" "testing" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) func TestDirectStoryExportIncludesNativeEvidenceAndHonorsAvailability(t *testing.T) { url := os.Getenv("TEST_DATABASE_URL") if url == "" { t.Skip("TEST_DATABASE_URL required") } ctx := context.Background() pool, err := pgxpool.New(ctx, url) if err != nil { t.Fatal(err) } defer pool.Close() exec := func(query string, args ...any) { t.Helper() if _, err := pool.Exec(ctx, query, args...); err != nil { t.Fatal(err) } } subject, session, story, revision := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString() const tenant = "10000000-0000-0000-0000-000000000001" exec(`INSERT INTO subject_profile(id,tenant_id,household_id,preferred_name,status) VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000201','native-export','active')`, subject, tenant) exec(`INSERT INTO memory_user_state(subject_id,tenant_id) VALUES($1::uuid,$2::uuid)`, subject, 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 call_session(id,device_id,tenant_id,subject_id,boot_id,hook_event_id,call_generation,binding_generation,call_state,transcript_state,transcript_revision,budget_snapshot,ended_at) VALUES($1::uuid,'10000000-0000-0000-0000-000000000501',$2::uuid,$3::uuid,$1::uuid,$1,0,0,'ENDED','COMPLETE',1,'{"protocol":"DIRECT-CHAT-1","story_extraction_allowed":true}',now())`, session, tenant, subject) exec(`INSERT INTO call_utterance(session_id,utterance_id,source_revision,speaker,text_content,interrupted,source_at) VALUES($1::uuid,'native-evidence',1,'USER','小时候和爷爷一起种树。',false,now())`, session) exec(`INSERT INTO story_draft(id,tenant_id,subject_id,call_session_id,call_privacy_epoch,title,generation_version) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,0,'和爷爷种树',1)`, story, tenant, subject, session) exec(`INSERT INTO story_revision(id,story_id,revision_no,content,author_type) VALUES($1::uuid,$2::uuid,1,'小时候和爷爷一起种树。','model')`, revision, story) exec(`INSERT INTO story_source(story_revision_id,call_session_id,call_utterance_id,call_source_revision,source_role) VALUES($1::uuid,$2::uuid,'native-evidence',1,'subject')`, revision, session) exec(`UPDATE story_draft SET current_revision_id=$2::uuid WHERE id=$1::uuid`, story, revision) processor, err := NewProcessor(pool, &exportTestStore{}, time.Hour) if err != nil { t.Fatal(err) } type exportedStory struct { Session string `json:"call_session_id"` Conversation *string `json:"conversation_id"` Sources []struct { Session string `json:"call_session_id"` Utterance string `json:"utterance_id"` Revision int64 `json:"source_revision"` Text string `json:"text"` } `json:"sources"` } read := func() []exportedStory { t.Helper() raw, err := processor.collectCategory(ctx, "stories", tenant, subject) if err != nil { t.Fatal(err) } var items []exportedStory if err := json.Unmarshal(raw, &items); err != nil { t.Fatal(err) } return items } items := read() if len(items) != 1 || items[0].Session != session || items[0].Conversation != nil || len(items[0].Sources) != 1 || items[0].Sources[0].Session != session || items[0].Sources[0].Utterance != "native-evidence" || items[0].Sources[0].Revision != 1 || items[0].Sources[0].Text != "小时候和爷爷一起种树。" { t.Fatalf("native export=%+v", items) } exec(`UPDATE call_utterance SET source_revision=2 WHERE session_id=$1::uuid`, session) if items := read(); len(items) != 0 { t.Fatalf("stale source export=%+v", items) } }