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