150 lines
4.5 KiB
Go
150 lines
4.5 KiB
Go
package usecase
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
|
||
radarDomain "apps/backend/internal/module/radar/domain"
|
||
"apps/backend/internal/module/scout/domain"
|
||
)
|
||
|
||
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")
|
||
}
|
||
}
|
||
|
||
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) {
|
||
ai := []string{"市集 分享", "文青 市集", "太長了吧這整句不行"}
|
||
rules := []string{"市集 推薦", "市集"}
|
||
got := mergeActivityTerms(ai, rules, 6)
|
||
if len(got) == 0 {
|
||
t.Fatal("empty merge")
|
||
}
|
||
if got[0] != "市集 推薦" {
|
||
t.Fatalf("deterministic intent term should come first, got %v", got)
|
||
}
|
||
// 長詞應被濾掉
|
||
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, " ", "")))
|
||
}
|