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)
|
|||
|
|
}
|