144 lines
4.7 KiB
Go
144 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|