121 lines
5.5 KiB
Go
121 lines
5.5 KiB
Go
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)
|
||
}
|