Files
majordomo/llm/jsonschema_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

141 lines
3.8 KiB
Go

package llm
import (
"encoding/json"
"strings"
"testing"
"time"
)
func mustSchema[T any](t *testing.T) map[string]any {
t.Helper()
raw, err := SchemaFor[T]()
if err != nil {
t.Fatalf("SchemaFor: %v", err)
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatalf("schema is not valid JSON: %v", err)
}
return m
}
func TestSchemaForBasicStruct(t *testing.T) {
type Person struct {
Name string `json:"name" description:"full name"`
Age int `json:"age"`
Score float64 `json:"score"`
Active bool `json:"active"`
Ignored string `json:"-"`
hidden string //nolint:unused
}
s := mustSchema[Person](t)
if s["type"] != "object" || s["additionalProperties"] != false {
t.Errorf("root = %v", s)
}
props := s["properties"].(map[string]any)
if len(props) != 4 {
t.Errorf("properties = %v", props)
}
if props["name"].(map[string]any)["description"] != "full name" {
t.Errorf("name = %v", props["name"])
}
if props["age"].(map[string]any)["type"] != "integer" ||
props["score"].(map[string]any)["type"] != "number" ||
props["active"].(map[string]any)["type"] != "boolean" {
t.Errorf("props = %v", props)
}
req := s["required"].([]any)
if len(req) != 4 {
t.Errorf("required = %v (all fields must be required for strict mode)", req)
}
}
func TestSchemaForNestedAndCollections(t *testing.T) {
type Inner struct {
V string `json:"v"`
}
type Outer struct {
Items []Inner `json:"items"`
Labels map[string]string `json:"labels"`
Blob []byte `json:"blob"`
When time.Time `json:"when"`
Extra json.RawMessage `json:"extra"`
}
s := mustSchema[Outer](t)
props := s["properties"].(map[string]any)
items := props["items"].(map[string]any)
if items["type"] != "array" {
t.Errorf("items = %v", items)
}
inner := items["items"].(map[string]any)
if inner["type"] != "object" || inner["additionalProperties"] != false {
t.Errorf("inner = %v", inner)
}
labels := props["labels"].(map[string]any)
if labels["type"] != "object" || labels["additionalProperties"].(map[string]any)["type"] != "string" {
t.Errorf("labels = %v", labels)
}
if props["blob"].(map[string]any)["type"] != "string" {
t.Errorf("blob = %v", props["blob"])
}
when := props["when"].(map[string]any)
if when["type"] != "string" || when["format"] != "date-time" {
t.Errorf("when = %v", when)
}
if len(props["extra"].(map[string]any)) != 0 {
t.Errorf("extra (RawMessage) should be the any-schema, got %v", props["extra"])
}
}
func TestSchemaForPointerIsNullable(t *testing.T) {
type Opt struct {
Note *string `json:"note"`
}
s := mustSchema[Opt](t)
note := s["properties"].(map[string]any)["note"].(map[string]any)
anyOf := note["anyOf"].([]any)
if len(anyOf) != 2 {
t.Fatalf("anyOf = %v", anyOf)
}
if anyOf[0].(map[string]any)["type"] != "string" || anyOf[1].(map[string]any)["type"] != "null" {
t.Errorf("anyOf = %v", anyOf)
}
// Still required (strict-mode style: optional == nullable).
if req := s["required"].([]any); len(req) != 1 {
t.Errorf("required = %v", req)
}
}
func TestSchemaForEnum(t *testing.T) {
type Choice struct {
Mood string `json:"mood" enum:"happy,sad, ambivalent"`
}
s := mustSchema[Choice](t)
mood := s["properties"].(map[string]any)["mood"].(map[string]any)
enum := mood["enum"].([]any)
if len(enum) != 3 || enum[2] != "ambivalent" {
t.Errorf("enum = %v (values must be trimmed)", enum)
}
}
func TestSchemaForRecursionRejected(t *testing.T) {
type Node struct {
Children []*Node `json:"children"`
}
_, err := SchemaFor[Node]()
if err == nil || !strings.Contains(err.Error(), "recursive") {
t.Errorf("err = %v, want recursion rejection", err)
}
}
func TestSchemaForUnsupportedKind(t *testing.T) {
type Bad struct {
Ch chan int `json:"ch"`
}
if _, err := SchemaFor[Bad](); err == nil {
t.Error("chan field must be rejected")
}
}