thread-master/apps/backend/internal/module/scout/usecase/planner_test.go

121 lines
5.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package usecase_test
import (
"context"
"testing"
"apps/backend/internal/module/scout/domain"
"apps/backend/internal/module/scout/repository"
"apps/backend/internal/module/scout/usecase"
"github.com/stretchr/testify/require"
)
type capturedSearchProvider struct {
terms []string
hits []usecase.ThreadSearchResult
}
func TestPrepareBriefPlansDeduplicatedTermsFromAllInputs(t *testing.T) {
svc := usecase.New(repository.NewMemory())
brand, err := svc.CreateBrand(context.Background(), 1, "B", "")
require.NoError(t, err)
product, err := svc.SaveProduct(context.Background(), 1, &domain.Product{
BrandID: brand.ID, Label: "P", PainPoints: []string{"敏感肌", "敏感肌"}, MatchTags: []string{"保養", "敏感肌"},
})
require.NoError(t, err)
brief, err := svc.PrepareBrief(context.Background(), 1, "敏感肌保養", brand.ID, product.ID, "value", false)
require.NoError(t, err)
require.Equal(t, []string{"敏感肌", "保養", "使用情境", "替代方案", "成分/規格", "敏感肌保養"}, brief.ScanTerms)
}
func (p *capturedSearchProvider) SearchThreads(_ context.Context, terms []string, _ int) ([]usecase.ThreadSearchResult, error) {
p.terms = append([]string(nil), terms...)
return p.hits, nil
}
func TestPlannerProductSignalsAndDeterministicPersistence(t *testing.T) {
provider := &capturedSearchProvider{hits: []usecase.ThreadSearchResult{
{URL: "https://www.threads.net/@a/post/1?utm_source=test", Snippet: "請問敏感肌怎麼舒緩?"},
{URL: "https://www.threads.net/@b/post/2", Snippet: "敏感肌保養有沒有推薦?"},
{URL: "https://www.threads.net/@c/post/3", Snippet: "敏感肌服務洽詢,現在限時優惠"},
{URL: "https://www.threads.net/@d/post/4", Snippet: "分享敏感肌保養的使用心得"},
{URL: "https://www.threads.net/@e/post/5", Snippet: "敏感肌抽獎,互追拿好禮"},
}}
store := repository.NewMemory()
svc := usecase.New(store)
svc.Provider = provider
brief := &domain.RunBrief{
Mode: domain.ModeProduct, Intent: "敏感肌保養", Pains: []string{"敏感肌", "敏感肌"},
Tags: []string{"保養", "敏感肌"}, Periphery: []string{"使用情境", "保養"},
}
brief.ScanTerms = []string{"敏感肌", "保養", "使用情境", "敏感肌保養"}
posts, err := svc.RunScanFromBrief(context.Background(), 42, brief)
require.NoError(t, err)
require.Equal(t, []string{"敏感肌", "保養", "使用情境", "敏感肌保養"}, provider.terms)
require.Len(t, posts, 4)
byClass := map[string]*domain.Post{}
for _, post := range posts {
byClass[post.Classification] = post
require.NotEmpty(t, post.Permalink)
require.Equal(t, post.Permalink, post.ExternalID)
require.InDelta(t, 0, post.Score, 100)
require.NotEmpty(t, post.MatchReason)
}
require.Contains(t, byClass, domain.ClassificationSeekingHelp)
require.Contains(t, byClass, domain.ClassificationSeekingRecommendation)
require.Contains(t, byClass, domain.ClassificationProviderOffer)
require.Contains(t, byClass, domain.ClassificationDiscussion)
require.Contains(t, byClass[domain.ClassificationSeekingHelp].MatchReason, "敏感肌")
require.Equal(t, "https://www.threads.net/@a/post/1", byClass[domain.ClassificationSeekingHelp].Permalink)
again, err := svc.RunScanFromBrief(context.Background(), 42, brief)
require.NoError(t, err)
require.Equal(t, byClass[domain.ClassificationSeekingHelp].ID, again[0].ID)
persisted, err := svc.ListPosts(context.Background(), 42, "")
require.NoError(t, err)
require.Len(t, persisted, 4)
}
func TestPlannerActivityClassifiesWithNeutralRecency(t *testing.T) {
provider := &capturedSearchProvider{hits: []usecase.ThreadSearchResult{
{URL: "https://www.threads.net/@a/post/1", Snippet: "請問這週市集怎麼報名?"},
{URL: "https://www.threads.net/@b/post/2", Snippet: "市集活動公告,週六開幕"},
{URL: "https://www.threads.net/@c/post/3", Snippet: "市集攤位服務洽詢"},
{URL: "https://www.threads.net/@d/post/4", Snippet: "分享市集逛街心得"},
{URL: "https://www.threads.net/@e/post/5", Snippet: "市集 crypto 賺錢群組"},
}}
svc := usecase.New(repository.NewMemory())
svc.Provider = provider
brief := &domain.RunBrief{Mode: domain.ModeActivity, Intent: "市集", ScanTerms: []string{"市集"}}
posts, err := svc.RunScanFromBrief(context.Background(), 7, brief)
require.NoError(t, err)
require.Len(t, posts, 4)
classes := map[string]bool{}
for _, post := range posts {
classes[post.Classification] = true
require.Contains(t, post.MatchReason, "recency unavailable (neutral)")
require.Greater(t, post.Score, 0)
}
require.True(t, classes[domain.ClassificationAsking])
require.True(t, classes[domain.ClassificationAnnouncement])
require.True(t, classes[domain.ClassificationProviderOffer])
require.True(t, classes[domain.ClassificationDiscussion])
}
func TestDraftOutreachIsDeterministicAndDoesNotNeedAI(t *testing.T) {
svc := usecase.New(repository.NewMemory())
svc.Provider = &capturedSearchProvider{hits: []usecase.ThreadSearchResult{{
URL: "https://www.threads.net/@author/post/1", Snippet: "請問怎麼處理頭皮困擾?",
}}}
posts, err := svc.RunScanFromBrief(context.Background(), 9, &domain.RunBrief{
Mode: domain.ModeTheme, Intent: "頭皮", ScanTerms: []string{"頭皮"},
})
require.NoError(t, err)
draft, err := svc.DraftOutreach(context.Background(), 9, posts[0].ID, "")
require.NoError(t, err)
require.Equal(t, "嗨 @author看到你提到「頭皮」我也遇過類似情況。若你願意想聽聽你後來怎麼處理。", draft.DraftText)
}