Files
majordomo/llm/tool_test.go
T
steve dcd004289f feat: foundations — canonical types, Parse grammar, env DSNs, health, chains
Phase 1 of the majordomo build:
- llm/ canonical contract (messages, parts, tools, capabilities, streaming,
  Model/Provider, error classification)
- health/ clock-injected tracker (threshold bench, exponential capped
  cooldown, reset-on-success)
- root Registry + Parse (verbatim model ids, inline recursive alias
  expansion with cycle detection, chain dedup), LLM_* env-DSN providers
  (go-llm parity: lazy fallback + eager LoadEnv), health-aware chain
  executor behind the Model interface
- provider/fake scriptable test provider; hermetic test suite incl. the
  trailing-thinking chain and foreman:// env loading
- ADRs 0001-0008, CLAUDE.md, README (honest matrix), CI workflow,
  docs/phase-1-design.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:35:34 +02:00

99 lines
2.9 KiB
Go

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)
}
}