Files
ragflow/internal/agent/tool/registry_test.go
Hz_ a7da78d0d7 refactor(go-agent): unify tool-backed canvas components (#16912)
## Summary

This PR consolidates Eino-backed Agent tools behind the shared
ToolBackedComponent implementation and aligns their Canvas
configuration,
runtime inputs, output conversion, validation, and registration.

## What changed

- Migrated these Canvas components to the unified tool-backed path:
    - Tavily Search and Extract
    - Execute SQL
    - Google
    - Yahoo Finance
    - Email
    - DuckDuckGo
    - Wikipedia
    - Google Scholar
    - ArXiv
    - PubMed
    - BGPT
    - GitHub
    - WenCai
    - SearXNG
    - Keenable Search

- Removed superseded component wrappers and their duplicate tests.
- Added dedicated registry builders for node-level configuration and
validation.
- Kept model-emitted runtime inputs separate from Canvas node
configuration.
- Moved Email defaults, template resolution, recipient parsing, SMTP
execution, and output conversion into the owning tool.
- Added complete ToolComponent specifications for Canvas inputs,
outputs, and input forms.
- Preserved raw upstream fields where downstream workflows may depend on
them.
- Added workflow registration coverage for all migrated component names.
- Kept HTTP Request, Docs Generator, and Browser as standalone
components because they are not Eino-backed tools.

## Testing

Passed:

bash build.sh --test ./internal/agent/tool/...
bash build.sh --test ./internal/agent/component/...
bash build.sh --test ./internal/agent/runtime/...

<img width="2057" height="1111" alt="image"
src="https://github.com/user-attachments/assets/c728d7a3-9d15-4c5c-b0eb-6b77ad0e41ac"
/>
<img width="2057" height="1111" alt="image"
src="https://github.com/user-attachments/assets/ef13119a-ef92-4b43-9061-eee9511bf492"
/>
<img width="2057" height="1111" alt="image"
src="https://github.com/user-attachments/assets/f50f2c30-eab0-463c-b0b3-0a02523219f1"
/>
2026-07-15 21:42:08 +08:00

211 lines
6.3 KiB
Go

package tool
import (
"context"
"strings"
"testing"
)
func TestBuildAll_KnownTools(t *testing.T) {
tools, err := BuildAll([]string{"retrieval", "wikipedia"}, nil)
if err != nil {
t.Fatalf("BuildAll: %v", err)
}
if len(tools) != 2 {
t.Fatalf("len(tools) = %d, want 2", len(tools))
}
info0, err := tools[0].Info(context.Background())
if err != nil {
t.Fatalf("tools[0].Info: %v", err)
}
if info0.Name != "search_my_dateset" {
t.Errorf("tools[0].Info().Name = %q, want search_my_dateset", info0.Name)
}
info1, err := tools[1].Info(context.Background())
if err != nil {
t.Fatalf("tools[1].Info: %v", err)
}
if info1.Name != "wikipedia_search" {
t.Errorf("tools[1].Info().Name = %q, want wikipedia_search", info1.Name)
}
}
func TestBuildAll_UnknownTool(t *testing.T) {
_, err := BuildAll([]string{"does_not_exist"}, nil)
if err == nil {
t.Fatal("expected error for unknown tool")
}
if !strings.Contains(err.Error(), `unsupported tool "does_not_exist"`) {
t.Fatalf("err = %q, want unsupported tool message", err.Error())
}
}
func TestBuildAll_AllRegisteredTools(t *testing.T) {
// Every key in registry.
names := []string{
"akshare", "arxiv", "bgpt", "code_exec", "crawler", "deepl",
"duckduckgo", "email", "exesql", "execute_sql", "github", "google",
"google_scholar", "google_scholar_search", "jin10", "keenable", "pubmed", "qweather",
"retrieval", "search_my_dataset", "search_my_dateset", "searxng",
"tavily", "tavily_extract", "tushare", "web_crawler", "wencai", "wikipedia", "wikipedia_search",
"yahoo_finance",
}
params := map[string]map[string]any{
"execute_sql": {
"db_type": "mysql",
"host": "127.0.0.1",
"port": 3306,
"database": "demo",
"username": "u",
"password": "p",
"max_records": 10,
},
"exesql": {
"db_type": "mysql",
"host": "127.0.0.1",
"port": 3306,
"database": "demo",
"username": "u",
"password": "p",
"max_records": 10,
},
"keenable": {
"api_key": "key-test",
},
}
tools, err := BuildAll(names, params)
if err != nil {
t.Fatalf("BuildAll(all registered): %v", err)
}
if len(tools) != len(names) {
t.Fatalf("len(tools) = %d, want %d", len(tools), len(names))
}
}
func TestBuildAll_ExeSQLRequiresNodeParams(t *testing.T) {
_, err := BuildAll([]string{"execute_sql"}, nil)
if err == nil {
t.Fatal("expected execute_sql config error")
}
if !strings.Contains(err.Error(), "execute_sql requires node-level params") {
t.Fatalf("err = %q, want execute_sql config error", err.Error())
}
}
// TestToolRegistry_SchemasAreComplete sweeps every name the public
// registry advertises (including the execute_sql/exesql and
// retrieval/search_my_dateset alias pairs), builds the tool, and
// asserts that its Info() returns a complete schema — non-empty
// Name and Desc, non-nil ParamsOneOf, and a consistent canonical
// name across alias entries. Catches drift like "tool renamed but
// registry not updated", "param added but schema not updated",
// "tool registered with empty description", and "alias points to
// the wrong canonical name".
func TestToolRegistry_SchemasAreComplete(t *testing.T) {
t.Parallel()
// Every entry the registry advertises.
names := []string{
"akshare", "arxiv", "bgpt", "code_exec", "crawler", "deepl",
"duckduckgo", "email", "execute_sql", "exesql", "github", "google",
"google_scholar", "google_scholar_search", "jin10", "keenable", "pubmed", "qweather",
"retrieval", "search_my_dataset", "search_my_dateset", "searxng",
"tavily", "tavily_extract", "tushare", "web_crawler", "wencai", "wikipedia", "wikipedia_search",
"yahoo_finance",
}
params := map[string]map[string]any{
"execute_sql": {
"db_type": "mysql",
"host": "127.0.0.1",
"port": 3306,
"database": "demo",
"username": "u",
"password": "p",
"max_records": 10,
},
"exesql": {
"db_type": "mysql",
"host": "127.0.0.1",
"port": 3306,
"database": "demo",
"username": "u",
"password": "p",
"max_records": 10,
},
"keenable": {
"api_key": "key-xyz",
},
}
tools, err := BuildAll(names, params)
if err != nil {
t.Fatalf("BuildAll(%d names): %v", len(names), err)
}
if len(tools) != len(names) {
t.Fatalf("BuildAll returned %d tools for %d names", len(tools), len(names))
}
// Schema-level checks per entry.
for i, name := range names {
info, err := tools[i].Info(context.Background())
if err != nil {
t.Errorf("tools[%d] (registry name %q).Info: %v", i, name, err)
continue
}
if info.Name == "" {
t.Errorf("tools[%d] (registry name %q).Info().Name is empty", i, name)
}
if info.Desc == "" {
t.Errorf("tools[%d] (registry name %q).Info().Desc is empty", i, name)
}
if info.ParamsOneOf == nil {
t.Errorf("tools[%d] (registry name %q).Info().ParamsOneOf is nil", i, name)
}
}
// Alias consistency: execute_sql and exesql must surface the
// same canonical Info().Name; same for retrieval/search_my_dataset/
// search_my_dateset and crawler/web_crawler. A bug here would mean
// an alias was accidentally pointed at a different tool.
canonicalByAlias := map[string]string{
"execute_sql": "execute_sql",
"exesql": "execute_sql",
"google_scholar": "google_scholar_search",
"google_scholar_search": "google_scholar_search",
"retrieval": "search_my_dateset",
"search_my_dataset": "search_my_dateset",
"search_my_dateset": "search_my_dateset",
"crawler": "web_crawler",
"web_crawler": "web_crawler",
"wikipedia": "wikipedia_search",
"wikipedia_search": "wikipedia_search",
}
for _, name := range names {
canonical, ok := canonicalByAlias[name]
if !ok {
continue
}
idx := indexOf(names, name)
info, err := tools[idx].Info(context.Background())
if err != nil {
continue
}
if info.Name != canonical {
t.Errorf("registry name %q: Info().Name = %q, want %q (alias must surface canonical name)",
name, info.Name, canonical)
}
}
}
// indexOf returns the index of s in xs, or -1 if not present.
// Tiny helper to keep the alias loop above free of a slice lookup
// closure; the test's names slice is <30 items so linear scan is
// fine.
func indexOf(xs []string, s string) int {
for i, x := range xs {
if x == s {
return i
}
}
return -1
}