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>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user