package creator import ( "errors" "fmt" "os" "path/filepath" "testing" "time" ) func TestCreatorPostgresWorkCoverCache(t *testing.T) { store, _, ctx := openCreatorIntegrationStore(t) stamp := fmt.Sprintf("%d", time.Now().UnixNano()) competitor, err := store.UpsertCompetitor(ctx, CompetitorInput{ Platform: PlatformDouyin, PlatformAccountKey: "MS4wLjABAAAAcover" + stamp, Nickname: "温江菜花小院", HomepageURL: "https://www.douyin.com/user/cover" + stamp, }) if err != nil { t.Fatal(err) } now := time.Now().UTC().Add(-time.Hour) work, inserted, err := store.UpsertWork(ctx, WorkInput{ Platform: PlatformDouyin, WorkKey: "cover-work-" + stamp, SourceType: SourceCompetitor, SourceID: competitor.ID, AuthorName: "木子不写代码", Title: "标题", PublishedAt: &now, PublishedAtStatus: "verified", CoverURL: "https://p3-pc-sign.douyinpic.com/cover.jpeg", }, now) if err != nil || !inserted { t.Fatalf("upsert work: inserted=%v err=%v", inserted, err) } missing, err := store.ListWorksMissingCover(ctx, SourceCompetitor, competitor.ID) if err != nil || len(missing) != 1 || missing[0].ID != work.ID { t.Fatalf("missing covers: %v %v", missing, err) } for _, item := range []struct{ mediaType, extension, bytes string }{{"image/jpeg", ".jpg", "jpeg-bytes"}, {"image/webp", ".webp", "webp-bytes"}} { if err := store.SaveWorkCover(ctx, work.ID, item.mediaType, []byte(item.bytes)); err != nil { t.Fatal(err) } path, err := store.GetWorkCover(ctx, work.ID) if err != nil { t.Fatal(err) } data, err := os.ReadFile(path) if err != nil || filepath.Ext(path) != item.extension || string(data) != item.bytes { t.Fatalf("cover: %s %q %v", path, data, err) } missing, err = store.ListWorksMissingCover(ctx, SourceCompetitor, competitor.ID) if err != nil || len(missing) != 0 { t.Fatalf("cached works still missing: %v %v", missing, err) } } if _, err := store.GetWorkCover(ctx, "missing"); !errors.Is(err, ErrNotFound) { t.Fatalf("missing cover: %v", err) } if err := store.SaveWorkCover(ctx, work.ID, "image/jpeg", nil); !errors.Is(err, ErrInvalid) { t.Fatalf("empty cover accepted: %v", err) } } func TestCreatorPostgresPageAndConversationState(t *testing.T) { store, phaseAStore, ctx := openCreatorIntegrationStore(t) if err := store.Ping(ctx); err != nil { t.Fatal(err) } store.SetSecretBridge(nil) if err := store.EnsureSchema(ctx); err != nil { t.Fatal(err) } accountID := createIntegrationAccount(t, ctx, phaseAStore, "pages") if err := store.EnsureAccountProfile(ctx, accountID); err != nil { t.Fatal(err) } now := time.Now().UTC().Add(-time.Hour) work, inserted, err := store.UpsertWork(ctx, WorkInput{ Platform: PlatformDouyin, WorkKey: "page-work", SourceType: SourceOwned, SourceID: accountID, AuthorName: "author", Title: "title", Body: "body", PublishedAt: &now, PublishedAtStatus: "verified", }, now) if err != nil || !inserted { t.Fatalf("upsert work: inserted=%v err=%v", inserted, err) } comment, inserted, err := store.SaveComment(ctx, CommentInput{ Platform: PlatformDouyin, CommentKey: "page-comment", WorkID: work.ID, AuthorUID: "peer", AuthorName: "Peer", Content: "hello", CommentType: "top_level", PublishedAt: &now, }) if err != nil || !inserted { t.Fatalf("save comment: inserted=%v err=%v", inserted, err) } workPage, err := store.ListWorksPage(ctx, WorkFilter{Platform: PlatformDouyin, SourceID: accountID, SourceType: SourceOwned}, 1, 1) if err != nil || len(workPage.Data) != 1 || workPage.Total != 1 { t.Fatalf("list works page: page=%+v err=%v", workPage, err) } commentPage, err := store.ListCommentsPage(ctx, PlatformDouyin, work.ID, 1, 1) if err != nil || len(commentPage.Data) != 1 || commentPage.Total != 1 { t.Fatalf("list comments page: page=%+v err=%v", commentPage, err) } if _, _, err := store.SaveComment(ctx, CommentInput{Platform: PlatformDouyin, CommentKey: "page-comment", WorkID: work.ID, Content: "updated", CommentType: "top_level"}); err != nil { t.Fatal(err) } if _, _, err := store.NextCollectionWindow(ctx, SourceOwned, accountID, now, time.Hour, 1); err != nil { t.Fatal(err) } if err := store.MarkCollectionBlocked(ctx, SourceOwned, accountID, "coverage block", now, 1); err != nil { t.Fatal(err) } if _, _, err := store.NextCollectionWindow(ctx, SourceOwned, accountID, now, time.Hour, 1); err != nil { t.Fatal(err) } if works, err := store.ListDueMetricWorks(ctx, now); err != nil || works == nil { t.Fatalf("list due metric works: works=%v err=%v", works, err) } settings, err := store.GetSettings(ctx) if err != nil { t.Fatal(err) } if err := store.EnsureMetricPlan(ctx, work.ID, settings); err != nil { t.Fatalf("ensure metric plan: %v", err) } if err := store.StopMetricPlan(ctx, work.ID, "coverage stop"); err != nil { t.Fatal(err) } lease, err := store.ClaimSourceSync(ctx, SourceOwned, accountID) if err != nil { t.Fatal(err) } if _, err := store.ClaimSourceSync(ctx, SourceOwned, accountID); !errors.Is(err, ErrConflict) { t.Fatalf("claim active source lease: %v", err) } if err := store.ReleaseSourceSync(ctx, SourceOwned, accountID, "wrong-token"); !errors.Is(err, ErrConflict) { t.Fatalf("release wrong lease token: %v", err) } if err := store.ReleaseSourceSync(ctx, SourceOwned, accountID, lease); err != nil { t.Fatal(err) } if _, err := store.RecordLoginResult(ctx, accountID, "needs_login", "coverage", ""); err != nil { t.Fatal(err) } if _, err := store.RecordVerifiedLoginResult(ctx, accountID, "sec_uid_"+accountID); err != nil { t.Fatal(err) } rule, err := store.CreateRule(ctx, LeadRuleInput{Name: "coverage rule", Enabled: true, SourceType: SourceOwned, Topic: "title", IncludeKeywords: []string{"hello"}, AIRequirement: "business lead"}) if err != nil { t.Fatal(err) } analysis, err := store.AnalyzeComments(ctx, []string{comment.ID, comment.ID}, rule.ID, nil) if len(analysis) != 1 || !errors.Is(err, ErrUnavailable) { t.Fatalf("analyze comments without AI: analysis=%v err=%v", analysis, err) } missing := []struct { name string get func() error }{ {"comment", func() error { _, err := store.GetComment(ctx, "missing"); return err }}, {"comment by key", func() error { _, err := store.GetCommentByKey(ctx, PlatformDouyin, "missing"); return err }}, {"competitor", func() error { _, err := store.GetCompetitor(ctx, "missing"); return err }}, {"rule", func() error { _, err := store.GetRule(ctx, "missing"); return err }}, {"work", func() error { _, err := store.GetWork(ctx, "missing"); return err }}, {"work by key", func() error { _, err := store.GetWorkByKey(ctx, PlatformDouyin, "missing"); return err }}, } for _, item := range missing { if err := item.get(); !errors.Is(err, ErrNotFound) { t.Fatalf("get missing %s: %v", item.name, err) } } for _, item := range []struct { name string set func() error }{ {"competitor", func() error { _, err := store.SetCompetitorEnabled(ctx, "missing", true); return err }}, {"rule", func() error { _, err := store.SetRuleEnabled(ctx, "missing", true); return err }}, } { if err := item.set(); !errors.Is(err, ErrNotFound) { t.Fatalf("set missing %s: %v", item.name, err) } } }