2026-08-09 07:57:35 +00:00
|
|
|
package usecase
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
"apps/backend/internal/module/scout/domain"
|
|
|
|
|
"apps/backend/internal/module/scout/repository"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestTextMatchesSearchTerm(t *testing.T) {
|
|
|
|
|
if !textMatchesSearchTerm("最近在找外包做 app", "外包 求推薦") {
|
|
|
|
|
t.Fatal("should match 外包")
|
|
|
|
|
}
|
|
|
|
|
if textMatchesSearchTerm("今天天氣真好去爬山", "外包 求推薦") {
|
|
|
|
|
t.Fatal("unrelated text must not match")
|
|
|
|
|
}
|
|
|
|
|
if !textMatchesSearchTerm("求推薦好用的保母", "保母") {
|
|
|
|
|
t.Fatal("should match 保母")
|
|
|
|
|
}
|
|
|
|
|
// 只有「求推薦」命中、沒主題核 → 最長核是求推薦(3字) 會要求含求推薦
|
|
|
|
|
if !textMatchesSearchTerm("有人求推薦嗎", "求推薦") {
|
|
|
|
|
t.Fatal("exact anchor term")
|
|
|
|
|
}
|
2026-08-13 02:22:24 +00:00
|
|
|
if !textMatchesSearchTerm("正在找後端外包工程師協作", "後端 外包") {
|
|
|
|
|
t.Fatal("multi-concept query should match all cores")
|
|
|
|
|
}
|
|
|
|
|
if textMatchesSearchTerm("餐飲品牌正在找社群外包", "後端 外包") {
|
|
|
|
|
t.Fatal("multi-concept query must not match only one core")
|
|
|
|
|
}
|
|
|
|
|
if !textMatchesSearchTerm("鬼滅之刃最新一集討論度很高", "鬼滅之刃") {
|
|
|
|
|
t.Fatal("split title query should match contiguous full title")
|
|
|
|
|
}
|
|
|
|
|
if !textMatchesSearchTerm("大家最近又在聊鬼滅之刃", "鬼滅之刃 熱門") {
|
|
|
|
|
t.Fatal("discovery modifier should not be required in body")
|
|
|
|
|
}
|
2026-08-09 07:57:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPersistSearchHitsRejectsIrrelevantAPIResult(t *testing.T) {
|
|
|
|
|
svc := New(repository.NewMemory())
|
|
|
|
|
posts, err := svc.persistSearchHits(context.Background(), 1, &domain.RunBrief{
|
|
|
|
|
Mode: domain.ModeActivity, ScanTerms: []string{"後端工程師"},
|
|
|
|
|
}, domain.PathAPI, []ThreadSearchResult{
|
|
|
|
|
{URL: "https://www.threads.net/@dev/post/1", Snippet: "正在找後端工程師一起做產品"},
|
|
|
|
|
{URL: "https://www.threads.net/@film/post/2", Snippet: "劇組徵拍電影的夥伴"},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("persistSearchHits: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(posts) != 1 {
|
|
|
|
|
t.Fatalf("posts=%d want 1", len(posts))
|
|
|
|
|
}
|
|
|
|
|
if posts[0].Permalink != "https://www.threads.net/@dev/post/1" {
|
|
|
|
|
t.Fatalf("unexpected post %q", posts[0].Permalink)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 02:22:24 +00:00
|
|
|
func TestPersistActivityHitsPrioritizesScore(t *testing.T) {
|
2026-08-09 07:57:35 +00:00
|
|
|
svc := New(repository.NewMemory())
|
|
|
|
|
posts, err := svc.persistSearchHits(context.Background(), 1, &domain.RunBrief{
|
|
|
|
|
Mode: domain.ModeActivity, ScanTerms: []string{"外包"},
|
|
|
|
|
}, domain.PathAPI, []ThreadSearchResult{
|
|
|
|
|
{URL: "https://www.threads.net/@chat/post/1", Snippet: "分享外包合作的心得", PublishedAt: 200},
|
|
|
|
|
{URL: "https://www.threads.net/@ask/post/2", Snippet: "請問外包工程師怎麼找?", PublishedAt: 100},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("persistSearchHits: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(posts) != 2 {
|
|
|
|
|
t.Fatalf("posts=%d want 2", len(posts))
|
|
|
|
|
}
|
|
|
|
|
if posts[0].Permalink != "https://www.threads.net/@ask/post/2" {
|
2026-08-13 02:22:24 +00:00
|
|
|
t.Fatalf("first post=%q want highest score", posts[0].Permalink)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPersistActivityHitsPrioritizesScoreWithTrackBoost(t *testing.T) {
|
|
|
|
|
svc := New(repository.NewMemory())
|
|
|
|
|
posts, err := svc.persistSearchHits(context.Background(), 1, &domain.RunBrief{
|
|
|
|
|
Mode: domain.ModeActivity, ScanTerms: []string{"鬼滅之刃"},
|
|
|
|
|
}, domain.PathCrawler, []ThreadSearchResult{
|
|
|
|
|
{
|
|
|
|
|
URL: "https://www.threads.net/@ask/post/1", Snippet: "請問鬼滅之刃哪裡好看?",
|
|
|
|
|
Track: "recent", PublishedAt: 300,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
URL: "https://www.threads.net/@hot/post/2", Snippet: "鬼滅之刃新篇大家都在討論",
|
|
|
|
|
Track: "both", PublishedAt: 200,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("persistSearchHits: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(posts) != 2 {
|
|
|
|
|
t.Fatalf("posts=%d want 2", len(posts))
|
|
|
|
|
}
|
|
|
|
|
if posts[0].Permalink != "https://www.threads.net/@hot/post/2" {
|
|
|
|
|
t.Fatalf("first post=%q want highest score", posts[0].Permalink)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDedupePostsByIdentityHidesLegacyAliasesAndKeepsHandledState(t *testing.T) {
|
|
|
|
|
posts := []*domain.Post{
|
|
|
|
|
{
|
|
|
|
|
ID: "legacy-new", Permalink: "https://threads.com/@old/post/DupeCode?share=1",
|
|
|
|
|
OutreachStatus: domain.OutreachNew, CreatedAt: 300, Score: 90,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
ID: "legacy-published", Permalink: "https://www.threads.net/@renamed/post/DupeCode",
|
|
|
|
|
OutreachStatus: domain.OutreachPublished, CreatedAt: 100, Score: 40,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
ID: "other", Permalink: "https://www.threads.net/@old/post/OtherCode",
|
|
|
|
|
OutreachStatus: domain.OutreachNew,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
got := dedupePostsByIdentity(posts)
|
|
|
|
|
if len(got) != 2 {
|
|
|
|
|
t.Fatalf("deduped posts=%d want 2", len(got))
|
|
|
|
|
}
|
|
|
|
|
if got[0].ID != "legacy-published" {
|
|
|
|
|
t.Fatalf("kept id=%q want handled published record", got[0].ID)
|
2026-08-09 07:57:35 +00:00
|
|
|
}
|
|
|
|
|
}
|