package placement import ( "testing" "time" libkg "haixun-backend/internal/library/knowledge" ) func TestHasCategoryConflictLaundryVsDish(t *testing.T) { ctx := PostScanContextFromPatrolInput(libkg.PatrolTagInput{ ProductName: "天然洗衣精", MatchTags: []string{"洗衣精", "衣物清潔"}, }, nil) text := "請問大家洗碗機清潔劑有推薦嗎?碗盤油污洗不掉" if !HasCategoryConflict(text, ctx) { t.Fatal("expected dishwasher post to conflict with laundry product") } if PassesPostScanFilters(text, "洗衣精 推薦", "", QueryRecency, 80, ctx) { t.Fatal("expected conflicting post to be filtered out") } } func TestPassesPostScanFiltersAcceptsMatchingLaundry(t *testing.T) { ctx := PostScanContextFromPatrolInput(libkg.PatrolTagInput{ ProductName: "天然洗衣精", MatchTags: []string{"洗衣精", "衣物"}, Pillars: []string{"衣物清潔", "敏感皮膚洗衣"}, }, []string{"洗碗精等其他品類"}) text := "最近衣服洗不乾淨,洗衣精有推薦嗎?污漬一直洗不掉" if !PassesPostScanFilters(text, "洗衣精 推薦", time.Now().Format("2006-01-02"), QueryRecency, 70, ctx) { t.Fatal("expected matching laundry post to pass") } } func TestPassesPostAgeAllowsMissingDateOnRelevance(t *testing.T) { if !PassesPostAge("", QueryRelevance, 0) { t.Fatal("expected missing posted_at to pass age gate at ingest") } } func TestPassesDiscoverFiltersAllowsWebSnippetWithoutDate(t *testing.T) { ctx := PostScanContextFromPatrolInput(libkg.PatrolTagInput{ ProductName: "天然洗衣精", MatchTags: []string{"洗衣精", "衣物"}, }, nil) text := "最近衣服洗不乾淨,洗衣精有推薦嗎?污漬一直洗不掉" if !PassesDiscoverFilters(text, "洗衣精 推薦", "", QueryRelevance, 70, 0, ctx) { t.Fatal("expected web snippet without date to pass discover filters") } } func TestPassesPostAgeRejectsOldRelevancePost(t *testing.T) { old := time.Now().AddDate(0, 0, -20).Format("2006-01-02") if PassesPostAge(old, QueryRelevance, 0) { t.Fatal("expected old relevance post to fail age gate") } } func TestHasPainPointDemandRejectsCasualChat(t *testing.T) { if HasPainPointDemand("今天天氣真好,晒照給大家看") { t.Fatal("expected casual chat without demand to fail") } } func TestHasPainPointDemandAcceptsHelpSeeking(t *testing.T) { text := "最近皮膚一直癢,沐浴乳有推薦嗎?" if !HasPainPointDemand(text) { t.Fatal("expected help-seeking post with pain signal to pass") } } func TestPostSolvedByProductRejectsCategoryMismatch(t *testing.T) { ctx := PostScanContextFromPatrolInput(libkg.PatrolTagInput{ ProductName: "天然洗衣精", MatchTags: []string{"洗衣精"}, }, nil) text := "洗碗精有推薦嗎?碗盤油污洗不掉" if PostSolvedByProduct(text, 80, ctx) { t.Fatal("expected dish post not to be solved by laundry product") } } func TestHasTangentialTopicMismatchRejectsWashingMachine(t *testing.T) { ctx := PostScanContextFromPatrolInput(libkg.PatrolTagInput{ ProductName: "抗敏無香洗衣精", ProductFeatures: "完全無香、抗敏、適合敏感肌與化療族群", MatchTags: []string{"無香", "抗敏", "洗衣精"}, Pillars: []string{"化療後衣物清潔", "無香洗衣"}, }, nil) text := "請問大家有推薦的洗衣機嗎?想買洗脫烘" if !HasTangentialTopicMismatch(text, ctx) { t.Fatal("expected washing machine post to be tangential mismatch for hypoallergenic detergent") } if PassesPostScanFilters(text, "無香 洗衣精 推薦", time.Now().Format("2006-01-02"), QueryRecency, 80, ctx) { t.Fatal("expected washing machine post to be filtered out") } } func TestHasTangentialTopicMismatchAcceptsDetergentPain(t *testing.T) { ctx := PostScanContextFromPatrolInput(libkg.PatrolTagInput{ ProductName: "抗敏無香洗衣精", ProductFeatures: "完全無香、抗敏", MatchTags: []string{"無香", "洗衣精"}, }, nil) text := "化療後對香味很敏感,有無香洗衣精推薦嗎?" if HasTangentialTopicMismatch(text, ctx) { t.Fatal("expected detergent pain post to pass tangential check") } } func TestHasTangentialTopicMismatchAcceptsMachineWithDetergentNeed(t *testing.T) { ctx := PostScanContextFromPatrolInput(libkg.PatrolTagInput{ ProductName: "抗敏無香洗衣精", ProductFeatures: "完全無香、抗敏", MatchTags: []string{"無香", "洗衣精"}, }, nil) text := "買了洗衣機,但想找不會讓皮膚癢的無香洗衣精" if HasTangentialTopicMismatch(text, ctx) { t.Fatal("expected combined machine + detergent pain post to pass") } } func TestPostSolvedByProductAcceptsAlignedDemand(t *testing.T) { ctx := PostScanContextFromPatrolInput(libkg.PatrolTagInput{ ProductName: "天然洗衣精", MatchTags: []string{"洗衣精", "衣物"}, Pillars: []string{"衣物清潔"}, }, nil) text := "衣服污漬洗不掉,洗衣精有推薦嗎?" if !PostSolvedByProduct(text, 70, ctx) { t.Fatal("expected aligned laundry demand to be solved by product") } } func TestExpandExclusionTokensIncludesDishSynonyms(t *testing.T) { tokens := ExpandExclusionTokens([]string{"洗碗精等其他品類"}) found := false for _, token := range tokens { if token == "洗碗機" || token == "洗碗精" { found = true break } } if !found { t.Fatalf("expected dish synonyms, got %v", tokens) } }