951 lines
33 KiB
Go
951 lines
33 KiB
Go
package usecase_test
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
"time"
|
||
|
||
"apps/backend/internal/module/ai"
|
||
"apps/backend/internal/module/studio/domain"
|
||
"apps/backend/internal/module/studio/publish"
|
||
"apps/backend/internal/module/studio/repository"
|
||
"apps/backend/internal/module/studio/usecase"
|
||
threadsDomain "apps/backend/internal/module/threads/domain"
|
||
usageDomain "apps/backend/internal/module/usage/domain"
|
||
usageRepo "apps/backend/internal/module/usage/repository"
|
||
usageUC "apps/backend/internal/module/usage/usecase"
|
||
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
type memAccounts struct {
|
||
byID map[string]*threadsDomain.Account
|
||
}
|
||
|
||
type blockingPublishTransport struct {
|
||
entered chan struct{}
|
||
release chan struct{}
|
||
}
|
||
|
||
func (t *blockingPublishTransport) Publish(ctx context.Context, _ domain.PublishRequest) (*domain.PublishResult, error) {
|
||
close(t.entered)
|
||
select {
|
||
case <-t.release:
|
||
return &domain.PublishResult{MediaID: "renewed-media"}, nil
|
||
case <-ctx.Done():
|
||
return nil, ctx.Err()
|
||
}
|
||
}
|
||
|
||
func (m *memAccounts) Get(_ context.Context, id string) (*threadsDomain.Account, error) {
|
||
a, ok := m.byID[id]
|
||
if !ok {
|
||
return nil, threadsDomain.ErrNotFound
|
||
}
|
||
cp := *a
|
||
return &cp, nil
|
||
}
|
||
|
||
func (m *memAccounts) List(_ context.Context, ownerUID int64) ([]*threadsDomain.Account, error) {
|
||
var out []*threadsDomain.Account
|
||
for _, a := range m.byID {
|
||
if a.OwnerUID == ownerUID {
|
||
cp := *a
|
||
out = append(out, &cp)
|
||
}
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (m *memAccounts) AccessToken(_ context.Context, a *threadsDomain.Account) (string, error) {
|
||
return "tok-" + a.ID, nil
|
||
}
|
||
|
||
func newStudio() (*usecase.Service, *publish.FakeTransport, *memAccounts) {
|
||
repo := repository.NewMemory()
|
||
tp := publish.NewFake()
|
||
acc := &memAccounts{byID: map[string]*threadsDomain.Account{}}
|
||
svc := usecase.New(repo, tp)
|
||
svc.Accounts = acc
|
||
svc.AI = &ai.FakeClient{}
|
||
// usage with unlimited platform
|
||
ures := usageRepo.NewMemory()
|
||
uresolver := &usageUC.StaticResolver{Map: map[string]string{}}
|
||
us := usageUC.New(ures, uresolver)
|
||
svc.Usage = us
|
||
return svc, tp, acc
|
||
}
|
||
|
||
func withUsage(svc *usecase.Service, uid int64) {
|
||
// unlimited + platform key
|
||
_ = svc.Usage.Repo.SavePrefs(context.Background(), &usageDomain.MemberPrefs{
|
||
UID: uid, PlanID: usageDomain.PlanPro, Unlimited: true, UpdatedAt: domain.NowNano(),
|
||
})
|
||
if sr, ok := svc.Usage.Resolver.(*usageUC.StaticResolver); ok {
|
||
if sr.Map == nil {
|
||
sr.Map = map[string]string{}
|
||
}
|
||
sr.Map[fmtUIDMeter(uid, usageDomain.MeterAICopy)] = usageDomain.KeyModePlatform
|
||
}
|
||
}
|
||
|
||
func fmtUIDMeter(uid int64, meter string) string {
|
||
return string(rune(0)) // placeholder — use fmt
|
||
}
|
||
|
||
func setupUID(svc *usecase.Service, uid int64) {
|
||
_ = svc.Usage.Repo.SavePrefs(context.Background(), &usageDomain.MemberPrefs{
|
||
UID: uid, PlanID: usageDomain.PlanPro, Unlimited: true, UpdatedAt: domain.NowNano(),
|
||
})
|
||
if sr, ok := svc.Usage.Resolver.(*usageUC.StaticResolver); ok {
|
||
if sr.Map == nil {
|
||
sr.Map = map[string]string{}
|
||
}
|
||
key := ""
|
||
// StaticResolver uses fmt.Sprintf("%d:%s", uid, meter)
|
||
key = sprintf("%d:%s", uid, usageDomain.MeterAICopy)
|
||
sr.Map[key] = usageDomain.KeyModePlatform
|
||
}
|
||
}
|
||
|
||
func sprintf(f string, a ...any) string {
|
||
return format(f, a...)
|
||
}
|
||
|
||
// avoid importing fmt in helpers clutter — use strconv
|
||
func format(f string, a ...any) string {
|
||
// minimal for our key only
|
||
if f == "%d:%s" && len(a) == 2 {
|
||
return itoa(a[0].(int64)) + ":" + a[1].(string)
|
||
}
|
||
return f
|
||
}
|
||
|
||
func itoa(n int64) string {
|
||
if n == 0 {
|
||
return "0"
|
||
}
|
||
neg := n < 0
|
||
if neg {
|
||
n = -n
|
||
}
|
||
var b [32]byte
|
||
i := len(b)
|
||
for n > 0 {
|
||
i--
|
||
b[i] = byte('0' + n%10)
|
||
n /= 10
|
||
}
|
||
if neg {
|
||
i--
|
||
b[i] = '-'
|
||
}
|
||
return string(b[i:])
|
||
}
|
||
|
||
func addAccount(acc *memAccounts, ownerUID int64, id, user string) {
|
||
acc.byID[id] = &threadsDomain.Account{
|
||
ID: id, OwnerUID: ownerUID, Username: user, DisplayName: user,
|
||
Connection: threadsDomain.ConnectionConnected, IsUsable: true,
|
||
}
|
||
}
|
||
|
||
// ---- PE ----
|
||
|
||
func TestPE_01_CreateSave(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_000_001)
|
||
setupUID(svc, uid)
|
||
p, err := svc.SavePersona(context.Background(), uid, &domain.Persona{Name: "小海", Brief: "溫暖"})
|
||
require.NoError(t, err)
|
||
require.NotEmpty(t, p.ID)
|
||
list, err := svc.ListPersonas(context.Background(), uid)
|
||
require.NoError(t, err)
|
||
require.Len(t, list, 1)
|
||
}
|
||
|
||
func TestPE_02_SetActive(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_000_002)
|
||
setupUID(svc, uid)
|
||
a, _ := svc.SavePersona(context.Background(), uid, &domain.Persona{Name: "A"})
|
||
b, _ := svc.SavePersona(context.Background(), uid, &domain.Persona{Name: "B"})
|
||
require.NoError(t, svc.SetActivePersonaID(context.Background(), uid, b.ID))
|
||
id, err := svc.GetActivePersonaID(context.Background(), uid)
|
||
require.NoError(t, err)
|
||
require.Equal(t, b.ID, id)
|
||
_ = a
|
||
}
|
||
|
||
func TestPE_03_RemoveActiveResets(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_000_003)
|
||
setupUID(svc, uid)
|
||
a, _ := svc.SavePersona(context.Background(), uid, &domain.Persona{Name: "A"})
|
||
b, _ := svc.SavePersona(context.Background(), uid, &domain.Persona{Name: "B"})
|
||
_ = svc.SetActivePersonaID(context.Background(), uid, a.ID)
|
||
require.NoError(t, svc.RemovePersona(context.Background(), uid, a.ID))
|
||
id, _ := svc.GetActivePersonaID(context.Background(), uid)
|
||
require.Equal(t, b.ID, id)
|
||
}
|
||
|
||
func TestPE_04_AnalyzeFromText(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_000_004)
|
||
setupUID(svc, uid)
|
||
p, _ := svc.SavePersona(context.Background(), uid, &domain.Persona{Name: "分析"})
|
||
raw := "今天天氣真好,可是我還是好累有沒有人懂\n\n---\n\n大家早安,後來我改成先寫使用情境再問問題"
|
||
out, err := svc.AnalyzeFromText(context.Background(), uid, p.ID, raw, "貼文")
|
||
require.NoError(t, err)
|
||
require.Equal(t, domain.PersonaReady, out.Status)
|
||
require.True(t, out.Style.AnalyzedAt > 0)
|
||
require.GreaterOrEqual(t, out.Style.SampleCount, 2)
|
||
// 概要 + 指紋 + 8D 都要填滿
|
||
require.NotEmpty(t, out.Brief)
|
||
require.NotEmpty(t, out.Style.DraftText)
|
||
require.NotEmpty(t, out.Style.Draft.Tone)
|
||
require.NotEmpty(t, out.Style.Draft.Identity)
|
||
require.Contains(t, out.Style.DraftText, "【")
|
||
require.Len(t, out.Style.Dimensions, 8)
|
||
require.NotEmpty(t, out.Style.Dimensions["d1Tone"].Summary)
|
||
require.NotEmpty(t, out.Guard.Avoid)
|
||
}
|
||
|
||
func TestPE_05_AnalyzeFromAccount(t *testing.T) {
|
||
restore := usecase.SetProfileFetchForTest(func(ctx context.Context, username, storageStateJSON string, limit int) ([]string, error) {
|
||
return []string{
|
||
"有時候真的會卡在不知道問誰,後來我改成先寫自己的使用情境。",
|
||
"懂那種越研究越焦慮的感覺。你們如果有推的,最好附一句為什麼。",
|
||
"講真的上次踩雷之後我都先問實際用起來再下單。",
|
||
}, nil
|
||
})
|
||
t.Cleanup(restore)
|
||
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_000_005)
|
||
setupUID(svc, uid)
|
||
p, _ := svc.SavePersona(context.Background(), uid, &domain.Persona{Name: "對標"})
|
||
out, err := svc.AnalyzeFromAccount(context.Background(), uid, p.ID, "cool_creator")
|
||
require.NoError(t, err)
|
||
require.Equal(t, domain.PersonaReady, out.Status)
|
||
require.Equal(t, "cool_creator", out.Style.BenchmarkUsername)
|
||
require.GreaterOrEqual(t, out.Style.SampleCount, 2)
|
||
require.NotContains(t, out.Style.SamplePreviews[0], "sample post about")
|
||
require.NotEmpty(t, out.Brief)
|
||
require.NotEmpty(t, out.Style.DraftText)
|
||
require.Len(t, out.Style.Dimensions, 8)
|
||
require.NotEmpty(t, out.Style.Dimensions["d8Risk"].Summary)
|
||
}
|
||
|
||
func TestPE_06_CrossUidForbidden(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
setupUID(svc, 4_000_006)
|
||
setupUID(svc, 4_000_007)
|
||
p, _ := svc.SavePersona(context.Background(), 4_000_006, &domain.Persona{Name: "私有"})
|
||
_, err := svc.GetPersona(context.Background(), 4_000_007, p.ID)
|
||
require.ErrorIs(t, err, domain.ErrForbidden)
|
||
err = svc.RemovePersona(context.Background(), 4_000_007, p.ID)
|
||
require.ErrorIs(t, err, domain.ErrForbidden)
|
||
}
|
||
|
||
// ---- PL ----
|
||
|
||
func TestPL_01_SaveGet(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_001)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
play, err := svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "方案A", Topic: "主題", LeadAccountID: "acc1",
|
||
Steps: []domain.PlayStep{{Kind: domain.StepRoot, AccountID: "acc1", Text: "主貼"}},
|
||
ScheduleStartAt: domain.NowNano(),
|
||
})
|
||
require.NoError(t, err)
|
||
got, err := svc.GetPlay(context.Background(), uid, play.ID)
|
||
require.NoError(t, err)
|
||
require.Equal(t, "方案A", got.Title)
|
||
require.Len(t, got.Steps, 1)
|
||
}
|
||
|
||
func TestPL_02_ReplyAccountNotSpeaker(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_002)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
addAccount(acc, uid, "acc_stranger", "x")
|
||
play, err := svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "bad", LeadAccountID: "acc1", CastAccountIDs: []string{},
|
||
Steps: []domain.PlayStep{
|
||
{Kind: domain.StepRoot, AccountID: "acc1", Text: "root"},
|
||
{Kind: domain.StepReply, AccountID: "acc_stranger", Text: "reply"},
|
||
},
|
||
ScheduleStartAt: domain.NowNano(),
|
||
})
|
||
require.NoError(t, err)
|
||
_, err = svc.SubmitPlay(context.Background(), uid, play.ID)
|
||
require.Error(t, err)
|
||
}
|
||
|
||
func TestPL_03_SubmitScheduleAligned(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_003)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
addAccount(acc, uid, "acc2", "cast")
|
||
// 明確未來開始(>30s)→ 第一則排到該時間
|
||
start := domain.NowNano() + int64(time.Hour)
|
||
play, _ := svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "sched", LeadAccountID: "acc1", CastAccountIDs: []string{"acc2"},
|
||
Steps: []domain.PlayStep{
|
||
{Kind: domain.StepRoot, AccountID: "acc1", Text: "root", DelayFromPreviousSec: 0},
|
||
{Kind: domain.StepReply, AccountID: "acc2", Text: "r1", DelayFromPreviousSec: 60},
|
||
},
|
||
ScheduleStartAt: start,
|
||
})
|
||
b, err := svc.SubmitPlay(context.Background(), uid, play.ID)
|
||
require.NoError(t, err)
|
||
require.Equal(t, start, b.Steps[0].ScheduledAt)
|
||
// 第二則 = 間隔 60s ± 抖動
|
||
delta := b.Steps[1].ScheduledAt - b.Steps[0].ScheduledAt
|
||
require.GreaterOrEqual(t, delta, int64(15*time.Second))
|
||
require.LessOrEqual(t, delta, int64(120*time.Second))
|
||
require.Equal(t, domain.StepScheduled, b.Steps[0].Status)
|
||
|
||
// 未設未來開始 → 第一則立刻 due
|
||
play2, _ := svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "now", LeadAccountID: "acc1",
|
||
Steps: []domain.PlayStep{
|
||
{Kind: domain.StepRoot, AccountID: "acc1", Text: "r", DelayFromPreviousSec: 0},
|
||
},
|
||
ScheduleStartAt: 0,
|
||
})
|
||
before := domain.NowNano()
|
||
b2, err := svc.SubmitPlay(context.Background(), uid, play2.ID)
|
||
require.NoError(t, err)
|
||
require.InDelta(t, float64(before), float64(b2.Steps[0].ScheduledAt), float64(2*time.Second))
|
||
}
|
||
|
||
func TestPL_04_ResolveExternalLink(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
t_, err := svc.ResolveExternalLink(context.Background(), "https://www.threads.net/@alice/post/AbC123")
|
||
require.NoError(t, err)
|
||
require.Contains(t, t_.URL, "threads.net")
|
||
require.Equal(t, "alice", t_.AuthorUsername)
|
||
require.Equal(t, "AbC123", t_.Shortcode)
|
||
}
|
||
|
||
func TestPL_05_ResolveBadURL(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
_, err := svc.ResolveExternalLink(context.Background(), "not-a-url")
|
||
require.ErrorIs(t, err, domain.ErrBadURL)
|
||
}
|
||
|
||
func TestPL_06_PublishScheduled(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_006)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
future := domain.NowNano() + int64(2*time.Hour)
|
||
b, err := svc.PublishSingle(context.Background(), uid, "acc1", "hello scheduled", "t", nil, future, "")
|
||
require.NoError(t, err)
|
||
require.Len(t, b.Steps, 1)
|
||
require.Equal(t, future, b.Steps[0].ScheduledAt)
|
||
}
|
||
|
||
func TestPL_07_PublishDefaultNow(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_007)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
before := domain.NowNano()
|
||
b, err := svc.PublishSingle(context.Background(), uid, "acc1", "now post", "", nil, 0, "")
|
||
require.NoError(t, err)
|
||
require.InDelta(t, float64(before), float64(b.Steps[0].ScheduledAt), float64(time.Second*2))
|
||
}
|
||
|
||
func TestPL_08_NoUsableAccount(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_001_008)
|
||
setupUID(svc, uid)
|
||
_, err := svc.PublishSingle(context.Background(), uid, "missing", "x", "", nil, 0, "")
|
||
require.ErrorIs(t, err, domain.ErrNoAccount)
|
||
}
|
||
|
||
func TestQueueExternalReply(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_081)
|
||
addAccount(acc, uid, "acc1", "replying")
|
||
before := domain.NowNano()
|
||
bundle, err := svc.QueueExternalReply(context.Background(), uid, "acc1", "123456789", "Thanks for sharing", "External reply")
|
||
require.NoError(t, err)
|
||
require.Equal(t, uid, bundle.OwnerUID)
|
||
require.Equal(t, "External reply", bundle.Title)
|
||
require.Equal(t, domain.OBScheduling, bundle.Status)
|
||
require.Equal(t, "123456789", bundle.ReplyToMediaID)
|
||
require.Len(t, bundle.Steps, 1)
|
||
require.Equal(t, domain.StepReply, bundle.Steps[0].Kind)
|
||
require.Equal(t, "acc1", bundle.Steps[0].AccountID)
|
||
require.Equal(t, "123456789", bundle.Steps[0].ReplyTo)
|
||
require.Equal(t, domain.StepScheduled, bundle.Steps[0].Status)
|
||
require.InDelta(t, float64(before), float64(bundle.Steps[0].ScheduledAt), float64(2*time.Second))
|
||
|
||
persisted, err := svc.GetOutbox(context.Background(), uid, bundle.ID)
|
||
require.NoError(t, err)
|
||
require.Equal(t, bundle, persisted)
|
||
}
|
||
|
||
func TestQueueExternalReplyValidation(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_082)
|
||
addAccount(acc, uid, "acc1", "replying")
|
||
|
||
_, err := svc.QueueExternalReply(context.Background(), uid, "acc1", "media_123", "reply", "")
|
||
require.ErrorIs(t, err, domain.ErrValidation)
|
||
_, err = svc.QueueExternalReply(context.Background(), uid, "missing", "123", "reply", "")
|
||
require.ErrorIs(t, err, domain.ErrNoAccount)
|
||
acc.byID["acc1"].IsUsable = false
|
||
_, err = svc.QueueExternalReply(context.Background(), uid, "acc1", "123", "reply", "")
|
||
require.ErrorIs(t, err, domain.ErrNoAccount)
|
||
}
|
||
|
||
func TestPL_09_RemovePlayKeepsOutbox(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_009)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
play, _ := svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "del", LeadAccountID: "acc1",
|
||
Steps: []domain.PlayStep{{Kind: domain.StepRoot, AccountID: "acc1", Text: "r"}},
|
||
ScheduleStartAt: domain.NowNano(),
|
||
})
|
||
b, err := svc.SubmitPlay(context.Background(), uid, play.ID)
|
||
require.NoError(t, err)
|
||
require.NoError(t, svc.RemovePlay(context.Background(), uid, play.ID))
|
||
list, _ := svc.ListPlays(context.Background(), uid)
|
||
require.Empty(t, list)
|
||
// outbox retained
|
||
got, err := svc.GetOutbox(context.Background(), uid, b.ID)
|
||
require.NoError(t, err)
|
||
require.Equal(t, b.ID, got.ID)
|
||
}
|
||
|
||
func TestPL_10_ListByPost(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_010)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
_, _ = svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "p1", LeadAccountID: "acc1", TargetOwnPostID: "post_x",
|
||
Steps: []domain.PlayStep{{Kind: domain.StepReply, AccountID: "acc1", Text: "hi"}},
|
||
})
|
||
_, _ = svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "p2", LeadAccountID: "acc1", TargetOwnPostID: "other",
|
||
Steps: []domain.PlayStep{{Kind: domain.StepReply, AccountID: "acc1", Text: "hi"}},
|
||
})
|
||
list, err := svc.ListPlaysByPost(context.Background(), uid, "post_x")
|
||
require.NoError(t, err)
|
||
require.Len(t, list, 1)
|
||
require.Equal(t, "p1", list[0].Title)
|
||
}
|
||
|
||
func TestPL_11_ListByExternalUrl(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_011)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
url := "https://www.threads.net/@bob/post/ZZ99"
|
||
_, _ = svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "ext", LeadAccountID: "acc1",
|
||
TargetExternal: &domain.ExternalTarget{URL: url},
|
||
Steps: []domain.PlayStep{{Kind: domain.StepReply, AccountID: "acc1", Text: "hi"}},
|
||
})
|
||
list, err := svc.ListPlaysByExternalURL(context.Background(), uid, url+"?utm=1")
|
||
require.NoError(t, err)
|
||
require.Len(t, list, 1)
|
||
}
|
||
|
||
// ---- CP ----
|
||
|
||
func TestCP_01_Mimic(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_002_001)
|
||
setupUID(svc, uid)
|
||
p, _ := svc.SavePersona(context.Background(), uid, &domain.Persona{Name: "P"})
|
||
_, _ = svc.AnalyzeFromText(context.Background(), uid, p.ID, "樣本文字風格", "")
|
||
out, err := svc.Mimic(context.Background(), uid, "原始貼文內容很長", p.ID, "")
|
||
require.NoError(t, err)
|
||
require.NotEmpty(t, out)
|
||
}
|
||
|
||
func TestCP_02_AnalyzeViral(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_002_002)
|
||
setupUID(svc, uid)
|
||
va, err := svc.AnalyzeViral(context.Background(), uid, "爆紅文結構測試")
|
||
require.NoError(t, err)
|
||
require.NotEmpty(t, va.Hooks)
|
||
require.NotEmpty(t, va.Structure)
|
||
}
|
||
|
||
// ---- OB ----
|
||
|
||
func TestOB_01_NotDue_NoTransport(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_003_001)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
future := domain.NowNano() + int64(time.Hour)
|
||
b, _ := svc.PublishSingle(context.Background(), uid, "acc1", "later", "", nil, future, "")
|
||
n, err := svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
require.NoError(t, err)
|
||
require.Equal(t, 0, n)
|
||
require.Equal(t, 0, tp.CallCount())
|
||
got, _ := svc.GetOutbox(context.Background(), uid, b.ID)
|
||
require.Equal(t, domain.StepScheduled, got.Steps[0].Status)
|
||
}
|
||
|
||
func TestOB_02_DueStep_PublishedViaTransport(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_003_002)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
b, _ := svc.PublishSingle(context.Background(), uid, "acc1", "now", "", nil, 0, "")
|
||
n, err := svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
require.NoError(t, err)
|
||
require.Equal(t, 1, n)
|
||
require.Equal(t, 1, tp.CallCount()) // must go through transport
|
||
got, _ := svc.GetOutbox(context.Background(), uid, b.ID)
|
||
require.Equal(t, domain.StepPublished, got.Steps[0].Status)
|
||
require.Equal(t, domain.OBCompleted, got.Status)
|
||
require.NotEmpty(t, got.Steps[0].MediaID)
|
||
}
|
||
|
||
func TestOB_03_TransportFail(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_003_003)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
tp.Fail = true
|
||
b, _ := svc.PublishSingle(context.Background(), uid, "acc1", "fail me", "", nil, 0, "")
|
||
_, _ = svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
got, _ := svc.GetOutbox(context.Background(), uid, b.ID)
|
||
require.Equal(t, domain.StepFailed, got.Steps[0].Status)
|
||
require.NotEmpty(t, got.Steps[0].Error)
|
||
require.NotEqual(t, domain.StepPublished, got.Steps[0].Status)
|
||
}
|
||
|
||
func TestOB_04_RootFail_BlocksReplies(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_003_004)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
addAccount(acc, uid, "acc2", "cast")
|
||
tp.FailOnNth = 1 // root fails
|
||
past := domain.NowNano() - 1
|
||
play, _ := svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "rootfail", LeadAccountID: "acc1", CastAccountIDs: []string{"acc2"},
|
||
Steps: []domain.PlayStep{
|
||
{Kind: domain.StepRoot, AccountID: "acc1", Text: "root"},
|
||
{Kind: domain.StepReply, AccountID: "acc2", Text: "reply", DelayFromPreviousSec: 0},
|
||
},
|
||
ScheduleStartAt: past,
|
||
})
|
||
b, err := svc.SubmitPlay(context.Background(), uid, play.ID)
|
||
require.NoError(t, err)
|
||
_, _ = svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
got, _ := svc.GetOutbox(context.Background(), uid, b.ID)
|
||
require.Equal(t, domain.StepFailed, got.Steps[0].Status)
|
||
require.NotEqual(t, domain.StepPublished, got.Steps[1].Status)
|
||
require.Equal(t, domain.StepBlocked, got.Steps[1].Status)
|
||
}
|
||
|
||
func TestOB_05_RootOk_ReplyPublishes(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_003_005)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
addAccount(acc, uid, "acc2", "cast")
|
||
past := domain.NowNano() - 1
|
||
play, _ := svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "ok", LeadAccountID: "acc1", CastAccountIDs: []string{"acc2"},
|
||
Steps: []domain.PlayStep{
|
||
{Kind: domain.StepRoot, AccountID: "acc1", Text: "root"},
|
||
{Kind: domain.StepReply, AccountID: "acc2", Text: "reply"},
|
||
},
|
||
ScheduleStartAt: past,
|
||
})
|
||
b, _ := svc.SubmitPlay(context.Background(), uid, play.ID)
|
||
n, err := svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
require.NoError(t, err)
|
||
require.Equal(t, 2, n)
|
||
require.Equal(t, 2, tp.CallCount())
|
||
got, _ := svc.GetOutbox(context.Background(), uid, b.ID)
|
||
require.Equal(t, domain.StepPublished, got.Steps[1].Status)
|
||
}
|
||
|
||
func TestOB_06_RetryStep(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_003_006)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
tp.Fail = true
|
||
b, _ := svc.PublishSingle(context.Background(), uid, "acc1", "retry", "", nil, 0, "")
|
||
_, _ = svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
tp.Fail = false
|
||
got, err := svc.RetryStep(context.Background(), uid, b.ID, b.Steps[0].ID)
|
||
require.NoError(t, err)
|
||
require.Equal(t, domain.StepScheduled, got.Steps[0].Status)
|
||
_, _ = svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
got, _ = svc.GetOutbox(context.Background(), uid, b.ID)
|
||
require.Equal(t, domain.StepPublished, got.Steps[0].Status)
|
||
}
|
||
|
||
func TestOB_07_RetryIllegalStatus(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_003_007)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
// scheduled not failed
|
||
b, _ := svc.PublishSingle(context.Background(), uid, "acc1", "x", "", nil, domain.NowNano()+int64(time.Hour), "")
|
||
_, err := svc.RetryStep(context.Background(), uid, b.ID, b.Steps[0].ID)
|
||
require.ErrorIs(t, err, domain.ErrIllegalStatus)
|
||
}
|
||
|
||
func TestOB_08_AllSuccessCompleted(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_003_008)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
b, _ := svc.PublishSingle(context.Background(), uid, "acc1", "done", "", nil, 0, "")
|
||
_, _ = svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
got, _ := svc.GetOutbox(context.Background(), uid, b.ID)
|
||
require.Equal(t, domain.OBCompleted, got.Status)
|
||
}
|
||
|
||
func TestOB_09_PartialFailed(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_003_009)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
addAccount(acc, uid, "acc2", "cast")
|
||
// fail only second call
|
||
tp.FailOnNth = 2
|
||
past := domain.NowNano() - 1
|
||
play, _ := svc.SavePlay(context.Background(), uid, &domain.Play{
|
||
Title: "partial", LeadAccountID: "acc1", CastAccountIDs: []string{"acc2"},
|
||
Steps: []domain.PlayStep{
|
||
{Kind: domain.StepRoot, AccountID: "acc1", Text: "root"},
|
||
{Kind: domain.StepReply, AccountID: "acc2", Text: "reply"},
|
||
},
|
||
ScheduleStartAt: past,
|
||
})
|
||
b, _ := svc.SubmitPlay(context.Background(), uid, play.ID)
|
||
_, _ = svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
got, _ := svc.GetOutbox(context.Background(), uid, b.ID)
|
||
require.Equal(t, domain.OBPartial, got.Status)
|
||
}
|
||
|
||
func TestOB_10_RemoveStopsWorker(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_003_010)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
b, _ := svc.PublishSingle(context.Background(), uid, "acc1", "rm", "", nil, 0, "")
|
||
require.NoError(t, svc.RemoveOutbox(context.Background(), uid, b.ID))
|
||
n, _ := svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
require.Equal(t, 0, n)
|
||
require.Equal(t, 0, tp.CallCount())
|
||
}
|
||
|
||
func TestOB_PublishRenewsStepLease(t *testing.T) {
|
||
repo := repository.NewMemory()
|
||
transport := &blockingPublishTransport{entered: make(chan struct{}), release: make(chan struct{})}
|
||
svc := usecase.New(repo, transport)
|
||
svc.OutboxLeaseDuration = 90 * time.Millisecond
|
||
now := domain.NowNano()
|
||
require.NoError(t, repo.SaveOutbox(context.Background(), &domain.OutboxBundle{
|
||
ID: "lease-bundle", Steps: []domain.OutboxStep{{ID: "lease-step", Kind: domain.StepRoot, Status: domain.StepScheduled, ScheduledAt: now}},
|
||
}))
|
||
done := make(chan error, 1)
|
||
go func() {
|
||
_, err := svc.ProcessDueSteps(context.Background(), now)
|
||
done <- err
|
||
}()
|
||
<-transport.entered
|
||
claimed, err := repo.GetOutbox(context.Background(), "lease-bundle")
|
||
require.NoError(t, err)
|
||
initialExpiry := claimed.Steps[0].LeaseExpiresAt
|
||
require.Eventually(t, func() bool {
|
||
got, getErr := repo.GetOutbox(context.Background(), "lease-bundle")
|
||
return getErr == nil && got.Steps[0].LeaseExpiresAt > initialExpiry
|
||
}, time.Second, 10*time.Millisecond)
|
||
close(transport.release)
|
||
require.NoError(t, <-done)
|
||
got, err := repo.GetOutbox(context.Background(), "lease-bundle")
|
||
require.NoError(t, err)
|
||
require.Equal(t, domain.StepPublished, got.Steps[0].Status)
|
||
}
|
||
|
||
func TestOB_11_CrossUid(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid1, uid2 := int64(4_003_011), int64(4_003_012)
|
||
setupUID(svc, uid1)
|
||
setupUID(svc, uid2)
|
||
addAccount(acc, uid1, "acc1", "lead")
|
||
b, _ := svc.PublishSingle(context.Background(), uid1, "acc1", "priv", "", nil, domain.NowNano(), "")
|
||
_, err := svc.GetOutbox(context.Background(), uid2, b.ID)
|
||
require.ErrorIs(t, err, domain.ErrForbidden)
|
||
_, err = svc.RetryStep(context.Background(), uid2, b.ID, b.Steps[0].ID)
|
||
require.ErrorIs(t, err, domain.ErrForbidden)
|
||
}
|
||
|
||
func TestOB_12_LiveRequiresTransport(t *testing.T) {
|
||
// document: FE simulate is not acceptance — ProcessDueSteps uses transport
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_003_013)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
_, _ = svc.PublishSingle(context.Background(), uid, "acc1", "x", "", nil, 0, "")
|
||
before := tp.CallCount()
|
||
_, _ = svc.ProcessDueSteps(context.Background(), domain.NowNano())
|
||
require.Greater(t, tp.CallCount(), before)
|
||
}
|
||
|
||
func TestPL_PublishRejectsPastSchedule(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_001_020)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "lead")
|
||
// 超過 1 分鐘才算過期
|
||
past := domain.NowNano() - int64(2*time.Minute)
|
||
_, err := svc.PublishSingle(context.Background(), uid, "acc1", "too late", "", nil, past, "")
|
||
require.ErrorIs(t, err, domain.ErrValidation)
|
||
// 略早於現在(grace 內)應可過,並 clamp 成現在
|
||
slight := domain.NowNano() - int64(5*time.Second)
|
||
b, err := svc.PublishSingle(context.Background(), uid, "acc1", "almost now", "", nil, slight, "")
|
||
require.NoError(t, err)
|
||
require.NotNil(t, b)
|
||
}
|
||
|
||
// ---- OP ----
|
||
|
||
func TestOP_01_Sync(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_004_001)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "me")
|
||
list, err := svc.SyncOwnPosts(context.Background(), uid, "acc1")
|
||
require.NoError(t, err)
|
||
require.NotEmpty(t, list)
|
||
ts, err := svc.LastSyncedAt(context.Background(), uid)
|
||
require.NoError(t, err)
|
||
require.True(t, ts > 0)
|
||
}
|
||
|
||
func TestOP_02_SyncFailDoesNotClear(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_004_002)
|
||
setupUID(svc, uid)
|
||
_, err := svc.SyncOwnPosts(context.Background(), uid, "nope")
|
||
require.Error(t, err)
|
||
}
|
||
|
||
func TestOP_03_GenerateReply(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_004_003)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "me")
|
||
list, _ := svc.SyncOwnPosts(context.Background(), uid, "acc1")
|
||
text, err := svc.GenerateReply(context.Background(), uid, list[0].ID, "", "")
|
||
require.NoError(t, err)
|
||
require.NotEmpty(t, text)
|
||
}
|
||
|
||
func TestOP_04_SendReply(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_004_004)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "me")
|
||
list, _ := svc.SyncOwnPosts(context.Background(), uid, "acc1")
|
||
post, err := svc.SendReply(context.Background(), uid, list[0].ID, "", "thanks!", "acc1", nil)
|
||
require.NoError(t, err)
|
||
require.Equal(t, 1, tp.CallCount())
|
||
require.True(t, post.Replies[len(post.Replies)-1].IsMine)
|
||
}
|
||
|
||
func TestOP_05_SendReplyFail(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_004_005)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "me")
|
||
list, _ := svc.SyncOwnPosts(context.Background(), uid, "acc1")
|
||
before := len(list[0].Replies)
|
||
tp.Fail = true
|
||
_, err := svc.SendReply(context.Background(), uid, list[0].ID, "", "nope", "acc1", nil)
|
||
require.Error(t, err)
|
||
got, _ := svc.ListOwnPosts(context.Background(), uid, "acc1")
|
||
// find same post
|
||
for _, p := range got {
|
||
if p.ID == list[0].ID {
|
||
// may have original seed replies only
|
||
require.LessOrEqual(t, len(p.Replies), before+1)
|
||
// last must not be ours if failed before save — count isMine
|
||
mine := 0
|
||
for _, r := range p.Replies {
|
||
if r.IsMine {
|
||
mine++
|
||
}
|
||
}
|
||
require.Equal(t, 0, mine)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestOP_06_MentionMarkAndSkip(t *testing.T) {
|
||
svc, tp, acc := newStudio()
|
||
uid := int64(4_004_006)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "me")
|
||
_ = svc.SeedMention(context.Background(), &domain.Mention{
|
||
OwnerUID: uid, AccountID: "acc1", MediaID: "media_mention_1",
|
||
FromUsername: "x", Text: "hi @me", ContextSnippet: "ctx",
|
||
})
|
||
list, _ := svc.ListMentions(context.Background(), uid, "")
|
||
require.Len(t, list, 1)
|
||
m, err := svc.MarkMentionReplied(context.Background(), uid, list[0].ID, "ok", nil)
|
||
require.NoError(t, err)
|
||
require.Equal(t, domain.MentionReplied, m.Status)
|
||
require.NotEmpty(t, tp.Calls)
|
||
require.Equal(t, "media_mention_1", tp.Calls[len(tp.Calls)-1].ReplyTo)
|
||
_ = svc.SeedMention(context.Background(), &domain.Mention{
|
||
OwnerUID: uid, AccountID: "acc1", MediaID: "media_mention_2",
|
||
FromUsername: "y", Text: "skip", ContextSnippet: "c",
|
||
})
|
||
list2, _ := svc.ListMentions(context.Background(), uid, "")
|
||
var skipID string
|
||
for _, x := range list2 {
|
||
if x.Status == domain.MentionPending {
|
||
skipID = x.ID
|
||
}
|
||
}
|
||
s, err := svc.SkipMention(context.Background(), uid, skipID)
|
||
require.NoError(t, err)
|
||
require.Equal(t, domain.MentionSkipped, s.Status)
|
||
}
|
||
|
||
func TestOP_07_SyncOtherAccountForbidden(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid1, uid2 := int64(4_004_007), int64(4_004_008)
|
||
setupUID(svc, uid1)
|
||
setupUID(svc, uid2)
|
||
addAccount(acc, uid1, "acc1", "a")
|
||
_, err := svc.SyncOwnPosts(context.Background(), uid2, "acc1")
|
||
require.ErrorIs(t, err, domain.ErrForbidden)
|
||
}
|
||
|
||
func TestOP_08_AnalyzePost(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_004_009)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "me")
|
||
list, _ := svc.SyncOwnPosts(context.Background(), uid, "acc1")
|
||
post, err := svc.AnalyzePost(context.Background(), uid, list[0].ID)
|
||
require.NoError(t, err)
|
||
require.NotEmpty(t, post.FormulaSummary)
|
||
require.NotEmpty(t, post.FormulaDetail)
|
||
}
|
||
|
||
func TestOP_09_MentionList(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_004_010)
|
||
setupUID(svc, uid)
|
||
_ = svc.SeedMention(context.Background(), &domain.Mention{
|
||
OwnerUID: uid, AccountID: "a1", FromUsername: "u", Text: "t", ContextSnippet: "c",
|
||
})
|
||
_ = svc.SeedMention(context.Background(), &domain.Mention{
|
||
OwnerUID: uid, AccountID: "a2", FromUsername: "u2", Text: "t2", ContextSnippet: "c",
|
||
})
|
||
list, err := svc.ListMentions(context.Background(), uid, "a1")
|
||
require.NoError(t, err)
|
||
require.Len(t, list, 1)
|
||
}
|
||
|
||
func TestOP_10_MentionGenerateReply(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
uid := int64(4_004_011)
|
||
setupUID(svc, uid)
|
||
_ = svc.SeedMention(context.Background(), &domain.Mention{
|
||
OwnerUID: uid, AccountID: "a1", FromUsername: "fan", Text: "喜歡你的文", ContextSnippet: "ctx",
|
||
})
|
||
list, _ := svc.ListMentions(context.Background(), uid, "")
|
||
m, err := svc.GenerateMentionReply(context.Background(), uid, list[0].ID, "")
|
||
require.NoError(t, err)
|
||
require.NotEmpty(t, m.DraftText)
|
||
}
|
||
|
||
func TestOP_11_GenerateFromFormulaRemoved(t *testing.T) {
|
||
svc, _, _ := newStudio()
|
||
err := svc.GenerateFromFormula(context.Background(), "p1", "")
|
||
require.ErrorIs(t, err, domain.ErrFormulaRemove)
|
||
}
|
||
|
||
type fakeMentionsMedia struct {
|
||
hits []usecase.FetchedMention
|
||
err error
|
||
}
|
||
|
||
func (f *fakeMentionsMedia) ListThreads(context.Context, string, int) ([]usecase.FetchedThread, error) {
|
||
return nil, nil
|
||
}
|
||
func (f *fakeMentionsMedia) GetInsights(context.Context, string, string) (usecase.FetchedInsights, error) {
|
||
return usecase.FetchedInsights{Status: "ok"}, nil
|
||
}
|
||
func (f *fakeMentionsMedia) ListConversation(context.Context, string, string, int) ([]usecase.FetchedReply, error) {
|
||
return nil, nil
|
||
}
|
||
func (f *fakeMentionsMedia) ListMentions(context.Context, string, string, int) ([]usecase.FetchedMention, error) {
|
||
if f.err != nil {
|
||
return nil, f.err
|
||
}
|
||
return f.hits, nil
|
||
}
|
||
func (f *fakeMentionsMedia) ListProfilePosts(context.Context, string, string, int) ([]usecase.FetchedThread, error) {
|
||
return nil, nil
|
||
}
|
||
|
||
func TestOP_12_SyncMentionsFromGraph(t *testing.T) {
|
||
svc, _, acc := newStudio()
|
||
uid := int64(4_004_012)
|
||
setupUID(svc, uid)
|
||
addAccount(acc, uid, "acc1", "me")
|
||
// real-ish token (not fake-)
|
||
svc.Media = &fakeMentionsMedia{hits: []usecase.FetchedMention{
|
||
{ID: "m_100", Text: "嗨 @me 看看", Username: "fan_a", Permalink: "https://threads.net/t/x", PublishedAt: domain.NowNano()},
|
||
{ID: "m_101", Text: "引用一下", Username: "fan_b", IsQuotePost: true, PublishedAt: domain.NowNano()},
|
||
}}
|
||
list, err := svc.SyncMentions(context.Background(), uid, "acc1")
|
||
require.NoError(t, err)
|
||
require.Len(t, list, 2)
|
||
byMedia := map[string]*domain.Mention{}
|
||
for _, m := range list {
|
||
byMedia[m.MediaID] = m
|
||
}
|
||
require.Contains(t, byMedia, "m_100")
|
||
require.Equal(t, "fan_a", byMedia["m_100"].FromUsername)
|
||
// re-sync preserves replied status
|
||
byMedia["m_100"].Status = domain.MentionReplied
|
||
byMedia["m_100"].DraftText = "已回"
|
||
_ = svc.Repo.SaveMention(context.Background(), byMedia["m_100"])
|
||
list2, err := svc.SyncMentions(context.Background(), uid, "acc1")
|
||
require.NoError(t, err)
|
||
var kept *domain.Mention
|
||
for _, m := range list2 {
|
||
if m.MediaID == "m_100" {
|
||
kept = m
|
||
}
|
||
}
|
||
require.NotNil(t, kept)
|
||
require.Equal(t, domain.MentionReplied, kept.Status)
|
||
require.Equal(t, "已回", kept.DraftText)
|
||
}
|