- Updated the CitationGenerator to produce simple integer IDs instead of formatted strings, improving clarity and consistency in citation references. - Enhanced the executeAutoSearch method to save both successful and failed search results, capturing detailed execution data for better traceability. - Introduced a new SearchExecutionResult type to structure search result data, including query, keywords, configuration, duration, and error information. - Updated related tests to reflect changes in citation ID format and ensure proper functionality of the new storage mechanisms. - Revised documentation to clarify the new citation format and search result handling processes.
403 lines
9 KiB
Go
403 lines
9 KiB
Go
package search
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/yaoapp/yao/agent/search/types"
|
|
)
|
|
|
|
func TestNew(t *testing.T) {
|
|
t.Run("with nil config and uses", func(t *testing.T) {
|
|
s := New(nil, nil)
|
|
assert.NotNil(t, s)
|
|
assert.NotNil(t, s.config)
|
|
assert.NotNil(t, s.handlers)
|
|
assert.NotNil(t, s.citation)
|
|
assert.Equal(t, 3, len(s.handlers)) // web, kb, db
|
|
})
|
|
|
|
t.Run("with config", func(t *testing.T) {
|
|
cfg := &types.Config{
|
|
Web: &types.WebConfig{
|
|
Provider: "tavily",
|
|
MaxResults: 10,
|
|
},
|
|
KB: &types.KBConfig{
|
|
Collections: []string{"docs"},
|
|
Threshold: 0.8,
|
|
},
|
|
DB: &types.DBConfig{
|
|
Models: []string{"product"},
|
|
MaxResults: 20,
|
|
},
|
|
}
|
|
s := New(cfg, nil)
|
|
assert.NotNil(t, s)
|
|
assert.Equal(t, cfg, s.config)
|
|
})
|
|
|
|
t.Run("with uses", func(t *testing.T) {
|
|
uses := &Uses{
|
|
Search: "builtin",
|
|
Web: "builtin",
|
|
Keyword: "builtin",
|
|
QueryDSL: "builtin",
|
|
Rerank: "builtin",
|
|
}
|
|
s := New(nil, uses)
|
|
assert.NotNil(t, s)
|
|
})
|
|
}
|
|
|
|
func TestSearcher_Search_UnsupportedType(t *testing.T) {
|
|
s := New(nil, nil)
|
|
|
|
req := &types.Request{
|
|
Type: "unsupported",
|
|
Query: "test",
|
|
}
|
|
|
|
result, err := s.Search(nil, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, "unsupported search type", result.Error)
|
|
}
|
|
|
|
func TestSearcher_Search_Web(t *testing.T) {
|
|
// Note: This test uses skeleton handlers that return empty results
|
|
// Real tests with actual API calls are in handlers/web/*_test.go
|
|
cfg := &types.Config{
|
|
Web: &types.WebConfig{
|
|
Provider: "tavily",
|
|
},
|
|
}
|
|
s := New(cfg, &Uses{Web: "builtin"})
|
|
|
|
req := &types.Request{
|
|
Type: types.SearchTypeWeb,
|
|
Query: "test query",
|
|
Source: types.SourceAuto,
|
|
}
|
|
|
|
result, err := s.Search(nil, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, types.SearchTypeWeb, result.Type)
|
|
assert.Equal(t, "test query", result.Query)
|
|
// Note: actual result depends on API key availability
|
|
}
|
|
|
|
func TestSearcher_Search_KB(t *testing.T) {
|
|
cfg := &types.Config{
|
|
KB: &types.KBConfig{
|
|
Collections: []string{"docs"},
|
|
Threshold: 0.7,
|
|
},
|
|
}
|
|
s := New(cfg, nil)
|
|
|
|
req := &types.Request{
|
|
Type: types.SearchTypeKB,
|
|
Query: "test query",
|
|
Source: types.SourceHook,
|
|
Collections: []string{"docs"},
|
|
}
|
|
|
|
result, err := s.Search(nil, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, types.SearchTypeKB, result.Type)
|
|
assert.Equal(t, "test query", result.Query)
|
|
assert.Equal(t, types.SourceHook, result.Source)
|
|
// Skeleton returns empty items
|
|
assert.Equal(t, 0, len(result.Items))
|
|
}
|
|
|
|
func TestSearcher_Search_DB(t *testing.T) {
|
|
cfg := &types.Config{
|
|
DB: &types.DBConfig{
|
|
Models: []string{"product"},
|
|
MaxResults: 20,
|
|
},
|
|
}
|
|
s := New(cfg, &Uses{QueryDSL: "builtin"})
|
|
|
|
req := &types.Request{
|
|
Type: types.SearchTypeDB,
|
|
Query: "find products under $100",
|
|
Source: types.SourceUser,
|
|
Models: []string{"product"},
|
|
}
|
|
|
|
result, err := s.Search(nil, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, types.SearchTypeDB, result.Type)
|
|
assert.Equal(t, "find products under $100", result.Query)
|
|
assert.Equal(t, types.SourceUser, result.Source)
|
|
// Skeleton returns empty items
|
|
assert.Equal(t, 0, len(result.Items))
|
|
}
|
|
|
|
func TestSearcher_Search_WeightAssignment(t *testing.T) {
|
|
cfg := &types.Config{
|
|
KB: &types.KBConfig{
|
|
Collections: []string{"docs"},
|
|
},
|
|
Weights: &types.WeightsConfig{
|
|
User: 1.0,
|
|
Hook: 0.8,
|
|
Auto: 0.6,
|
|
},
|
|
}
|
|
s := New(cfg, nil)
|
|
|
|
// Test with different sources
|
|
sources := []struct {
|
|
source types.SourceType
|
|
weight float64
|
|
}{
|
|
{types.SourceUser, 1.0},
|
|
{types.SourceHook, 0.8},
|
|
{types.SourceAuto, 0.6},
|
|
}
|
|
|
|
for _, tc := range sources {
|
|
req := &types.Request{
|
|
Type: types.SearchTypeKB,
|
|
Query: "test",
|
|
Source: tc.source,
|
|
Collections: []string{"docs"},
|
|
}
|
|
result, err := s.Search(nil, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
// Items are empty in skeleton, so weight assignment can't be verified here
|
|
// This test ensures the code path works without error
|
|
}
|
|
}
|
|
|
|
func TestSearcher_All(t *testing.T) {
|
|
cfg := &types.Config{
|
|
KB: &types.KBConfig{
|
|
Collections: []string{"docs"},
|
|
},
|
|
DB: &types.DBConfig{
|
|
Models: []string{"product"},
|
|
},
|
|
}
|
|
s := New(cfg, nil)
|
|
|
|
reqs := []*types.Request{
|
|
{
|
|
Type: types.SearchTypeKB,
|
|
Query: "KB query",
|
|
Source: types.SourceAuto,
|
|
Collections: []string{"docs"},
|
|
},
|
|
{
|
|
Type: types.SearchTypeDB,
|
|
Query: "DB query",
|
|
Source: types.SourceAuto,
|
|
Models: []string{"product"},
|
|
},
|
|
}
|
|
|
|
// Test All() - waits for all searches to complete (like Promise.all)
|
|
results, err := s.All(nil, reqs)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 2, len(results))
|
|
|
|
// Verify each result corresponds to its request
|
|
assert.Equal(t, types.SearchTypeKB, results[0].Type)
|
|
assert.Equal(t, "KB query", results[0].Query)
|
|
|
|
assert.Equal(t, types.SearchTypeDB, results[1].Type)
|
|
assert.Equal(t, "DB query", results[1].Query)
|
|
}
|
|
|
|
func TestSearcher_Any(t *testing.T) {
|
|
cfg := &types.Config{
|
|
KB: &types.KBConfig{
|
|
Collections: []string{"docs"},
|
|
},
|
|
DB: &types.DBConfig{
|
|
Models: []string{"product"},
|
|
},
|
|
}
|
|
s := New(cfg, nil)
|
|
|
|
reqs := []*types.Request{
|
|
{
|
|
Type: types.SearchTypeKB,
|
|
Query: "KB query",
|
|
Source: types.SourceAuto,
|
|
Collections: []string{"docs"},
|
|
},
|
|
{
|
|
Type: types.SearchTypeDB,
|
|
Query: "DB query",
|
|
Source: types.SourceAuto,
|
|
Models: []string{"product"},
|
|
},
|
|
}
|
|
|
|
// Test Any() - returns when first search has results (like Promise.any)
|
|
// Note: With skeleton handlers returning empty results, this will wait for all
|
|
results, err := s.Any(nil, reqs)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 2, len(results))
|
|
}
|
|
|
|
func TestSearcher_Race(t *testing.T) {
|
|
cfg := &types.Config{
|
|
KB: &types.KBConfig{
|
|
Collections: []string{"docs"},
|
|
},
|
|
DB: &types.DBConfig{
|
|
Models: []string{"product"},
|
|
},
|
|
}
|
|
s := New(cfg, nil)
|
|
|
|
reqs := []*types.Request{
|
|
{
|
|
Type: types.SearchTypeKB,
|
|
Query: "KB query",
|
|
Source: types.SourceAuto,
|
|
Collections: []string{"docs"},
|
|
},
|
|
{
|
|
Type: types.SearchTypeDB,
|
|
Query: "DB query",
|
|
Source: types.SourceAuto,
|
|
Models: []string{"product"},
|
|
},
|
|
}
|
|
|
|
// Test Race() - returns when first search completes (like Promise.race)
|
|
results, err := s.Race(nil, reqs)
|
|
assert.NoError(t, err)
|
|
// At least one result should be set
|
|
hasResult := false
|
|
for _, r := range results {
|
|
if r != nil {
|
|
hasResult = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasResult)
|
|
}
|
|
|
|
func TestSearcher_All_Empty(t *testing.T) {
|
|
s := New(nil, nil)
|
|
|
|
results, err := s.All(nil, []*types.Request{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 0, len(results))
|
|
}
|
|
|
|
func TestSearcher_Any_Empty(t *testing.T) {
|
|
s := New(nil, nil)
|
|
|
|
results, err := s.Any(nil, []*types.Request{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 0, len(results))
|
|
}
|
|
|
|
func TestSearcher_Race_Empty(t *testing.T) {
|
|
s := New(nil, nil)
|
|
|
|
results, err := s.Race(nil, []*types.Request{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 0, len(results))
|
|
}
|
|
|
|
func TestSearcher_All_ManyRequests(t *testing.T) {
|
|
cfg := &types.Config{
|
|
KB: &types.KBConfig{
|
|
Collections: []string{"docs"},
|
|
},
|
|
}
|
|
s := New(cfg, nil)
|
|
|
|
// Create multiple requests to test parallel execution
|
|
reqs := make([]*types.Request, 10)
|
|
for i := 0; i < 10; i++ {
|
|
reqs[i] = &types.Request{
|
|
Type: types.SearchTypeKB,
|
|
Query: "test query",
|
|
Source: types.SourceAuto,
|
|
Collections: []string{"docs"},
|
|
}
|
|
}
|
|
|
|
results, err := s.All(nil, reqs)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 10, len(results))
|
|
|
|
// All results should be valid
|
|
for _, result := range results {
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, types.SearchTypeKB, result.Type)
|
|
}
|
|
}
|
|
|
|
func TestSearcher_BuildReferences(t *testing.T) {
|
|
s := New(nil, nil)
|
|
|
|
results := []*types.Result{
|
|
{
|
|
Type: types.SearchTypeWeb,
|
|
Items: []*types.ResultItem{
|
|
{
|
|
CitationID: "1",
|
|
Type: types.SearchTypeWeb,
|
|
Source: types.SourceAuto,
|
|
Weight: 0.6,
|
|
Title: "Web Result",
|
|
Content: "Web content",
|
|
URL: "https://example.com",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Type: types.SearchTypeKB,
|
|
Items: []*types.ResultItem{
|
|
{
|
|
CitationID: "2",
|
|
Type: types.SearchTypeKB,
|
|
Source: types.SourceHook,
|
|
Weight: 0.8,
|
|
Title: "KB Result",
|
|
Content: "KB content",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
refs := s.BuildReferences(results)
|
|
assert.Equal(t, 2, len(refs))
|
|
assert.Equal(t, "1", refs[0].ID)
|
|
assert.Equal(t, "2", refs[1].ID)
|
|
}
|
|
|
|
func TestSearcher_CitationGeneration(t *testing.T) {
|
|
s := New(nil, nil)
|
|
|
|
// Reset citation generator for predictable IDs
|
|
s.citation.Reset()
|
|
|
|
// Note: This test would need actual results with items to verify citation generation
|
|
// The skeleton handlers return empty items, so we test the citation generator directly
|
|
|
|
id1 := s.citation.Next()
|
|
id2 := s.citation.Next()
|
|
id3 := s.citation.Next()
|
|
|
|
// Citation IDs are now simple integers
|
|
assert.Equal(t, "1", id1)
|
|
assert.Equal(t, "2", id2)
|
|
assert.Equal(t, "3", id3)
|
|
}
|