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

122 lines
4.9 KiB
Go
Raw Permalink Normal View History

2026-08-13 02:22:24 +00:00
package usecase
import (
"strings"
"testing"
)
func TestTopicSignatureExcludesAnchorsAndKeepsNamedEntity(t *testing.T) {
sig := NewTopicSignature("鬼滅之刃", []string{"鬼滅之刃 最新", "鬼滅 分享"})
if len(sig.Concepts) != 1 {
t.Fatalf("concepts=%+v, want one named-entity concept", sig.Concepts)
}
if !sig.Matches("鬼滅之刃最新一集大家都在討論") {
t.Fatal("named entity should match")
}
if sig.Matches("最近想看動畫,有推薦熱門作品嗎") {
t.Fatal("generic recommendation must not bypass entity gate")
}
if len(sig.Concepts[0].Aliases) != 1 || sig.Concepts[0].Aliases[0] != "鬼滅" {
t.Fatalf("aliases=%+v, want explicit 鬼滅 alias", sig.Concepts[0].Aliases)
}
}
func TestTopicSignatureRequiresEveryConceptGroup(t *testing.T) {
sig := BuildTopicSignature("外包 後端工程師", []string{"後端 外包", "後端工程師 推薦"})
if len(sig.Concepts) != 2 {
t.Fatalf("concepts=%+v, want outsourcing and backend groups", sig.Concepts)
}
if !sig.Matches("想找後端工程師做外包合作,有推薦的接案夥伴嗎") {
t.Fatal("candidate with both concepts should match")
}
missing := sig.Match("請問一般工程師轉職需要準備什麼")
if missing.Matched || len(missing.MissingGroups) != 2 {
t.Fatalf("missing concepts were not rejected: %+v", missing)
}
if sig.Matches("正在找後端工程師轉職") {
t.Fatal("explicitly missing outsourcing concept should fail")
}
}
func TestTopicSignatureDoesNotTurnConjunctionFragmentsIntoAliases(t *testing.T) {
sig := NewTopicSignature("外包 後端工程師", []string{"後端 外包"})
if sig.Matches("有人分享後端外包合作經驗") {
t.Fatal("a conjunction fragment must not alias 後端工程師")
}
if !sig.Matches("有人分享後端工程師外包合作經驗") {
t.Fatal("full backend-engineer and outsourcing concepts should match")
}
}
func TestTopicSignatureUsesApprovedSplitAndLatinCase(t *testing.T) {
sig := NewTopicSignature("台北週末市集", []string{"台北 市集", "週末 市集"})
if len(sig.Concepts) != 3 {
t.Fatalf("split concepts=%+v, want city/weekend/market", sig.Concepts)
}
if !sig.Matches("這週末台北有沒有適合拍照的市集?") {
t.Fatal("approved split concepts should match natural sentence")
}
if sig.Matches("台北週末天氣好像會下雨") {
t.Fatal("weather post missing market concept")
}
latin := NewTopicSignature("OpenAI API", []string{"openai api"})
if !latin.Matches("請問 OPENAI API 要怎麼申請") {
t.Fatal("latin topic matching should ignore case")
}
}
func TestTopicSignatureDecomposesNaturalIntentWithoutRequiringWholeSentence(t *testing.T) {
sig := NewTopicSignature("想找台北週末市集可以拍什麼", []string{"台北 市集", "週末 市集"})
if len(sig.Concepts) != 2 {
t.Fatalf("natural concepts=%+v, want location and topic", sig.Concepts)
}
if !sig.Matches("台北這週有市集活動,適合拍照") {
t.Fatal("natural sentence should match its meaningful concepts")
}
if sig.Matches("台北這週有咖啡活動,適合拍照") {
t.Fatal("different topic should remain irrelevant")
}
}
func TestTopicSignatureKeepsNaturalWorkConceptsButDropsGeneratedVariants(t *testing.T) {
sig := NewTopicSignature("找後端工程師接案", []string{"後端 外包", "後端 接案", "後端 工程師"})
if len(sig.Concepts) != 3 {
t.Fatalf("work concepts=%+v, want backend/engineer/freelance", sig.Concepts)
}
if !sig.Matches("正在找後端工程師接案夥伴") {
t.Fatal("natural work intent should match")
}
if !sig.Matches("正在找後端工程師做外包合作") {
t.Fatal("接案 and 外包 should be bounded work-intent synonyms")
}
if sig.Matches("正在找前端工程師接案夥伴") {
t.Fatal("different specialty should remain irrelevant")
}
}
func TestTopicSignatureExtractsTopicFromQuestionLikeIntent(t *testing.T) {
sig := NewTopicSignature("週末有插座", []string{"插座 求推薦", "咖啡 插座"})
if len(sig.Concepts) != 1 || !sig.Matches("請問這間咖啡店週末有插座嗎?") {
t.Fatalf("question-like intent signature=%+v", sig)
}
}
func TestTopicSignatureExtractsNamedEntityAfterRecommendationPhrase(t *testing.T) {
sig := NewTopicSignature("推薦鬼滅之刃", []string{"鬼滅之刃 最新", "鬼滅 分享"})
if len(sig.Concepts) != 1 || !sig.Matches("最近在討論鬼滅之刃最新一集") {
t.Fatalf("recommendation phrase signature=%+v", sig)
}
}
func TestTopicSignatureReasonNamesMatchedCores(t *testing.T) {
sig := NewTopicSignature("外包 後端", nil)
match := sig.Match("後端工程師正在找外包合作")
if !match.Matched || !strings.Contains(match.Reason, "外包") || !strings.Contains(match.Reason, "後端") {
t.Fatalf("reason=%q does not explain matched cores", match.Reason)
}
anchors := NewTopicSignature("推薦 分享", nil)
if len(anchors.Concepts) != 0 || anchors.Matches("推薦分享") {
t.Fatal("anchor-only intent must not become a topic")
}
}