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

150 lines
4.5 KiB
Go
Raw Permalink Normal View History

2026-08-09 07:57:35 +00:00
package usecase
import (
"context"
"strings"
"testing"
radarDomain "apps/backend/internal/module/radar/domain"
2026-08-13 02:22:24 +00:00
"apps/backend/internal/module/scout/domain"
2026-08-09 07:57:35 +00:00
)
func TestPrepareActivityBriefUsesGeneratedTerms(t *testing.T) {
svc := &Service{}
brief, err := svc.PrepareBrief(context.Background(), 1, "市集", "", "", "activity", true)
if err != nil {
t.Fatalf("PrepareBrief: %v", err)
}
if brief.Mode != domain.ModeActivity {
t.Fatalf("mode=%q want %q", brief.Mode, domain.ModeActivity)
}
if len(brief.ScanTerms) < 2 {
t.Fatalf("ScanTerms=%v want generated variants", brief.ScanTerms)
}
}
func TestPlanActivityTermsGeneratesVariantsNotJustSplit(t *testing.T) {
// 單一字主題:應產出「核 + 口語錨點」,不是只有原字
got := planActivityTerms("市集")
if len(got) < 3 {
t.Fatalf("市集 variants=%v want >=3 generated terms", got)
}
hasPair := false
for _, term := range got {
if !radarDomain.IsThreadsSearchable(term) {
t.Fatalf("term %q not Threads-searchable", term)
}
if strings.Contains(term, " ") {
hasPair = true
}
}
if !hasPair {
t.Fatalf("expected multi-token variants, got %v", got)
}
// 長句:不應只回整句,要抽出核並產變體
got2 := planActivityTerms("想找台北週末市集可以拍什麼")
if len(got2) < 2 {
t.Fatalf("long intent variants=%v want >=2", got2)
}
for _, term := range got2 {
if !radarDomain.IsThreadsSearchable(term) {
t.Fatalf("long-intent term %q not searchable", term)
}
// 不應留下整句長字串
if utf8Count(term) > 12 {
t.Fatalf("term too long: %q", term)
}
}
// 應含地區或市集相關
joined := strings.Join(got2, "|")
if !strings.Contains(joined, "市集") && !strings.Contains(joined, "台北") {
t.Fatalf("expected 市集/台北 in variants, got %v", got2)
}
}
func TestPlanActivityTermsNotJustTokenizeIntent(t *testing.T) {
// 舊行為tokenizeIntent("週末市集") → ["週末市集"] 或空拆
// 新行為:至少有錨點變體
got := planActivityTerms("週末市集")
if len(got) < 2 {
t.Fatalf("got %v, want multiple generated variants", got)
}
// 若只有把四字當一詞,也要再有「週末 市集」類或「市集 推薦」
onlyExact := len(got) == 1 && got[0] == "週末市集"
if onlyExact {
t.Fatal("still only returning exact intent — not generating")
}
}
2026-08-13 02:22:24 +00:00
func TestPlanActivityTermsInfersBackendFreelanceIntent(t *testing.T) {
got := planActivityTerms("外包 工程師 後端")
if len(got) == 0 || got[0] != "後端 外包" {
t.Fatalf("terms=%v want 後端 外包 as primary intent query", got)
}
joined := "|" + strings.Join(got, "|") + "|"
for _, want := range []string{"|後端 外包|", "|後端 接案|", "|後端 工程師|"} {
if !strings.Contains(joined, want) {
t.Fatalf("terms=%v missing inferred query %s", got, want)
}
}
for _, term := range got {
if !radarDomain.IsThreadsSearchable(term) {
t.Fatalf("term %q not Threads-searchable", term)
}
}
}
func TestPlanActivityTermsPreservesExactTitleIntent(t *testing.T) {
got := planActivityTerms("鬼滅之刃")
if len(got) == 0 || got[0] != "鬼滅之刃" {
t.Fatalf("terms=%v want exact title as primary query", got)
}
joined := "|" + strings.Join(got, "|") + "|"
for _, want := range []string{"|鬼滅之刃 最新|", "|鬼滅之刃 熱門|"} {
if !strings.Contains(joined, want) {
t.Fatalf("terms=%v missing discovery query %s", got, want)
}
}
for _, bad := range []string{"鬼滅之", "滅之刃"} {
if strings.Contains(joined, "|"+bad+"|") {
t.Fatalf("broken title fragment %q leaked: %v", bad, got)
}
}
}
func TestMergeActivityTermsKeepsDeterministicIntentFirst(t *testing.T) {
2026-08-09 07:57:35 +00:00
ai := []string{"市集 分享", "文青 市集", "太長了吧這整句不行"}
rules := []string{"市集 推薦", "市集"}
got := mergeActivityTerms(ai, rules, 6)
if len(got) == 0 {
t.Fatal("empty merge")
}
2026-08-13 02:22:24 +00:00
if got[0] != "市集 推薦" {
t.Fatalf("deterministic intent term should come first, got %v", got)
2026-08-09 07:57:35 +00:00
}
// 長詞應被濾掉
for _, t2 := range got {
if strings.Contains(t2, "太長") {
t.Fatalf("unssearchable term leaked: %v", got)
}
}
}
func TestParseActivityAITerms(t *testing.T) {
raw := "這裡是開場\n[\"市集 分享\", \"週末 市集\"]\n結尾"
got := parseActivityAITerms(raw)
if len(got) != 2 {
t.Fatalf("parse strings: %v", got)
}
raw2 := `[{"term":"市集 心得","reason":"x"},{"term":"活動 市集"}]`
got2 := parseActivityAITerms(raw2)
if len(got2) != 2 {
t.Fatalf("parse objects: %v", got2)
}
}
func utf8Count(s string) int {
return len([]rune(strings.ReplaceAll(s, " ", "")))
}