The source headers, LICENSE files, and license metadata had drifted apart. Align the entire project to MIT: - Convert every source-file header to the MIT text across all comment styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including SPDX-License-Identifier tags - Set the root and cookie-banner LICENSE files to the MIT text with a "MIT License" title line - Switch the package.json license fields, Docker image label, and cookie-banner README to MIT - Update docs and the genmodels header generator accordingly - Normalize copyright lines to a single format (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the hello@getprobo.com and hello@probo.inc emails to hello@probo.com and the comma-separated years to a hyphenated range Genuine third-party references are intentionally left untouched: the Lucide icon attributions (Lucide is ISC) and the trivy dependency license allowlist. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
371 lines
10 KiB
Go
371 lines
10 KiB
Go
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGenerateSchema_PointerFieldsStripNull(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Pointer fields without omitempty are treated as required by the schema
|
|
// library. null is stripped and they appear as simple non-nullable types.
|
|
type Params struct {
|
|
Name *string `json:"name"`
|
|
Count *int `json:"count"`
|
|
Score *float64 `json:"score"`
|
|
Done *bool `json:"done"`
|
|
}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
props := schema["properties"].(map[string]any)
|
|
|
|
for _, field := range []struct {
|
|
name string
|
|
wantType string
|
|
}{
|
|
{"name", "string"},
|
|
{"count", "integer"},
|
|
{"score", "number"},
|
|
{"done", "boolean"},
|
|
} {
|
|
prop := props[field.name].(map[string]any)
|
|
assert.Equal(t, field.wantType, prop["type"], "field %s", field.name)
|
|
}
|
|
}
|
|
|
|
func TestGenerateSchema_IntegerBoundsStripped(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Params struct {
|
|
Int8 int8 `json:"int8"`
|
|
Int16 int16 `json:"int16"`
|
|
Int32 int32 `json:"int32"`
|
|
Uint8 uint8 `json:"uint8"`
|
|
Uint16 uint16 `json:"uint16"`
|
|
}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
props := schema["properties"].(map[string]any)
|
|
|
|
for _, name := range []string{"int8", "int16", "int32", "uint8", "uint16"} {
|
|
prop := props[name].(map[string]any)
|
|
assert.Equal(t, "integer", prop["type"], "field %s", name)
|
|
assert.Nil(t, prop["minimum"], "field %s should have no minimum", name)
|
|
assert.Nil(t, prop["maximum"], "field %s should have no maximum", name)
|
|
}
|
|
}
|
|
|
|
func TestGenerateSchema_EmptyStruct(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Params struct{}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
assert.Equal(t, "object", schema["type"])
|
|
|
|
props, ok := schema["properties"].(map[string]any)
|
|
require.True(t, ok, "empty struct should have a properties field")
|
|
assert.Empty(t, props)
|
|
}
|
|
|
|
func TestGenerateSchema_MapField(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Params struct {
|
|
Metadata map[string]string `json:"metadata"`
|
|
}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
props := schema["properties"].(map[string]any)
|
|
metaProp := props["metadata"].(map[string]any)
|
|
|
|
assert.Equal(t, "object", metaProp["type"])
|
|
|
|
addlProps, ok := metaProp["additionalProperties"].(map[string]any)
|
|
require.True(t, ok, "map field should produce additionalProperties")
|
|
assert.Equal(t, "string", addlProps["type"])
|
|
}
|
|
|
|
func TestGenerateSchema_NestedPointerStruct(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Inner struct {
|
|
Value *string `json:"value"`
|
|
}
|
|
|
|
type Params struct {
|
|
Inner *Inner `json:"inner"`
|
|
}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
props := schema["properties"].(map[string]any)
|
|
|
|
// Pointer-to-struct without omitempty is in required with a simple type.
|
|
innerProp := props["inner"].(map[string]any)
|
|
assert.Equal(t, "object", innerProp["type"])
|
|
assert.Nil(t, innerProp["types"], "pointer to struct should not have union types")
|
|
|
|
// Nested pointer field without omitempty is also required and non-nullable.
|
|
innerProps := innerProp["properties"].(map[string]any)
|
|
valueProp := innerProps["value"].(map[string]any)
|
|
assert.Equal(t, "string", valueProp["type"])
|
|
assert.Nil(t, valueProp["types"], "nested pointer field should not have union types")
|
|
}
|
|
|
|
func TestGenerateSchema_SliceOfPointers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Params struct {
|
|
Names []*string `json:"names"`
|
|
}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
props := schema["properties"].(map[string]any)
|
|
|
|
namesProp := props["names"].(map[string]any)
|
|
assert.Equal(t, "array", namesProp["type"])
|
|
|
|
items := namesProp["items"].(map[string]any)
|
|
assert.Equal(t, "string", items["type"])
|
|
assert.Nil(t, items["types"], "array items from pointer should not have union types")
|
|
}
|
|
|
|
func TestGenerateSchema_MapWithPointerValues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Params struct {
|
|
Scores map[string]*int `json:"scores"`
|
|
}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
props := schema["properties"].(map[string]any)
|
|
|
|
scoresProp := props["scores"].(map[string]any)
|
|
assert.Equal(t, "object", scoresProp["type"])
|
|
|
|
addlProps := scoresProp["additionalProperties"].(map[string]any)
|
|
assert.Equal(t, "integer", addlProps["type"])
|
|
assert.Nil(t, addlProps["types"], "map pointer values should not have union types")
|
|
assert.Nil(t, addlProps["minimum"])
|
|
assert.Nil(t, addlProps["maximum"])
|
|
}
|
|
|
|
func TestGenerateSchema_DescriptionFromJsonschemaTag(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Params struct {
|
|
Query string `json:"query" jsonschema:"The search query to execute"`
|
|
Limit int `json:"limit" jsonschema:"Maximum number of results to return"`
|
|
}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
props := schema["properties"].(map[string]any)
|
|
|
|
queryProp := props["query"].(map[string]any)
|
|
assert.Equal(t, "The search query to execute", queryProp["description"])
|
|
|
|
limitProp := props["limit"].(map[string]any)
|
|
assert.Equal(t, "Maximum number of results to return", limitProp["description"])
|
|
}
|
|
|
|
func TestGenerateSchema_RequiredVsOptional(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Params struct {
|
|
Required string `json:"required"`
|
|
Optional *string `json:"optional,omitempty"`
|
|
OmitEmpty string `json:"omit_empty,omitempty"`
|
|
AlsoNeeded int `json:"also_needed"`
|
|
}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
// All properties must appear in required (OpenAI requirement).
|
|
required := schema["required"].([]any)
|
|
assert.Contains(t, required, "required")
|
|
assert.Contains(t, required, "also_needed")
|
|
assert.Contains(t, required, "optional")
|
|
assert.Contains(t, required, "omit_empty")
|
|
|
|
// Optional properties must be nullable so the model can pass null.
|
|
props := schema["properties"].(map[string]any)
|
|
|
|
optionalType := props["optional"].(map[string]any)["type"].([]any)
|
|
assert.Contains(t, optionalType, "string")
|
|
assert.Contains(t, optionalType, "null")
|
|
|
|
omitEmptyType := props["omit_empty"].(map[string]any)["type"].([]any)
|
|
assert.Contains(t, omitEmptyType, "string")
|
|
assert.Contains(t, omitEmptyType, "null")
|
|
|
|
// Truly required properties must not be nullable.
|
|
requiredType := props["required"].(map[string]any)["type"]
|
|
assert.Equal(t, "string", requiredType)
|
|
|
|
alsoNeededType := props["also_needed"].(map[string]any)["type"]
|
|
assert.Equal(t, "integer", alsoNeededType)
|
|
}
|
|
|
|
func TestGenerateSchema_DeeplyNestedStructure(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Level3 struct {
|
|
Value *int `json:"value"`
|
|
}
|
|
|
|
type Level2 struct {
|
|
Items []Level3 `json:"items"`
|
|
}
|
|
|
|
type Level1 struct {
|
|
Child *Level2 `json:"child"`
|
|
}
|
|
|
|
type Params struct {
|
|
Root Level1 `json:"root"`
|
|
}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
props := schema["properties"].(map[string]any)
|
|
|
|
rootProp := props["root"].(map[string]any)
|
|
assert.Equal(t, "object", rootProp["type"])
|
|
|
|
rootProps := rootProp["properties"].(map[string]any)
|
|
childProp := rootProps["child"].(map[string]any)
|
|
assert.Equal(t, "object", childProp["type"])
|
|
assert.Nil(t, childProp["types"])
|
|
|
|
childProps := childProp["properties"].(map[string]any)
|
|
itemsProp := childProps["items"].(map[string]any)
|
|
assert.Equal(t, "array", itemsProp["type"])
|
|
|
|
itemsItems := itemsProp["items"].(map[string]any)
|
|
assert.Equal(t, "object", itemsItems["type"])
|
|
|
|
level3Props := itemsItems["properties"].(map[string]any)
|
|
valueProp := level3Props["value"].(map[string]any)
|
|
assert.Equal(t, "integer", valueProp["type"])
|
|
assert.Nil(t, valueProp["types"])
|
|
assert.Nil(t, valueProp["minimum"])
|
|
assert.Nil(t, valueProp["maximum"])
|
|
}
|
|
|
|
func TestGenerateSchema_SliceOfStructs(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type Item struct {
|
|
Name string `json:"name"`
|
|
Count *int `json:"count,omitempty"`
|
|
}
|
|
|
|
type Params struct {
|
|
Items []Item `json:"items"`
|
|
}
|
|
|
|
raw, err := jsonSchemaFor[Params]()
|
|
require.NoError(t, err)
|
|
|
|
var schema map[string]any
|
|
require.NoError(t, json.Unmarshal(raw, &schema))
|
|
|
|
props := schema["properties"].(map[string]any)
|
|
|
|
itemsProp := props["items"].(map[string]any)
|
|
assert.Equal(t, "array", itemsProp["type"])
|
|
|
|
items := itemsProp["items"].(map[string]any)
|
|
assert.Equal(t, "object", items["type"])
|
|
|
|
itemProps := items["properties"].(map[string]any)
|
|
assert.Contains(t, itemProps, "name")
|
|
assert.Contains(t, itemProps, "count")
|
|
|
|
countProp := itemProps["count"].(map[string]any)
|
|
countType := countProp["type"].([]any)
|
|
assert.Contains(t, countType, "integer")
|
|
assert.Contains(t, countType, "null")
|
|
assert.Nil(t, countProp["minimum"])
|
|
}
|
|
|
|
func TestStripNullTypes_NilSchema(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.NotPanics(t, func() {
|
|
stripNullTypes(nil)
|
|
})
|
|
}
|