198 lines
5.5 KiB
Go
198 lines
5.5 KiB
Go
package knowledge
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"haixun-backend/internal/library/websearch"
|
|
)
|
|
|
|
func TestUniqueSourceCount(t *testing.T) {
|
|
count := uniqueSourceCount([]BraveSource{
|
|
{URL: "https://a.example"},
|
|
{URL: "https://a.example"},
|
|
{URL: "https://b.example"},
|
|
{URL: ""},
|
|
})
|
|
if count != 2 {
|
|
t.Fatalf("expected 2 unique urls, got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestSourcesAreSufficientRequiresProcessedQuality(t *testing.T) {
|
|
items := make([]BraveSource, 0, 14)
|
|
for i := 0; i < 14; i++ {
|
|
items = append(items, BraveSource{
|
|
Query: "q1",
|
|
URL: fmt.Sprintf("https://example.com/%d", i),
|
|
Title: "薄",
|
|
})
|
|
}
|
|
cfg := BraveCollectConfig{MinSourcesBeforeStop: 14, MaxSourcesCap: 22}
|
|
if sourcesAreSufficient(items, []string{"q1", "q2"}, cfg) {
|
|
t.Fatal("thin same-query results should not be considered sufficient")
|
|
}
|
|
|
|
for i := range items {
|
|
items[i].Snippet = "這是一段整理後足以支撐判斷的搜尋摘要內容,包含痛點與情境"
|
|
if i >= 7 {
|
|
items[i].Query = "q2"
|
|
}
|
|
}
|
|
if !sourcesAreSufficient(items, []string{"q1", "q2"}, cfg) {
|
|
t.Fatal("diverse useful results should be considered sufficient")
|
|
}
|
|
}
|
|
|
|
func TestCollectWebSourcesParallelStopsAfterSufficientBatch(t *testing.T) {
|
|
client := &fakeSearchClient{results: map[string][]websearch.SearchResult{}}
|
|
for qi := 0; qi < 5; qi++ {
|
|
query := fmt.Sprintf("q%d", qi+1)
|
|
for ri := 0; ri < 7; ri++ {
|
|
client.results[query] = append(client.results[query], websearch.SearchResult{
|
|
URL: fmt.Sprintf("https://example.com/%s/%d", query, ri),
|
|
Title: "有用來源",
|
|
Snippet: "這是一段整理後足以支撐判斷的搜尋摘要內容,包含痛點與情境",
|
|
})
|
|
}
|
|
}
|
|
|
|
out := CollectWebSources(
|
|
context.Background(),
|
|
client,
|
|
BraveSearchLocale{},
|
|
[]string{"q1", "q2", "q3", "q4", "q5"},
|
|
BraveCollectConfig{ResultsPerQuery: 7, MinSourcesBeforeStop: 14, MaxSourcesCap: 30, Concurrency: 2},
|
|
nil,
|
|
nil,
|
|
)
|
|
if len(out) != 14 {
|
|
t.Fatalf("expected first batch to provide 14 sources, got %d", len(out))
|
|
}
|
|
if got := client.callCount(); got != 2 {
|
|
t.Fatalf("expected collection to stop after first sufficient batch, called %d queries", got)
|
|
}
|
|
}
|
|
|
|
func TestPlanQueriesHybridBudget(t *testing.T) {
|
|
queries := PlanQueries(PlanInput{
|
|
Seed: "敏感肌",
|
|
TargetAudience: "孕婦",
|
|
Questions: []string{"q1", "q2", "q3", "q4"},
|
|
Pillars: []string{"p1", "p2", "p3"},
|
|
L1Labels: []string{"a", "b", "c", "d"},
|
|
Strategy: ExpandStrategyHybrid,
|
|
})
|
|
if len(queries) > 4 {
|
|
t.Fatalf("hybrid should cap at 4 queries, got %d: %v", len(queries), queries)
|
|
}
|
|
if len(queries) == 0 {
|
|
t.Fatal("expected some queries")
|
|
}
|
|
}
|
|
|
|
func TestPlanQueriesPrioritizesPatrolKeywords(t *testing.T) {
|
|
queries := PlanQueries(PlanInput{
|
|
Seed: "敏感肌",
|
|
PatrolKeywords: []string{"化療 沐浴乳", "無香沐浴乳 推薦"},
|
|
Questions: []string{"很長的受眾提問一", "很長的受眾提問二"},
|
|
Strategy: ExpandStrategyBrave,
|
|
})
|
|
if len(queries) == 0 {
|
|
t.Fatal("expected queries")
|
|
}
|
|
if queries[0] != "化療 沐浴乳" {
|
|
t.Fatalf("expected patrol keyword first, got %q", queries[0])
|
|
}
|
|
}
|
|
|
|
func TestQueriesExcept(t *testing.T) {
|
|
remaining := QueriesExcept(
|
|
[]string{"a", "b", "c", "d"},
|
|
[]string{"a", "c"},
|
|
)
|
|
if len(remaining) != 2 || remaining[0] != "b" || remaining[1] != "d" {
|
|
t.Fatalf("unexpected remaining: %v", remaining)
|
|
}
|
|
}
|
|
|
|
func TestPlanBootstrapQueriesSkipsResearchMapFields(t *testing.T) {
|
|
full := PlanQueries(PlanInput{
|
|
Seed: "敏感肌",
|
|
Questions: []string{"q1"},
|
|
Pillars: []string{"p1"},
|
|
Strategy: ExpandStrategyBrave,
|
|
})
|
|
bootstrap := PlanBootstrapQueries(PlanInput{
|
|
Seed: "敏感肌",
|
|
Questions: []string{"q1"},
|
|
Pillars: []string{"p1"},
|
|
Strategy: ExpandStrategyBrave,
|
|
})
|
|
if len(bootstrap) == 0 {
|
|
t.Fatal("expected bootstrap queries")
|
|
}
|
|
for _, q := range bootstrap {
|
|
if q == "q1" || q == "p1 請問" {
|
|
t.Fatalf("bootstrap should not include research map query %q", q)
|
|
}
|
|
}
|
|
if len(full) <= len(bootstrap) {
|
|
t.Fatalf("full plan should include more queries than bootstrap: full=%v bootstrap=%v", full, bootstrap)
|
|
}
|
|
}
|
|
|
|
func TestMergeBraveSourcesDedupesURLs(t *testing.T) {
|
|
merged := MergeBraveSources(
|
|
[]BraveSource{{URL: "https://a.example", Query: "q1"}},
|
|
[]BraveSource{{URL: "https://a.example", Query: "q2"}, {URL: "https://b.example", Query: "q3"}},
|
|
)
|
|
if len(merged) != 2 {
|
|
t.Fatalf("expected 2 merged sources, got %d", len(merged))
|
|
}
|
|
}
|
|
|
|
func TestSupplementalQueriesSkippedForHybrid(t *testing.T) {
|
|
queries := PlanQueries(PlanInput{
|
|
Seed: "敏感肌",
|
|
Supplemental: true,
|
|
Strategy: ExpandStrategyHybrid,
|
|
})
|
|
if len(queries) != 0 {
|
|
t.Fatalf("hybrid supplemental brave should be empty, got %v", queries)
|
|
}
|
|
}
|
|
|
|
type fakeSearchClient struct {
|
|
mu sync.Mutex
|
|
calls []string
|
|
results map[string][]websearch.SearchResult
|
|
}
|
|
|
|
func (f *fakeSearchClient) Enabled() bool { return true }
|
|
|
|
func (f *fakeSearchClient) Provider() websearch.Provider { return websearch.ProviderBrave }
|
|
|
|
func (f *fakeSearchClient) Search(_ context.Context, opts websearch.SearchOptions) (websearch.SearchResponse, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.results == nil {
|
|
f.results = map[string][]websearch.SearchResult{}
|
|
}
|
|
f.calls = append(f.calls, opts.Query)
|
|
return websearch.SearchResponse{
|
|
Query: opts.Query,
|
|
Status: "success",
|
|
Provider: websearch.ProviderBrave,
|
|
Results: append([]websearch.SearchResult(nil), f.results[opts.Query]...),
|
|
}, nil
|
|
}
|
|
|
|
func (f *fakeSearchClient) callCount() int {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return len(f.calls)
|
|
}
|