Files
majordomo/generate_test.go
T
steve 7dab4112ff feat: agent run loop, Generate[T], reflect-derived schemas
Phase 5:
- agent/: model + system prompt + toolboxes composition; bounded
  tool-dispatch loop (default 10 steps); panic-proof tool execution;
  unknown-tool and duplicate-name handling; history continuation; step
  observers; partial results on ErrMaxSteps/errors (ADR-0012)
- llm.SchemaFor[T]: strict-compatible JSON schemas from Go types
  (nullable pointers, description/enum tags, recursion rejected)
- majordomo.Generate[T]: typed structured output with fence-stripping
  decode and model-naming errors
- README agents/structured-output sections + matrix synced

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

90 lines
2.5 KiB
Go

package majordomo
import (
"context"
"encoding/json"
"strings"
"testing"
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
"gitea.stevedudenhoeffer.com/steve/majordomo/provider/fake"
)
type verdict struct {
Guilty bool `json:"guilty"`
Why string `json:"why"`
}
func TestGenerateTyped(t *testing.T) {
r := newTestRegistry(t)
fp := fake.New("fp")
r.RegisterProvider(fp)
fp.Enqueue("judge", fake.Reply(`{"guilty":true,"why":"caught red-handed"}`))
m, _ := r.Parse("fp/judge")
v, err := Generate[verdict](context.Background(), m, Request{Messages: []Message{UserText("verdict?")}})
if err != nil {
t.Fatalf("Generate: %v", err)
}
if !v.Guilty || v.Why != "caught red-handed" {
t.Errorf("verdict = %+v", v)
}
// The schema derived from the struct reached the provider, named after
// the type.
req := fp.Calls()[0].Request
if req.SchemaName != "verdict" {
t.Errorf("schema name = %q", req.SchemaName)
}
var schema map[string]any
if err := json.Unmarshal(req.Schema, &schema); err != nil {
t.Fatalf("schema: %v", err)
}
props := schema["properties"].(map[string]any)
if _, ok := props["guilty"]; !ok {
t.Errorf("schema = %v", schema)
}
}
func TestGenerateStripsMarkdownFence(t *testing.T) {
r := newTestRegistry(t)
fp := fake.New("fp")
r.RegisterProvider(fp)
fp.Enqueue("judge", fake.Reply("```json\n{\"guilty\":false,\"why\":\"alibi\"}\n```"))
m, _ := r.Parse("fp/judge")
v, err := Generate[verdict](context.Background(), m, Request{Messages: []Message{UserText("verdict?")}})
if err != nil {
t.Fatalf("Generate: %v", err)
}
if v.Guilty || v.Why != "alibi" {
t.Errorf("verdict = %+v", v)
}
}
func TestGenerateDecodeErrorNamesModel(t *testing.T) {
r := newTestRegistry(t)
fp := fake.New("fp")
r.RegisterProvider(fp)
fp.Enqueue("judge", fake.Reply("not json at all"))
m, _ := r.Parse("fp/judge")
_, err := Generate[verdict](context.Background(), m, Request{Messages: []Message{UserText("verdict?")}})
if err == nil || !strings.Contains(err.Error(), "fp/judge") {
t.Errorf("err = %v, want decode error naming the model", err)
}
}
func TestGenerateEmptyResponse(t *testing.T) {
r := newTestRegistry(t)
fp := fake.New("fp")
r.RegisterProvider(fp)
fp.Enqueue("judge", fake.ReplyWith(llm.Response{FinishReason: llm.FinishLength}))
m, _ := r.Parse("fp/judge")
_, err := Generate[verdict](context.Background(), m, Request{Messages: []Message{UserText("verdict?")}})
if err == nil || !strings.Contains(err.Error(), "empty") {
t.Errorf("err = %v, want empty-response error", err)
}
}