Why: Agent-Native ships a Tavily integration (key-pool rotation, markdown output, fallback copy) that the backend never registered, so the catalog's WebSearch prompt had no carrier; wiring the tool gives it one and gives the agent live-web reach. Evidence: 4 new tool tests (registration, markdown formatting, empty query, failure fallback carried inside the error to match registry replay semantics); go test ./... 16 packages ok; live boot check - no keys means not registered, two keys log registration with both tools in /api/tools and 46 library modules in /api/prompts. Impact: tools.tavily.apiKeys/endpoint settings (restart effect; empty pool means the tool is absent, no silent degradation); tool.tavily_search and tool.web_search prompt modules inject citation discipline (cite sources, keep inference separate, fall back to workspace materials on failure); library floor raised to 46. Refs: feature/tavily-search-tool task card; guide disposition table in Documents.
86 lines
2.5 KiB
Go
86 lines
2.5 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
agenttools "agent-native/runtime/tools"
|
|
core "agent-system-core"
|
|
)
|
|
|
|
type fakeProvider struct {
|
|
resp agenttools.SearchResponse
|
|
err error
|
|
}
|
|
|
|
func (f *fakeProvider) Search(ctx context.Context, req agenttools.SearchRequest) (agenttools.SearchResponse, error) {
|
|
return f.resp, f.err
|
|
}
|
|
|
|
func newSearchRegistry(t *testing.T, p agenttools.SearchProvider) *Registry {
|
|
t.Helper()
|
|
reg := NewRegistry()
|
|
if err := reg.RegisterBuiltin(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := RegisterSearchTools(reg, p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return reg
|
|
}
|
|
|
|
func TestSearchToolsRegistered(t *testing.T) {
|
|
reg := newSearchRegistry(t, &fakeProvider{})
|
|
for _, name := range []string{"tavily_search", "web_search"} {
|
|
if _, _, ok := reg.Get(name); !ok {
|
|
t.Errorf("tool %s not registered", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func execSearchTool(t *testing.T, reg *Registry, name, args string) core.ToolCallResult {
|
|
t.Helper()
|
|
return reg.Execute(nil, core.ToolCallRequest{CallID: "c1", Name: name, Arguments: json.RawMessage(args)})
|
|
}
|
|
|
|
func TestSearchExecuteFormatsResults(t *testing.T) {
|
|
reg := newSearchRegistry(t, &fakeProvider{resp: agenttools.SearchResponse{
|
|
Answer: "概要答案",
|
|
Results: []agenttools.SearchResult{
|
|
{Title: "结果一", Content: "内容甲", Score: 0.9},
|
|
{Title: "结果二", Content: "内容乙", Score: 0.8},
|
|
},
|
|
}})
|
|
res := execSearchTool(t, reg, "tavily_search", `{"query":"超声 法规","search_depth":"basic","max_results":5}`)
|
|
if res.Status != "ok" {
|
|
t.Fatalf("status=%s output=%q", res.Status, res.Output)
|
|
}
|
|
for _, want := range []string{"Tavily 实时网络搜索结果", "概要答案", "结果一", "内容乙"} {
|
|
if !strings.Contains(res.Output, want) {
|
|
t.Errorf("output missing %q:\n%s", want, res.Output)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSearchEmptyQuery(t *testing.T) {
|
|
reg := newSearchRegistry(t, &fakeProvider{})
|
|
res := execSearchTool(t, reg, "web_search", `{"query":" "}`)
|
|
if res.Status == "ok" || !strings.Contains(res.Output, "query cannot be empty") {
|
|
t.Errorf("empty query: status=%s out=%q", res.Status, res.Output)
|
|
}
|
|
}
|
|
|
|
func TestSearchFailureFallback(t *testing.T) {
|
|
reg := newSearchRegistry(t, &fakeProvider{err: errors.New("all keys failed")})
|
|
res := execSearchTool(t, reg, "tavily_search", `{"query":"q"}`)
|
|
if res.Status == "ok" {
|
|
t.Error("provider failure should surface as tool error status")
|
|
}
|
|
if !strings.Contains(res.Output, "[Tavily Search Failed]") || !strings.Contains(res.Output, "fallback") {
|
|
t.Errorf("fallback copy missing: %q", res.Output)
|
|
}
|
|
}
|