- 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.
327 lines
9.8 KiB
Go
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)
|
|
}
|