feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
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:
@@ -0,0 +1,117 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user