Files
story/internal/extraction/events_test.go
T

118 lines
6.3 KiB
Go
Raw Normal View History

package extraction
import (
"context"
"data-terminal-cloud/internal/outbox"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"os"
"strings"
"testing"
)
func TestEventComparisonRejectsUnknownStoryAndPreservesNewFacts(t *testing.T) {
in := GenerationInput{PreviousStories: []PreviousStory{{ID: "old", Content: "我1970年进厂。"}}}
r := GenerationResult{Meaningful: true, Stories: []EventStory{{Title: "进厂", Content: "进厂的故事", FactContent: "1970年进厂", MatchID: "other"}}}
if normalizeEvents(&r, in) == nil {
t.Fatal("unknown matched story accepted")
}
r.Stories[0].MatchID = "old"
r.Stories[0].NewDetails = []string{"我1970年进厂。", "娘把杯子洗好了。"}
if e := normalizeEvents(&r, in); e != nil || len(r.Stories[0].NewDetails) != 1 {
t.Fatalf("result=%v err=%v", r, e)
}
}
func TestDailyEventsExcludePlansHabitsAndPast(t *testing.T) {
r := GenerationResult{DailyEvents: []DailyEvent{
{Title: "晚饭", Content: "今天做了白菜炖土豆。", TimeScope: "today_actual"},
{Title: "明天上班", Content: "明天去厂里。", TimeScope: "future"},
{Title: "做手套", Content: "前几天答应做手套,还没动手。", TimeScope: "past"},
{Title: "习惯", Content: "通常下班会喝水。", TimeScope: "habit"},
}}
if e := normalizeEvents(&r, GenerationInput{}); e != nil || len(r.DailyEvents) != 1 || r.DailyEvents[0].Title != "晚饭" {
t.Fatalf("daily=%v err=%v", r.DailyEvents, e)
}
}
func TestImportedEventsSplitDeduplicateAndSupplement(t *testing.T) {
if os.Getenv("TEST_DATABASE_URL") == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, e := pgxpool.New(ctx, os.Getenv("TEST_DATABASE_URL"))
if e != nil {
t.Fatal(e)
}
defer pool.Close()
subject := uuid.NewString()
tenant := "10000000-0000-0000-0000-000000000001"
user := "10000000-0000-0000-0000-000000000101"
exec := func(sql string, args ...any) {
t.Helper()
if _, e := pool.Exec(ctx, sql, args...); e != nil {
t.Fatal(e)
}
}
exec(`INSERT INTO subject_profile(id,tenant_id,household_id,preferred_name) VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000201','回忆录测试')`, subject, tenant)
exec(`INSERT INTO consent_record(tenant_id,subject_id,consent_type,policy_version,notice_digest,actor_user_id,granted_at) SELECT $1::uuid,$2::uuid,k,'test','test',$3::uuid,now()-interval '1 hour' FROM unnest(ARRAY['transcript_storage','story_extraction']) k`, tenant, subject, user)
t.Cleanup(func() {
for _, sql := range []string{`DELETE FROM story_generation_event WHERE story_id IN(SELECT id FROM story_draft WHERE subject_id=$1::uuid)`, `DELETE FROM fact_candidate WHERE subject_id=$1::uuid`, `UPDATE story_draft SET current_revision_id=NULL WHERE subject_id=$1::uuid`, `DELETE FROM story_revision WHERE story_id IN(SELECT id FROM story_draft WHERE subject_id=$1::uuid)`, `DELETE FROM story_draft WHERE subject_id=$1::uuid`, `DELETE FROM chat_daily_event WHERE subject_id=$1::uuid`, `DELETE FROM memoir_import_turn WHERE import_id IN(SELECT id FROM memoir_chat_import WHERE subject_id=$1::uuid)`, `DELETE FROM memoir_chat_import WHERE subject_id=$1::uuid`, `DELETE FROM consent_record WHERE subject_id=$1::uuid`, `DELETE FROM subject_profile WHERE id=$1::uuid`} {
_, _ = pool.Exec(ctx, sql, subject)
}
})
makeImport := func(text string) string {
id := uuid.NewString()
exec(`INSERT INTO memoir_chat_import(id,tenant_id,subject_id,created_by,title,transcript_digest)VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,'test','test')`, id, tenant, subject, user)
exec(`INSERT INTO memoir_import_turn(import_id,sequence_no,speaker,content)VALUES($1::uuid,1,'subject',$2),($1::uuid,2,'agent','助手编造:开过飞船')`, id, text)
return id
}
mode := 0
generator := directStoryGeneratorFunc(func(_ context.Context, in GenerationInput) (GenerationResult, error) {
if strings.Contains(in.Content, "飞船") {
t.Fatal("assistant speech included")
}
sid := strings.Split(strings.Split(in.Content, "]")[0], "[")[1]
stories := []EventStory{{Title: "进厂第一天", Content: "1970年我第一次进厂。", FactContent: "1970年进厂", EventTime: "1970年", SourceIDs: []string{sid}}, {Title: "结婚搬家", Content: "1973年我搬了家。", FactContent: "1973年搬家", EventTime: "1973年", SourceIDs: []string{sid}}}
if mode > 0 {
stories = stories[:1]
for _, old := range in.PreviousStories {
if old.Title == "进厂第一天" {
stories[0].MatchID = old.ID
}
}
}
if mode == 2 {
stories[0].NewDetails = []string{"第一天下班围裙上沾满线头。"}
}
return GenerationResult{Meaningful: true, StoryWorthy: true, AllowStory: true, Confidence: 0.9, ModelRef: "test", PromptVersion: "events-test", Stories: stories, DailyEvents: []DailyEvent{{Title: "今天晚饭", Content: "今天做了白菜炖土豆。", SourceIDs: []string{sid}}}}, nil
})
p, _ := NewProcessor(pool, generator)
run := func(id string) {
t.Helper()
if e := p.HandleImport(ctx, outbox.Event{AggregateID: id}); e != nil {
t.Fatal(e)
}
}
first := makeImport("1970年我进厂,1973年搬家,今天吃了晚饭。")
run(first)
run(first)
mode = 1
run(makeImport("1970年我进厂。"))
var count int
var version int64
var content string
if e = pool.QueryRow(ctx, `SELECT count(*) FROM story_draft WHERE subject_id=$1::uuid`, subject).Scan(&count); e != nil || count != 2 {
t.Fatalf("count=%d err=%v", count, e)
}
mode = 2
run(makeImport("1970年我进厂,第一天下班围裙上沾满线头。"))
if e = pool.QueryRow(ctx, `SELECT sd.version,sr.content FROM story_draft sd JOIN story_revision sr ON sr.id=sd.current_revision_id WHERE sd.subject_id=$1::uuid AND sd.title='进厂第一天'`, subject).Scan(&version, &content); e != nil || version != 2 || !strings.Contains(content, "1970") || !strings.Contains(content, "线头") {
t.Fatalf("version=%d content=%s err=%v", version, content, e)
}
// A merged story becomes unavailable if any contributing original is erased.
exec(`UPDATE memoir_import_turn SET content='[deleted]' WHERE import_id=$1::uuid AND speaker='subject'`, first)
var available bool
if e = pool.QueryRow(ctx, `SELECT call_story_available(id) FROM story_draft WHERE subject_id=$1::uuid AND title='进厂第一天'`, subject).Scan(&available); e != nil || available {
t.Fatalf("erased source still available: %v %v", available, e)
}
}