yao/agent/context/jsapi_search_test.go
Max 7768ca73b3 Enhance Search API with New V8 Binding Methods
- Introduced a new search object in the context to expose search methods (Web, KB, DB, All, Any, Race) for JavaScript integration.
- Implemented individual search methods with argument validation and error handling, improving the robustness of the API.
- Updated the JSAPI implementation to utilize the new search object, ensuring seamless interaction with the search functionalities.
- Enhanced documentation in DESIGN.md to reflect the new V8 binding methods and their usage, providing clear guidance for developers.
2025-12-13 16:11:30 +08:00

342 lines
11 KiB
Go

package context_test
import (
stdContext "context"
"encoding/json"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/yao/agent/assistant"
"github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/agent/search/types"
"github.com/yaoapp/yao/agent/testutils"
oauthTypes "github.com/yaoapp/yao/openapi/oauth/types"
)
// Note: SearchAPIFactory is set by assistant.init() with proper config getter
// We import assistant package to ensure init() runs before tests
// newSearchTestContext creates a Context for search JSAPI testing
func newSearchTestContext(chatID, assistantID string) *context.Context {
authorized := &oauthTypes.AuthorizedInfo{
Subject: "test-user",
ClientID: "test-client-id",
Scope: "openid profile email",
SessionID: "test-session-id",
UserID: "test-user-123",
}
ctx := context.New(stdContext.Background(), authorized, chatID)
ctx.AssistantID = assistantID
ctx.Locale = "en-us"
ctx.Referer = context.RefererAPI
ctx.Accept = context.AcceptWebCUI
ctx.Metadata = make(map[string]interface{})
return ctx
}
// getResponseContent extracts the content from the first assistant message
func getResponseContent(res *context.HookCreateResponse) string {
if res == nil || len(res.Messages) == 0 {
return ""
}
for _, msg := range res.Messages {
if msg.Role == "assistant" {
if content, ok := msg.Content.(string); ok {
return content
}
}
}
return ""
}
// TestSearchJSAPI_Web tests ctx.search.Web() via Create Hook
func TestSearchJSAPI_Web(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
// Load the search-jsapi test assistant
agent, err := assistant.Get("tests.search-jsapi")
require.NoError(t, err, "Failed to get tests.search-jsapi assistant")
require.NotNil(t, agent.HookScript, "The tests.search-jsapi assistant has no script")
ctx := newSearchTestContext("chat-search-web", "tests.search-jsapi")
// Call Create hook with test:web command
res, _, err := agent.HookScript.Create(ctx, []context.Message{{Role: "user", Content: "test:web Yao App Engine"}})
require.NoError(t, err, "Create hook failed")
require.NotNil(t, res, "Expected non-nil response")
// Get response content from messages
content := getResponseContent(res)
require.NotEmpty(t, content, "Expected response content")
// Parse the JSON response
var result types.Result
err = json.Unmarshal([]byte(content), &result)
require.NoError(t, err, "Response should be valid JSON: %s", content)
// Verify result
assert.Equal(t, types.SearchTypeWeb, result.Type, "type should be web")
assert.Equal(t, "Yao App Engine", result.Query, "query should match")
assert.Empty(t, result.Error, "should not have error: %s", result.Error)
assert.Greater(t, len(result.Items), 0, "should have items")
t.Logf("Web search returned %d items", len(result.Items))
for i, item := range result.Items {
if i < 3 {
t.Logf(" [%s] %s - %s", item.CitationID, item.Title, item.URL)
}
}
}
// TestSearchJSAPI_WebWithSites tests ctx.search.Web() with site restriction
func TestSearchJSAPI_WebWithSites(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
agent, err := assistant.Get("tests.search-jsapi")
require.NoError(t, err)
require.NotNil(t, agent.HookScript)
ctx := newSearchTestContext("chat-search-web-sites", "tests.search-jsapi")
res, _, err := agent.HookScript.Create(ctx, []context.Message{{Role: "user", Content: "test:web_sites Yao App Engine"}})
require.NoError(t, err)
require.NotNil(t, res)
content := getResponseContent(res)
require.NotEmpty(t, content, "Expected response content")
var result types.Result
err = json.Unmarshal([]byte(content), &result)
require.NoError(t, err, "Response should be valid JSON: %s", content)
assert.Equal(t, types.SearchTypeWeb, result.Type)
assert.Empty(t, result.Error, "should not have error: %s", result.Error)
assert.Greater(t, len(result.Items), 0, "should have items")
// Verify all results are from allowed sites
allowedSites := []string{"github.com", "yaoapps.com"}
for _, item := range result.Items {
isAllowed := false
for _, site := range allowedSites {
if strings.Contains(item.URL, site) {
isAllowed = true
break
}
}
assert.True(t, isAllowed, "URL %s should be from allowed sites", item.URL)
}
t.Logf("Site-restricted search returned %d items", len(result.Items))
}
// TestSearchJSAPI_KB tests ctx.search.KB() via Create Hook (skeleton)
func TestSearchJSAPI_KB(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
agent, err := assistant.Get("tests.search-jsapi")
require.NoError(t, err)
require.NotNil(t, agent.HookScript)
ctx := newSearchTestContext("chat-search-kb", "tests.search-jsapi")
res, _, err := agent.HookScript.Create(ctx, []context.Message{{Role: "user", Content: "test:kb test query"}})
require.NoError(t, err)
require.NotNil(t, res)
content := getResponseContent(res)
require.NotEmpty(t, content, "Expected response content")
var result types.Result
err = json.Unmarshal([]byte(content), &result)
require.NoError(t, err, "Response should be valid JSON: %s", content)
assert.Equal(t, types.SearchTypeKB, result.Type, "type should be kb")
assert.Equal(t, "test query", result.Query, "query should match")
assert.Equal(t, types.SourceHook, result.Source, "source should be hook")
}
// TestSearchJSAPI_DB tests ctx.search.DB() via Create Hook (skeleton)
func TestSearchJSAPI_DB(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
agent, err := assistant.Get("tests.search-jsapi")
require.NoError(t, err)
require.NotNil(t, agent.HookScript)
ctx := newSearchTestContext("chat-search-db", "tests.search-jsapi")
res, _, err := agent.HookScript.Create(ctx, []context.Message{{Role: "user", Content: "test:db test query"}})
require.NoError(t, err)
require.NotNil(t, res)
content := getResponseContent(res)
require.NotEmpty(t, content, "Expected response content")
var result types.Result
err = json.Unmarshal([]byte(content), &result)
require.NoError(t, err, "Response should be valid JSON: %s", content)
assert.Equal(t, types.SearchTypeDB, result.Type, "type should be db")
assert.Equal(t, "test query", result.Query, "query should match")
assert.Equal(t, types.SourceHook, result.Source, "source should be hook")
}
// TestSearchJSAPI_All tests ctx.search.All() via Create Hook
func TestSearchJSAPI_All(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
agent, err := assistant.Get("tests.search-jsapi")
require.NoError(t, err)
require.NotNil(t, agent.HookScript)
ctx := newSearchTestContext("chat-search-all", "tests.search-jsapi")
res, _, err := agent.HookScript.Create(ctx, []context.Message{{Role: "user", Content: "test:all"}})
require.NoError(t, err)
require.NotNil(t, res)
content := getResponseContent(res)
require.NotEmpty(t, content, "Expected response content")
// Parse as array of results
var results []*types.Result
err = json.Unmarshal([]byte(content), &results)
require.NoError(t, err, "Response should be valid JSON array: %s", content)
assert.Len(t, results, 2, "should have 2 results")
// Both should succeed
successCount := 0
totalItems := 0
for _, r := range results {
if r != nil && r.Error == "" {
successCount++
totalItems += len(r.Items)
}
}
assert.Equal(t, 2, successCount, "both searches should succeed")
assert.Greater(t, totalItems, 0, "should have items")
t.Logf("All search: %d results, %d total items", len(results), totalItems)
}
// TestSearchJSAPI_Any tests ctx.search.Any() via Create Hook
func TestSearchJSAPI_Any(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
agent, err := assistant.Get("tests.search-jsapi")
require.NoError(t, err)
require.NotNil(t, agent.HookScript)
ctx := newSearchTestContext("chat-search-any", "tests.search-jsapi")
res, _, err := agent.HookScript.Create(ctx, []context.Message{{Role: "user", Content: "test:any"}})
require.NoError(t, err)
require.NotNil(t, res)
content := getResponseContent(res)
require.NotEmpty(t, content, "Expected response content")
var results []*types.Result
err = json.Unmarshal([]byte(content), &results)
require.NoError(t, err, "Response should be valid JSON array: %s", content)
assert.Len(t, results, 2, "should have 2 result slots")
// At least one should have results
hasSuccess := false
for _, r := range results {
if r != nil && len(r.Items) > 0 && r.Error == "" {
hasSuccess = true
break
}
}
assert.True(t, hasSuccess, "at least one search should succeed")
t.Logf("Any search completed")
}
// TestSearchJSAPI_Race tests ctx.search.Race() via Create Hook
func TestSearchJSAPI_Race(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
agent, err := assistant.Get("tests.search-jsapi")
require.NoError(t, err)
require.NotNil(t, agent.HookScript)
ctx := newSearchTestContext("chat-search-race", "tests.search-jsapi")
res, _, err := agent.HookScript.Create(ctx, []context.Message{{Role: "user", Content: "test:race"}})
require.NoError(t, err)
require.NotNil(t, res)
content := getResponseContent(res)
require.NotEmpty(t, content, "Expected response content")
var results []*types.Result
err = json.Unmarshal([]byte(content), &results)
require.NoError(t, err, "Response should be valid JSON array: %s", content)
assert.Len(t, results, 2, "should have 2 result slots")
// At least one should have completed
hasResult := false
for _, r := range results {
if r != nil {
hasResult = true
break
}
}
assert.True(t, hasResult, "at least one search should complete")
t.Logf("Race search completed")
}
// TestSearchJSAPI_InvalidCommand tests invalid test command
func TestSearchJSAPI_InvalidCommand(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
agent, err := assistant.Get("tests.search-jsapi")
require.NoError(t, err)
require.NotNil(t, agent.HookScript)
ctx := newSearchTestContext("chat-search-invalid", "tests.search-jsapi")
res, _, err := agent.HookScript.Create(ctx, []context.Message{{Role: "user", Content: "invalid command"}})
require.NoError(t, err)
require.NotNil(t, res)
content := getResponseContent(res)
assert.Contains(t, content, "Invalid test command", "should return error message")
}
// TestSearchJSAPI_UnknownMethod tests unknown test method
func TestSearchJSAPI_UnknownMethod(t *testing.T) {
testutils.Prepare(t)
defer testutils.Clean(t)
agent, err := assistant.Get("tests.search-jsapi")
require.NoError(t, err)
require.NotNil(t, agent.HookScript)
ctx := newSearchTestContext("chat-search-unknown", "tests.search-jsapi")
res, _, err := agent.HookScript.Create(ctx, []context.Message{{Role: "user", Content: "test:unknown"}})
require.NoError(t, err)
require.NotNil(t, res)
content := getResponseContent(res)
assert.Contains(t, content, "Unknown test method", "should return error message")
}