删除任务链(drafts/confirmations/tasks/attempts)、creator 运营链(策略/事件/操作/会话/消息/监听器/素材/关系)、runtime_use_lease 机制及其 API/UI;taskstate 包与对应测试一并移除。
180 lines
7.5 KiB
Go
180 lines
7.5 KiB
Go
package creator
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"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.Fatalf("upsert competitor: %v", 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)
|
|
}
|
|
// 采集到封面 URL 但尚未缓存 → 出现在回填列表。
|
|
missing, err := store.ListWorksMissingCover(ctx, competitor.ID)
|
|
if err != nil || len(missing) != 1 || missing[0].ID != work.ID {
|
|
t.Fatalf("list missing covers: works=%+v err=%v", missing, err)
|
|
}
|
|
if err := store.SaveWorkCover(ctx, work.ID, "cover", "image/jpeg", []byte("jpeg-bytes")); err != nil {
|
|
t.Fatalf("save cover: %v", err)
|
|
}
|
|
contentType, data, err := store.GetWorkCover(ctx, work.ID, "cover")
|
|
if err != nil || contentType != "image/jpeg" || string(data) != "jpeg-bytes" {
|
|
t.Fatalf("get cover: contentType=%q data=%q err=%v", contentType, data, err)
|
|
}
|
|
// 缓存后不再出现在回填列表。
|
|
missing, err = store.ListWorksMissingCover(ctx, competitor.ID)
|
|
if err != nil || len(missing) != 0 {
|
|
t.Fatalf("missing covers after cache: works=%+v err=%v", missing, err)
|
|
}
|
|
// 重复写入覆盖旧内容(新采集周期封面可能更新)。
|
|
if err := store.SaveWorkCover(ctx, work.ID, "cover", "image/webp", []byte("webp-bytes")); err != nil {
|
|
t.Fatalf("overwrite cover: %v", err)
|
|
}
|
|
contentType, data, err = store.GetWorkCover(ctx, work.ID, "cover")
|
|
if err != nil || contentType != "image/webp" || string(data) != "webp-bytes" {
|
|
t.Fatalf("overwritten cover: contentType=%q data=%q err=%v", contentType, data, err)
|
|
}
|
|
if _, _, err := store.GetWorkCover(ctx, "missing", "cover"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("missing cover must be ErrNotFound: err=%v", err)
|
|
}
|
|
// 非法输入拒绝。
|
|
if err := store.SaveWorkCover(ctx, work.ID, "cover", "image/jpeg", nil); !errors.Is(err, ErrInvalid) {
|
|
t.Fatalf("empty cover data must be rejected: err=%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.Fatal(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)
|
|
}
|
|
}
|
|
}
|