package llm import ( "context" "encoding/json" "errors" "strings" "testing" ) func TestToolboxAddRejectsDuplicatesAndEmptyNames(t *testing.T) { b := NewToolbox("box") if err := b.Add(Tool{Name: "a"}); err != nil { t.Fatalf("Add: %v", err) } if err := b.Add(Tool{Name: "a"}); err == nil { t.Error("duplicate name should error") } if err := b.Add(Tool{}); err == nil { t.Error("empty name should error") } } func TestToolboxOrderPreserved(t *testing.T) { b := NewToolbox("box", Tool{Name: "z"}, Tool{Name: "a"}, Tool{Name: "m"}) var names []string for _, tool := range b.Tools() { names = append(names, tool.Name) } if got, want := strings.Join(names, ","), "z,a,m"; got != want { t.Errorf("order = %s, want %s", got, want) } } func TestExecuteUnknownTool(t *testing.T) { b := NewToolbox("box") res := b.Execute(context.Background(), ToolCall{ID: "1", Name: "missing"}) if !res.IsError || !strings.Contains(res.Content, "missing") { t.Errorf("result = %+v, want unknown-tool error", res) } } func TestExecuteHandlerOutcomes(t *testing.T) { echo := func(v any, err error) Tool { return Tool{Name: "t", Handler: func(context.Context, json.RawMessage) (any, error) { return v, err }} } tests := []struct { name string tool Tool wantContent string wantErr bool }{ {"string passthrough", echo("plain", nil), "plain", false}, {"struct json-encoded", echo(struct { N int `json:"n"` }{4}, nil), `{"n":4}`, false}, {"raw message passthrough", echo(json.RawMessage(`{"k":1}`), nil), `{"k":1}`, false}, {"nil becomes null", echo(nil, nil), "null", false}, {"handler error", echo(nil, errors.New("boom")), "boom", true}, {"unencodable value", echo(func() {}, nil), "unencodable", true}, {"no handler", Tool{Name: "t"}, "no handler", true}, } for _, tt := range tests { res := ExecuteTool(context.Background(), tt.tool, ToolCall{ID: "c1", Name: "t"}) if res.IsError != tt.wantErr { t.Errorf("%s: IsError = %v, want %v (%+v)", tt.name, res.IsError, tt.wantErr, res) } if !strings.Contains(res.Content, tt.wantContent) { t.Errorf("%s: content = %q, want it to contain %q", tt.name, res.Content, tt.wantContent) } if res.ID != "c1" { t.Errorf("%s: result ID = %q, want c1", tt.name, res.ID) } } } func TestExecuteRecoversPanic(t *testing.T) { tool := Tool{Name: "t", Handler: func(context.Context, json.RawMessage) (any, error) { panic("kaboom") }} res := ExecuteTool(context.Background(), tool, ToolCall{ID: "1", Name: "t"}) if !res.IsError || !strings.Contains(res.Content, "kaboom") { t.Errorf("result = %+v, want recovered panic error", res) } } func TestExecuteEmptyArgsBecomeEmptyObject(t *testing.T) { var got json.RawMessage tool := Tool{Name: "t", Handler: func(_ context.Context, args json.RawMessage) (any, error) { got = args return "ok", nil }} ExecuteTool(context.Background(), tool, ToolCall{ID: "1", Name: "t"}) if string(got) != "{}" { t.Errorf("args = %q, want {}", got) } }