package core import ( "context" "testing" "ragflow/internal/harness/core/schema" ) func TestToolRegistry_RegisterAndLookup(t *testing.T) { r := NewToolRegistry() tool := newTestTool("get_weather", "Get weather") r.Register(tool) found, ok := r.Lookup("get_weather") if !ok { t.Fatal("expected to find 'get_weather'") } if found.Name() != "get_weather" { t.Errorf("expected 'get_weather', got %s", found.Name()) } } func TestToolRegistry_LookupNotFound(t *testing.T) { r := NewToolRegistry() _, ok := r.Lookup("nonexistent") if ok { t.Error("expected false for nonexistent tool") } } func TestToolRegistry_WithAlias(t *testing.T) { r := NewToolRegistry() tool := newTestTool("web_search_v2", "Search web") r.Register(tool, WithAlias("search")) // Lookup by alias found, ok := r.Lookup("search") if !ok { t.Fatal("expected to find via alias 'search'") } if found.Name() != "web_search_v2" { t.Errorf("expected 'web_search_v2', got %s", found.Name()) } // Lookup by canonical name still works found, ok = r.Lookup("web_search_v2") if !ok { t.Fatal("expected to find via canonical name") } } func TestToolRegistry_WithCategory(t *testing.T) { r := NewToolRegistry() r.Register(newTestTool("cat_tool1", ""), WithCategory("file", "read")) r.Register(newTestTool("cat_tool2", ""), WithCategory("file")) r.Register(newTestTool("other_tool", ""), WithCategory("network")) fileTools := r.LookupByCategory("file") if len(fileTools) != 2 { t.Errorf("expected 2 file tools, got %d", len(fileTools)) } readTools := r.LookupByCategory("read") if len(readTools) != 1 { t.Errorf("expected 1 read tool, got %d", len(readTools)) } networkTools := r.LookupByCategory("network") if len(networkTools) != 1 { t.Errorf("expected 1 network tool, got %d", len(networkTools)) } emptyTools := r.LookupByCategory("nonexistent") if len(emptyTools) != 0 { t.Errorf("expected 0 tools for nonexistent category, got %d", len(emptyTools)) } } func TestToolRegistry_AllTools(t *testing.T) { r := NewToolRegistry() r.Register(newTestTool("a", "")) r.Register(newTestTool("b", "")) all := r.AllTools() if len(all) != 2 { t.Errorf("expected 2 tools, got %d", len(all)) } } func TestToolRegistry_ToSlice(t *testing.T) { r := NewToolRegistry() r.Register(newTestTool("x", "")) r.Register(newTestTool("y", "")) slice := r.ToSlice() if len(slice) != 2 { t.Errorf("expected 2 tools, got %d", len(slice)) } } func TestToolRegistry_Filter(t *testing.T) { r := NewToolRegistry() r.Register(newTestTool("search_web", ""), WithCategory("web")) r.Register(newTestTool("search_file", ""), WithCategory("file")) r.Register(newTestTool("read_file", ""), WithCategory("file")) filtered := r.Filter(func(tool Tool) bool { return tool.Name() == "search_file" }) if len(filtered.AllTools()) != 1 { t.Errorf("expected 1 filtered tool, got %d", len(filtered.AllTools())) } } func TestToolRegistry_Merge(t *testing.T) { r1 := NewToolRegistry() r1.Register(newTestTool("a", ""), WithAlias("alias_a")) r2 := NewToolRegistry() r2.Register(newTestTool("b", ""), WithAlias("alias_b")) r1.Merge(r2) if _, ok := r1.Lookup("a"); !ok { t.Error("expected 'a' after merge") } if _, ok := r1.Lookup("b"); !ok { t.Error("expected 'b' after merge") } if _, ok := r1.Lookup("alias_b"); !ok { t.Error("expected 'alias_b' after merge") } } func TestToolRegistry_Unregister(t *testing.T) { r := NewToolRegistry() r.Register(newTestTool("temp", ""), WithAlias("t"), WithCategory("test")) r.Unregister("temp") if _, ok := r.Lookup("temp"); ok { t.Error("expected 'temp' to be removed") } if _, ok := r.Lookup("t"); ok { t.Error("expected alias 't' to be removed") } if len(r.LookupByCategory("test")) != 0 { t.Error("expected category 'test' to be empty") } } func TestToolRegistry_MustLookup(t *testing.T) { r := NewToolRegistry() r.Register(newTestTool("safe", "")) defer func() { if r := recover(); r == nil { t.Error("expected panic for missing tool") } }() r.MustLookup("nonexistent") } func TestToolRegistry_ConcurrentAccess(t *testing.T) { r := NewToolRegistry() r.Register(newTestTool("concurrent_tool", "")) done := make(chan struct{}, 2) go func() { for i := 0; i < 100; i++ { r.Lookup("concurrent_tool") } done <- struct{}{} }() go func() { for i := 0; i < 100; i++ { r.Register(newTestTool("other", "")) } done <- struct{}{} }() <-done <-done } func TestToolToSliceForReActConfig(t *testing.T) { r := NewToolRegistry() r.Register(newTestTool("t1", "")) r.Register(newTestTool("t2", "")) cfg := &ReActConfig[Message]{ Tools: r.ToSlice(), } if len(cfg.Tools) != 2 { t.Errorf("expected 2 tools in config, got %d", len(cfg.Tools)) } } func TestToolRegistry_LookupPanicsForTool(t *testing.T) { // Non-existent tool via MustLookup r := NewToolRegistry() r.Register(&testPanicTool{name: "good"}) // Should not panic _ = r.MustLookup("good") } type testPanicTool struct{ name string } func (t *testPanicTool) Name() string { return t.name } func (t *testPanicTool) Description() string { return "" } func (t *testPanicTool) Invoke(ctx context.Context, s string, opts ...ToolOption) (string, error) { return "", nil } func (t *testPanicTool) Stream(ctx context.Context, s string, opts ...ToolOption) (*schema.StreamReader[string], error) { return schema.StreamReaderFromArray([]string{""}), nil }