Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions internal/agent/tools/param_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"strings"
"unicode/utf8"
)

// ValidationError describes a specific parameter validation failure.
Expand Down Expand Up @@ -132,14 +133,16 @@ func validateProperty(name string, val any, prop map[string]any) []ValidationErr
// String length bounds
if targetType == "string" {
if s, ok := val.(string); ok {
if minLen, ok := getFloat(prop, "minLength"); ok && float64(len(s)) < minLen {
// JSON Schema measures string length in Unicode code points, not bytes.
length := float64(utf8.RuneCountInString(s))
if minLen, ok := getFloat(prop, "minLength"); ok && length < minLen {
errs = append(errs, ValidationError{
Param: name,
Message: fmt.Sprintf("parameter '%s' must have at least %d characters",
name, int(minLen)),
})
}
if maxLen, ok := getFloat(prop, "maxLength"); ok && float64(len(s)) > maxLen {
if maxLen, ok := getFloat(prop, "maxLength"); ok && length > maxLen {
errs = append(errs, ValidationError{
Param: name,
Message: fmt.Sprintf("parameter '%s' must have at most %d characters",
Expand Down
41 changes: 41 additions & 0 deletions internal/agent/tools/param_validate_unicode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package tools

import (
"encoding/json"
"strings"
"testing"
)

func TestValidateParamsUnicodeLength(t *testing.T) {
cases := []struct {
name, value, constraint, wantError string
}{
{"Chinese maximum", "你好", `"maxLength":2`, ""},
{"emoji maximum", "😀", `"maxLength":1`, ""},
{"accent maximum", "é", `"maxLength":1`, ""},
{"Chinese minimum", "你", `"minLength":2`, "at least 2 characters"},
{"emoji minimum", "😀", `"minLength":2`, "at least 2 characters"},
{"mixed exact length", "A你😀", `"minLength":3,"maxLength":3`, ""},
{"combining code points", "e\u0301", `"minLength":2,"maxLength":2`, ""},
{"too many code points", "你好啊", `"maxLength":2`, "at most 2 characters"},
{"empty minimum", "", `"minLength":1`, "at least 1 characters"},
{"ASCII exact length", "abc", `"minLength":3,"maxLength":3`, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
args, err := json.Marshal(map[string]string{"text": tc.value})
if err != nil {
t.Fatal(err)
}
schema := json.RawMessage(`{"type":"object","properties":{"text":{"type":"string",` + tc.constraint + `}}}`)
errs := ValidateParams(args, schema)
if tc.wantError == "" {
if len(errs) != 0 {
t.Fatalf("valid code-point length rejected: %v", errs)
}
} else if len(errs) != 1 || errs[0].Param != "text" || !strings.Contains(errs[0].Message, tc.wantError) {
t.Fatalf("expected %q for text, got %v", tc.wantError, errs)
}
})
}
}
Loading