Files
ragflow/internal/harness/core/tool_schema_test.go
Yingfeng 706e0d2d06 Refactor harness framework (#16271)
### What problem does this PR solve?

- Tools management
- Pregel engine wrapper for better usage
- UT race
- Coding style

### Type of change

- [x] Refactoring
2026-06-23 20:18:04 +08:00

232 lines
6.4 KiB
Go

package core
import (
"context"
"testing"
)
// === Test struct for schema generation ===
type weatherArgs struct {
City string `json:"city" description:"The city name" required:"true"`
Country string `json:"country,omitempty" description:"Optional country name"`
Temp float64 `json:"temp" description:"Temperature" required:"true"`
Units string `json:"units" enum:"metric,imperial" description:"Temperature units"`
}
type emptyArgs struct{}
type nestedArgs struct {
Query string `json:"query" description:"Search query"`
Page int `json:"page" description:"Page number"`
Tags []string `json:"tags" description:"Filter tags"`
}
// ======================== GenerateToolInfo ========================
func TestGenerateToolInfo_Basic(t *testing.T) {
fn := func(ctx context.Context, args *weatherArgs) (string, error) {
return "sunny", nil
}
info, err := GenerateToolInfo[weatherArgs]("get_weather", "Get current weather", fn)
if err != nil {
t.Fatalf("GenerateToolInfo: %v", err)
}
if info.Name != "get_weather" {
t.Errorf("expected 'get_weather', got %s", info.Name)
}
if info.Description != "Get current weather" {
t.Errorf("expected 'Get current weather', got %s", info.Description)
}
if info.InputSchema == nil {
t.Fatal("expected non-nil InputSchema")
}
props, ok := info.InputSchema.(map[string]interface{})["properties"].(map[string]interface{})
if !ok {
t.Fatal("expected properties map")
}
// Verify each field exists with correct type
cityProp, ok := props["city"].(map[string]interface{})
if !ok {
t.Fatal("expected city property schema")
}
if cityProp["type"] != "string" {
t.Errorf("expected city type 'string', got %v", cityProp["type"])
}
if cityProp["description"] != "The city name" {
t.Errorf("expected city description 'The city name', got %v", cityProp["description"])
}
tempProp, ok := props["temp"].(map[string]interface{})
if !ok {
t.Fatal("expected temp property schema")
}
if tempProp["type"] != "number" {
t.Errorf("expected temp type 'number', got %v", tempProp["type"])
}
unitsProp, ok := props["units"].(map[string]interface{})
if !ok {
t.Fatal("expected units property schema")
}
if unitsProp["type"] != "string" {
t.Errorf("expected units type 'string', got %v", unitsProp["type"])
}
}
func TestGenerateToolInfo_EmptyStruct(t *testing.T) {
fn := func(ctx context.Context, args *emptyArgs) (string, error) {
return "no args", nil
}
info, err := GenerateToolInfo[emptyArgs]("noop", "Does nothing", fn)
if err != nil {
t.Fatalf("GenerateToolInfo: %v", err)
}
if info.Name != "noop" {
t.Errorf("expected 'noop', got %s", info.Name)
}
props := info.InputSchema.(map[string]interface{})["properties"].(map[string]interface{})
if len(props) != 0 {
t.Errorf("expected 0 properties, got %d", len(props))
}
}
func TestGenerateToolInfo_NestedTypes(t *testing.T) {
fn := func(ctx context.Context, args *nestedArgs) (string, error) {
return "nested", nil
}
info, err := GenerateToolInfo[nestedArgs]("search", "Search", fn)
if err != nil {
t.Fatalf("GenerateToolInfo: %v", err)
}
props := info.InputSchema.(map[string]interface{})["properties"].(map[string]interface{})
pageProp := props["page"].(map[string]interface{})
if pageProp["type"] != "integer" {
t.Errorf("expected page type 'integer', got %v", pageProp["type"])
}
tagsProp := props["tags"].(map[string]interface{})
if tagsProp["type"] != "array" {
t.Errorf("expected tags type 'array', got %v", tagsProp["type"])
}
}
func TestGenerateToolInfo_PointerStruct(t *testing.T) {
fn := func(ctx context.Context, args *weatherArgs) (string, error) { return "ok", nil }
info, err := GenerateToolInfo[*weatherArgs]("ptr_test", "test", fn)
if err != nil {
t.Fatalf("GenerateToolInfo pointer: %v", err)
}
if info.Name != "ptr_test" {
t.Errorf("expected 'ptr_test', got %s", info.Name)
}
}
// ======================== ReflectTool ========================
func TestReflectTool_Basic(t *testing.T) {
tool, err := ReflectTool("greet", "Greet someone",
func(ctx context.Context, args *weatherArgs) (string, error) {
return "Hello " + args.City, nil
})
if err != nil {
t.Fatalf("ReflectTool: %v", err)
}
if tool.Name() != "greet" {
t.Errorf("expected 'greet', got %s", tool.Name())
}
if tool.Description() != "Greet someone" {
t.Errorf("expected 'Greet someone', got %s", tool.Description())
}
result, err := tool.Invoke(context.Background(), `{"city":"London"}`)
if err != nil {
t.Fatalf("Invoke: %v", err)
}
if result != "Hello London" {
t.Errorf("expected 'Hello London', got %s", result)
}
}
func TestReflectTool_Stream(t *testing.T) {
tool, err := ReflectTool("echo", "Echo",
func(ctx context.Context, args *weatherArgs) (string, error) {
return "echo: " + args.City, nil
})
if err != nil {
t.Fatalf("ReflectTool: %v", err)
}
stream, err := tool.Stream(context.Background(), `{"city":"test"}`)
if err != nil {
t.Fatalf("Stream: %v", err)
}
if stream == nil {
t.Fatal("expected non-nil stream")
}
}
func TestReflectTool_ToolInfo(t *testing.T) {
tool, err := ReflectTool("info_test", "Info test",
func(ctx context.Context, args *weatherArgs) (string, error) {
return "info", nil
})
if err != nil {
t.Fatalf("ReflectTool: %v", err)
}
info := tool.ToolInfo()
if info.Name != "info_test" {
t.Errorf("expected 'info_test', got %s", info.Name)
}
}
func TestReflectTool_InvalidJSON(t *testing.T) {
tool, err := ReflectTool("bad_json", "Bad JSON",
func(ctx context.Context, args *weatherArgs) (string, error) {
return "ok", nil
})
if err != nil {
t.Fatalf("ReflectTool: %v", err)
}
_, err = tool.Invoke(context.Background(), `not valid json`)
if err == nil {
t.Error("expected error for invalid JSON")
}
}
func TestMustReflectTool(t *testing.T) {
tool := MustReflectTool("must", "Must tool",
func(ctx context.Context, args *weatherArgs) (string, error) {
return "must ok", nil
})
if tool.Name() != "must" {
t.Errorf("expected 'must', got %s", tool.Name())
}
}
func TestReflectTool_RegistryIntegration(t *testing.T) {
r := NewToolRegistry()
tool := MustReflectTool("registry_test", "Registry test",
func(ctx context.Context, args *weatherArgs) (string, error) {
return "registry ok", nil
})
r.Register(tool, WithCategory("test"))
found, ok := r.Lookup("registry_test")
if !ok {
t.Fatal("expected to find tool in registry")
}
if found.Name() != "registry_test" {
t.Errorf("expected 'registry_test', got %s", found.Name())
}
}