Add support for multiple providers

This commit is contained in:
dwrz
2026-06-19 14:31:41 +00:00
parent c71300b1bf
commit f5e72b1c7a
12 changed files with 948 additions and 869 deletions

View File

@@ -1,229 +1,135 @@
package tool
import (
"context"
"slices"
"testing"
"time"
"code.chimeric.al/chimerical/odidere/internal/config"
)
func TestToolValidate(t *testing.T) {
func TestTool_OpenAI(t *testing.T) {
tool := &Tool{
Name: "test_tool",
Description: "test description",
Parameters: map[string]any{"type": "object"},
}
openaiTool := tool.OpenAI()
if openaiTool.Function.Name != "test_tool" {
t.Errorf("expected name test_tool, got %s", openaiTool.Function.Name)
}
}
func TestTool_ParseArguments(t *testing.T) {
tool := &Tool{
Arguments: []string{"--name {{.name}}", "{{if .flag}}--enabled{{end}}"},
}
tests := []struct {
name string
args string
expected []string
}{
{
name: "basic",
args: `{"name": "test"}`,
expected: []string{"--name test"},
},
{
name: "with flag",
args: `{"name": "test", "flag": true}`,
expected: []string{"--name test", "--enabled"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tool.ParseArguments(tt.args)
if err != nil {
t.Fatalf("ParseArguments failed: %v", err)
}
if len(got) != len(tt.expected) {
t.Errorf("expected len %d, got %d", len(tt.expected), len(got))
}
for i := range got {
if got[i] != tt.expected[i] {
t.Errorf("expected %q, got %q", tt.expected[i], got[i])
}
}
})
}
}
func TestNewTool(t *testing.T) {
tests := []struct {
name string
tool Tool
cfg config.ToolConfig
wantErr bool
}{
{
name: "valid tool",
tool: Tool{
Name: "echo",
Description: "echoes input",
Command: "echo",
Arguments: []string{"{{.message}}"},
},
wantErr: false,
},
{
name: "valid tool with timeout",
tool: Tool{
Name: "slow",
Description: "slow command",
Command: "sleep",
Arguments: []string{"1"},
Timeout: "5s",
},
wantErr: false,
},
{
name: "missing name",
tool: Tool{
Description: "test",
Command: "echo",
},
wantErr: true,
},
{
name: "missing description",
tool: Tool{
Name: "test",
Command: "echo",
},
wantErr: true,
},
{
name: "missing command",
tool: Tool{
name: "valid",
cfg: config.ToolConfig{
Name: "test",
Description: "test",
Description: "desc",
Command: "ls",
Timeout: "10s",
},
wantErr: true,
wantErr: false,
},
{
name: "invalid timeout",
tool: Tool{
cfg: config.ToolConfig{
Name: "test",
Description: "test",
Command: "echo",
Description: "desc",
Command: "ls",
Timeout: "invalid",
},
wantErr: true,
},
{
name: "invalid template",
tool: Tool{
Name: "test",
Description: "test",
Command: "echo",
Arguments: []string{"{{.unclosed"},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.tool.Validate()
tool, err := NewTool(tt.cfg)
if (err != nil) != tt.wantErr {
t.Errorf(
"Validate() error = %v, wantErr %v",
err, tt.wantErr,
)
t.Errorf("NewTool() error = %v, wantErr %v", err, tt.wantErr)
}
if err == nil {
if tool.Name != tt.cfg.Name {
t.Errorf("expected name %s, got %s", tt.cfg.Name, tool.Name)
}
if tt.cfg.Timeout == "10s" && tool.timeout != 10*time.Second {
t.Errorf("expected timeout 10s, got %v", tool.timeout)
}
}
})
}
}
func TestToolParseArguments(t *testing.T) {
func TestRegistry_NewRegistry(t *testing.T) {
tests := []struct {
name string
tool Tool
args string
want []string
tools []config.ToolConfig
wantErr bool
}{
{
name: "simple substitution",
tool: Tool{
Arguments: []string{"{{.message}}"},
},
args: `{"message": "hello"}`,
want: []string{"hello"},
},
{
name: "multiple arguments",
tool: Tool{
Arguments: []string{"-n", "{{.count}}", "{{.file}}"},
},
args: `{"count": "10", "file": "test.txt"}`,
want: []string{"-n", "10", "test.txt"},
},
{
name: "conditional with if",
tool: Tool{
Arguments: []string{
"{{.required}}", "{{if .optional}}{{.optional}}{{end}}",
},
},
args: `{"required": "value"}`,
want: []string{"value"},
},
{
name: "json function",
tool: Tool{
Arguments: []string{`{{json .data}}`},
},
args: `{"data": {"key": "value"}}`,
want: []string{`{"key":"value"}`},
},
{
name: "empty JSON object",
tool: Tool{
Arguments: []string{"fixed"},
},
args: "{}",
want: []string{"fixed"},
},
{
name: "empty string args",
tool: Tool{
Arguments: []string{"fixed"},
},
args: "",
want: []string{"fixed"},
},
{
name: "invalid JSON",
tool: Tool{
Arguments: []string{"{{.x}}"},
},
args: "not json",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.tool.ParseArguments(tt.args)
if (err != nil) != tt.wantErr {
t.Errorf(
"ParseArguments() error = %v, wantErr %v",
err, tt.wantErr,
)
return
}
if !tt.wantErr && !slices.Equal(got, tt.want) {
t.Errorf(
"ParseArguments() = %v, want %v",
got, tt.want,
)
}
})
}
}
func TestNewRegistry(t *testing.T) {
tests := []struct {
name string
tools []Tool
wantErr bool
}{
{
name: "valid registry",
tools: []Tool{
{Name: "a", Description: "a", Command: "echo"},
{Name: "b", Description: "b", Command: "cat"},
name: "valid",
tools: []config.ToolConfig{
{Name: "t1", Description: "d1", Command: "c1"},
{Name: "t2", Description: "d2", Command: "c2"},
},
wantErr: false,
},
{
name: "empty registry",
tools: []Tool{},
wantErr: false,
},
{
name: "duplicate names",
tools: []Tool{
{
Name: "dup",
Description: "first",
Command: "echo",
},
{
Name: "dup",
Description: "second",
Command: "cat",
},
name: "duplicate",
tools: []config.ToolConfig{
{Name: "t1", Description: "d1", Command: "c1"},
{Name: "t1", Description: "d2", Command: "c2"},
},
wantErr: true,
},
{
name: "invalid tool",
tools: []Tool{
{
Name: "",
Description: "missing name",
Command: "echo",
},
tools: []config.ToolConfig{
{Name: "t1", Description: "d1", Command: "c1", Timeout: "invalid"},
},
wantErr: true,
},
@@ -233,177 +139,8 @@ func TestNewRegistry(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
_, err := NewRegistry(tt.tools)
if (err != nil) != tt.wantErr {
t.Errorf(
"NewRegistry() error = %v, wantErr %v",
err, tt.wantErr,
)
t.Errorf("NewRegistry() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestRegistryGet(t *testing.T) {
r, err := NewRegistry([]Tool{
{Name: "echo", Description: "echo", Command: "echo"},
})
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
tool, ok := r.Get("echo")
if !ok {
t.Error("Get(echo) returned false, want true")
}
if tool.Name != "echo" {
t.Errorf("Get(echo).Name = %q, want %q", tool.Name, "echo")
}
_, ok = r.Get("nonexistent")
if ok {
t.Error("Get(nonexistent) returned true, want false")
}
}
func TestRegistryList(t *testing.T) {
r, err := NewRegistry([]Tool{
{Name: "a", Description: "a", Command: "echo"},
{Name: "b", Description: "b", Command: "cat"},
{Name: "c", Description: "c", Command: "ls"},
})
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
names := r.List()
if len(names) != 3 {
t.Errorf("List() returned %d names, want 3", len(names))
}
slices.Sort(names)
want := []string{"a", "b", "c"}
if !slices.Equal(names, want) {
t.Errorf("List() = %v, want %v", names, want)
}
}
func TestRegistryExecute(t *testing.T) {
r, err := NewRegistry([]Tool{
{
Name: "echo",
Description: "echo message",
Command: "echo",
Arguments: []string{"{{.message}}"},
},
{
Name: "cat",
Description: "cat stdin",
Command: "cat",
Arguments: []string{},
},
})
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
ctx := context.Background()
// Successful execution.
out, err := r.Execute(ctx, "echo", `{"message": "hello world"}`)
if err != nil {
t.Errorf("Execute(echo) error = %v", err)
}
if out != "hello world\n" {
t.Errorf("Execute(echo) = %q, want %q", out, "hello world\n")
}
// Unknown tool.
_, err = r.Execute(ctx, "unknown", "{}")
if err == nil {
t.Error("Execute(unknown) expected error, got nil")
}
// Invalid JSON arguments.
_, err = r.Execute(ctx, "echo", "not json")
if err == nil {
t.Error("Execute with invalid JSON expected error, got nil")
}
}
func TestRegistryExecuteTimeout(t *testing.T) {
r, err := NewRegistry([]Tool{
{
Name: "slow",
Description: "slow command",
Command: "sleep",
Arguments: []string{"10"},
Timeout: "50ms",
},
})
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
ctx := context.Background()
start := time.Now()
_, err = r.Execute(ctx, "slow", "{}")
elapsed := time.Since(start)
if err == nil {
t.Error("Execute(slow) expected timeout error, got nil")
}
if elapsed > time.Second {
t.Errorf(
"Execute(slow) took %v, expected ~50ms timeout",
elapsed,
)
}
}
func TestRegistryExecuteFailure(t *testing.T) {
r, err := NewRegistry([]Tool{
{
Name: "fail",
Description: "always fails",
Command: "false",
},
})
if err != nil {
t.Fatalf("NewRegistry() error = %v", err)
}
_, err = r.Execute(context.Background(), "fail", "{}")
if err == nil {
t.Error("Execute(fail) expected error, got nil")
}
}
func TestToolOpenAI(t *testing.T) {
tool := Tool{
Name: "test",
Description: "test tool",
Command: "echo",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"message": map[string]any{
"type": "string",
"description": "the message",
},
},
},
}
openaiTool := tool.OpenAI()
if openaiTool.Function.Name != "test" {
t.Errorf(
"OpenAI().Function.Name = %q, want %q",
openaiTool.Function.Name, "test",
)
}
if openaiTool.Function.Description != "test tool" {
t.Errorf(
"OpenAI().Function.Description = %q, want %q",
openaiTool.Function.Description, "test tool",
)
}
}