yao/agent/search/handlers/web/serper_test.go
Max e4d1701dab Add Search API configuration and enhance documentation
- Introduced new environment variables for TAVILY_API_KEY, SERPAPI_API_KEY, and SERPER_API_KEY in both `pr-test.yml` and `unit-test.yml` workflows to support additional search providers.
- Updated `DESIGN.md` to reflect the inclusion of SerpAPI as a search provider, detailing its usage and configuration options, including support for multiple search engines.
- Enhanced the `builtinSearch` function in `handler.go` to accommodate the new SerpAPI provider, ensuring proper handling of search requests.
- Revised the `WebConfig` struct in `config.go` to include an `Engine` field for specifying the search engine when using SerpAPI, improving flexibility in search configurations.
- Updated documentation to clarify the roles of new search providers and their integration within the search module, ensuring comprehensive guidance for developers.
2025-12-13 12:37:07 +08:00

327 lines
9.8 KiB
Go

package web_test
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/yao/agent/assistant"
"github.com/yaoapp/yao/agent/search/handlers/web"
"github.com/yaoapp/yao/agent/search/types"
"github.com/yaoapp/yao/agent/testutils"
)
// skipIfNoSerperKey skips the test if SERPER_API_KEY is not set
// Note: Serper (serper.dev) requires registration at https://serper.dev
func skipIfNoSerperKey(t *testing.T) {
if os.Getenv("SERPER_API_KEY") == "" {
t.Skip("Skipping Serper test: SERPER_API_KEY not set. Register at https://serper.dev for free 2500 queries.")
}
}
// TestSerperProviderWithAssistantConfig tests SerperProvider using web-serper assistant config
func TestSerperProviderWithAssistantConfig(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
skipIfNoSerperKey(t)
testutils.Prepare(t)
defer testutils.Clean(t)
// Load the web-serper test assistant to get its config
ast, err := assistant.LoadPath("/assistants/tests/web-serper")
require.NoError(t, err)
require.NotNil(t, ast)
require.NotNil(t, ast.Search)
require.NotNil(t, ast.Search.Web)
// Verify assistant config
assert.Equal(t, "tests.web-serper", ast.ID)
assert.Equal(t, "serper", ast.Search.Web.Provider)
assert.Equal(t, "$ENV.SERPER_API_KEY", ast.Search.Web.APIKeyEnv)
assert.Equal(t, 10, ast.Search.Web.MaxResults)
// Create SerperProvider with assistant's web config
provider := web.NewSerperProvider(ast.Search.Web)
require.NotNil(t, provider)
// Execute search
req := &types.Request{
Query: "Yao App Engine",
Type: types.SearchTypeWeb,
Source: types.SourceAuto,
Limit: 5,
}
result, err := provider.Search(req)
require.NoError(t, err)
require.NotNil(t, result)
// Verify result structure
assert.Equal(t, types.SearchTypeWeb, result.Type)
assert.Equal(t, "Yao App Engine", result.Query)
assert.Equal(t, types.SourceAuto, result.Source)
// API key must be valid - search should succeed
require.Empty(t, result.Error, "Search should succeed with valid API key, got error: %s", result.Error)
// Verify we got results
assert.Greater(t, result.Total, 0)
assert.NotEmpty(t, result.Items)
assert.Greater(t, result.Duration, int64(0))
// Verify result item structure
for _, item := range result.Items {
assert.Equal(t, types.SearchTypeWeb, item.Type)
assert.Equal(t, types.SourceAuto, item.Source)
assert.NotEmpty(t, item.Title)
assert.NotEmpty(t, item.URL)
assert.Greater(t, item.Score, 0.0)
}
t.Logf("Search returned %d results in %dms", result.Total, result.Duration)
}
// TestSerperProviderWithSiteRestriction tests SerperProvider with domain restriction
func TestSerperProviderWithSiteRestriction(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
skipIfNoSerperKey(t)
testutils.Prepare(t)
defer testutils.Clean(t)
// Load the web-serper test assistant
ast, err := assistant.LoadPath("/assistants/tests/web-serper")
require.NoError(t, err)
require.NotNil(t, ast.Search)
require.NotNil(t, ast.Search.Web)
// Create SerperProvider
provider := web.NewSerperProvider(ast.Search.Web)
// Execute search with site restriction
req := &types.Request{
Query: "documentation",
Type: types.SearchTypeWeb,
Source: types.SourceHook,
Sites: []string{"github.com"},
Limit: 3,
}
result, err := provider.Search(req)
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, types.SearchTypeWeb, result.Type)
assert.Equal(t, types.SourceHook, result.Source)
// API key must be valid - search should succeed
require.Empty(t, result.Error, "Search should succeed with valid API key, got error: %s", result.Error)
require.NotEmpty(t, result.Items, "Search should return results")
// All results should be from github.com
for _, item := range result.Items {
assert.Contains(t, item.URL, "github.com", "Result URL should be from github.com")
}
t.Logf("Site-restricted search returned %d results from github.com", result.Total)
}
// TestSerperProviderWithMultipleSites tests SerperProvider with multiple domain restrictions
func TestSerperProviderWithMultipleSites(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
skipIfNoSerperKey(t)
testutils.Prepare(t)
defer testutils.Clean(t)
// Load the web-serper test assistant
ast, err := assistant.LoadPath("/assistants/tests/web-serper")
require.NoError(t, err)
require.NotNil(t, ast.Search)
require.NotNil(t, ast.Search.Web)
// Create SerperProvider
provider := web.NewSerperProvider(ast.Search.Web)
// Execute search with multiple site restrictions
req := &types.Request{
Query: "golang tutorial",
Type: types.SearchTypeWeb,
Source: types.SourceAuto,
Sites: []string{"github.com", "golang.org"},
Limit: 5,
}
result, err := provider.Search(req)
require.NoError(t, err)
require.NotNil(t, result)
// API key must be valid - search should succeed
require.Empty(t, result.Error, "Search should succeed with valid API key, got error: %s", result.Error)
require.NotEmpty(t, result.Items, "Search should return results")
// Results should be from either github.com or golang.org
for _, item := range result.Items {
isValidSite := false
for _, site := range req.Sites {
if contains(item.URL, site) {
isValidSite = true
break
}
}
assert.True(t, isValidSite, "Result URL should be from github.com or golang.org: %s", item.URL)
}
t.Logf("Multi-site search returned %d results", result.Total)
}
// TestSerperProviderWithTimeRange tests SerperProvider with time range filter
func TestSerperProviderWithTimeRange(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
skipIfNoSerperKey(t)
testutils.Prepare(t)
defer testutils.Clean(t)
// Load the web-serper test assistant
ast, err := assistant.LoadPath("/assistants/tests/web-serper")
require.NoError(t, err)
require.NotNil(t, ast.Search)
require.NotNil(t, ast.Search.Web)
// Create SerperProvider
provider := web.NewSerperProvider(ast.Search.Web)
// Execute search with time range
req := &types.Request{
Query: "artificial intelligence news",
Type: types.SearchTypeWeb,
Source: types.SourceAuto,
TimeRange: "week", // Last week
Limit: 5,
}
result, err := provider.Search(req)
require.NoError(t, err)
require.NotNil(t, result)
// API key must be valid - search should succeed
require.Empty(t, result.Error, "Search should succeed with valid API key, got error: %s", result.Error)
t.Logf("Time-ranged search (last week) returned %d results in %dms", result.Total, result.Duration)
}
// TestSerperProviderWithoutAPIKey tests graceful degradation when API key is missing
func TestSerperProviderWithoutAPIKey(t *testing.T) {
// Create provider with nil config (no API key)
provider := web.NewSerperProvider(nil)
require.NotNil(t, provider)
req := &types.Request{
Query: "test query",
Type: types.SearchTypeWeb,
Source: types.SourceAuto,
}
result, err := provider.Search(req)
// Should not return error, but result should have error message
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, types.SearchTypeWeb, result.Type)
assert.Equal(t, "test query", result.Query)
assert.NotEmpty(t, result.Error)
assert.Contains(t, result.Error, "API key")
assert.Empty(t, result.Items)
assert.Equal(t, 0, result.Total)
}
// TestSerperProviderWithEmptyConfig tests provider with empty config
func TestSerperProviderWithEmptyConfig(t *testing.T) {
// Create provider with empty config
cfg := &types.WebConfig{}
provider := web.NewSerperProvider(cfg)
require.NotNil(t, provider)
req := &types.Request{
Query: "test query",
Type: types.SearchTypeWeb,
Source: types.SourceUser,
}
result, err := provider.Search(req)
require.NoError(t, err)
require.NotNil(t, result)
assert.NotEmpty(t, result.Error)
assert.Contains(t, result.Error, "API key")
}
// TestSerperProviderMaxResults tests that max_results from config is respected
func TestSerperProviderMaxResults(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
skipIfNoSerperKey(t)
testutils.Prepare(t)
defer testutils.Clean(t)
// Load the web-serper test assistant
ast, err := assistant.LoadPath("/assistants/tests/web-serper")
require.NoError(t, err)
require.NotNil(t, ast.Search)
require.NotNil(t, ast.Search.Web)
// Create SerperProvider
provider := web.NewSerperProvider(ast.Search.Web)
// Execute search without limit (should use config's max_results)
req := &types.Request{
Query: "machine learning",
Type: types.SearchTypeWeb,
Source: types.SourceAuto,
// No Limit set, should use config's max_results (10)
}
result, err := provider.Search(req)
require.NoError(t, err)
require.NotNil(t, result)
// API key must be valid - search should succeed
require.Empty(t, result.Error, "Search should succeed with valid API key, got error: %s", result.Error)
// Should respect max_results from config (+1 for possible answer box)
assert.LessOrEqual(t, result.Total, ast.Search.Web.MaxResults+1)
t.Logf("Search without limit returned %d results (max: %d)", result.Total, ast.Search.Web.MaxResults)
// Execute search with explicit limit
req2 := &types.Request{
Query: "machine learning",
Type: types.SearchTypeWeb,
Source: types.SourceAuto,
Limit: 3, // Override config's max_results
}
result2, err := provider.Search(req2)
require.NoError(t, err)
require.NotNil(t, result2)
// API key must be valid - search should succeed
require.Empty(t, result2.Error, "Search should succeed with valid API key, got error: %s", result2.Error)
// Should respect request's limit (+1 for possible answer box)
assert.LessOrEqual(t, result2.Total, 4)
t.Logf("Search with limit=3 returned %d results", result2.Total)
}
// contains checks if s contains substr (uses containsSite from serpapi_test.go)
func contains(s, substr string) bool {
return containsSite(s, substr)
}