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

144 lines
4.7 KiB
Go
Raw Normal View History

2026-08-13 02:22:24 +00:00
package usecase
import (
"context"
"fmt"
"testing"
"apps/backend/internal/module/scout/domain"
"apps/backend/internal/module/scout/repository"
)
type stagedPipelineProvider struct {
calls []int
firstBad int
allRelevant bool
}
type oneTermExpansionProvider struct {
calls []string
}
func (p *oneTermExpansionProvider) SearchThreads(_ context.Context, terms []string, limit int) ([]ThreadSearchResult, error) {
term := ""
if len(terms) > 0 {
term = terms[0]
}
p.calls = append(p.calls, term)
out := make([]ThreadSearchResult, 0, limit)
for i := 0; i < limit; i++ {
text := "外包合作正在找人"
if term != "外包" {
text = "想找後端工程師做外包合作,歡迎推薦接案夥伴"
}
out = append(out, ThreadSearchResult{
URL: fmt.Sprintf("https://www.threads.net/@source/post/%s-%d", term, i), Snippet: text,
})
}
return out, nil
}
func (p *stagedPipelineProvider) SearchThreads(_ context.Context, terms []string, limit int) ([]ThreadSearchResult, error) {
call := len(p.calls) + 1
p.calls = append(p.calls, limit)
term := "市集"
if len(terms) > 0 {
term = terms[0]
}
out := make([]ThreadSearchResult, 0, limit)
for i := 0; i < limit; i++ {
text := term + "討論內容"
if !p.allRelevant && call == 1 && i < p.firstBad {
text = "完全無關的內容"
}
out = append(out, ThreadSearchResult{
URL: fmt.Sprintf("https://www.threads.net/@source/post/c%d-%d", call, i), Snippet: text,
})
}
return out, nil
}
func newPipelineService(provider ThreadSearchProvider) *Service {
svc := New(repository.NewMemory())
svc.Provider = provider
return svc
}
func TestSearchPipelineContinuesWhenRawHitsExceedEligibleHits(t *testing.T) {
provider := &stagedPipelineProvider{firstBad: 13}
svc := newPipelineService(provider)
result, err := svc.searchEligiblePipeline(context.Background(), 7, []string{"市集"}, &domain.RunBrief{
Mode: domain.ModeActivity, Intent: "市集", ScanTerms: []string{"市集"},
}, 10, domain.PathAPI, "", 20)
if err != nil {
t.Fatal(err)
}
if len(result.Hits) != 10 || result.Diagnostics.EligibleCount != 10 {
t.Fatalf("pipeline result=%+v", result)
}
if len(provider.calls) != 2 || provider.calls[0] != 20 || provider.calls[1] != 20 {
t.Fatalf("calls=%v want initial + same-source boost", provider.calls)
}
if result.Diagnostics.IrrelevantCount != 13 || result.Diagnostics.RawCount != 40 {
t.Fatalf("diagnostics=%+v", result.Diagnostics)
}
}
func TestSearchPipelineStopsAtEligibleTarget(t *testing.T) {
provider := &stagedPipelineProvider{allRelevant: true}
svc := newPipelineService(provider)
result, err := svc.searchEligiblePipeline(context.Background(), 7, []string{"市集", "活動"}, &domain.RunBrief{
Mode: domain.ModeActivity, Intent: "市集", ScanTerms: []string{"市集", "活動"},
}, 3, domain.PathAPI, "", 20)
if err != nil {
t.Fatal(err)
}
if len(result.Hits) != 3 || len(provider.calls) != 1 {
t.Fatalf("result=%+v calls=%v; target should stop first query", result, provider.calls)
}
if result.Diagnostics.Stages != 1 {
t.Fatalf("stages=%d want 1", result.Diagnostics.Stages)
}
}
func TestSearchPipelineCapsRawCandidatesAt320(t *testing.T) {
provider := &stagedPipelineProvider{allRelevant: true}
svc := newPipelineService(provider)
terms := []string{"一", "二", "三", "四", "五", "六", "七", "八"}
result, err := svc.searchEligiblePipeline(context.Background(), 7, terms, &domain.RunBrief{
Mode: domain.ModeActivity, Intent: "市集", ScanTerms: []string{"市集"},
}, 999, domain.PathAPI, "", 20)
if err != nil {
t.Fatal(err)
}
if result.Diagnostics.RawCount != maxPipelineRawCandidates || result.Diagnostics.Stages != 2 {
t.Fatalf("diagnostics=%+v", result.Diagnostics)
}
if len(provider.calls) != 16 {
t.Fatalf("calls=%d want 16 (8 terms x 2 stages)", len(provider.calls))
}
if len(result.Diagnostics.ShortfallReasons) == 0 {
t.Fatal("raw cap shortfall reason missing")
}
}
func TestSearchPipelineExpandsOneApprovedTermWhenProviderRepeatsFirstPage(t *testing.T) {
provider := &oneTermExpansionProvider{}
svc := newPipelineService(provider)
result, err := svc.searchEligiblePipeline(context.Background(), 7, []string{"外包"}, &domain.RunBrief{
Mode: domain.ModeActivity, Intent: "外包 後端工程師", ScanTerms: []string{"外包"},
}, 5, domain.PathAPI, "", 20)
if err != nil {
t.Fatal(err)
}
if len(result.Hits) != 5 {
t.Fatalf("expanded result=%+v, want five eligible hits", result)
}
if len(provider.calls) < 3 || provider.calls[0] != "外包" || provider.calls[1] != "外包" {
t.Fatalf("calls=%v, want repeated first query before expansion", provider.calls)
}
if result.Diagnostics.Stages != 3 {
t.Fatalf("stages=%d, want initial, boost, expansion", result.Diagnostics.Stages)
}
}