dcd004289f
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>
99 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|