feat(tool): Top5#2 完整 JSON-Schema 校验 + 类型强转 + 错误路径 + defaults(对齐 OpenClaw validation)
- 引入 github.com/santhosh-tekuri/jsonschema/v5(纯 Go,OpenClaw/MCP SDK 同思路)。 - validate.go 重写:validateArgs 用 jsonschema Compile+Validate 替换手写子集(required单层), 支持 required/properties/嵌套 object/array items/additionalProperties/allOf/anyOf/oneOf/ 类型联合/enum/min-max/pattern/defaults;编译器缓存 sync.Map;Draft7。 - coerce.go:对齐 OpenClaw coerceWithJsonSchema 类型强转(string→number/integer/boolean/ array/object,number/bool→string;union 处理;object/array 递归)。 - defaults.go:applyDefaults 缺失属性按 default 填(含嵌套),先填默认再强转再校验。 - 错误路径:ValidationError.InstanceLocation → 带路径报告(/count: expected integer...)。 - 测试:tool_test.go 更新 required 语义 + 补强转/default/错误路径用例。 go build/vet/test 全绿 + windows/darwin 交叉编译 + gofmt。
This commit is contained in:
1 parent
24d5924cee
commit
464ed2cad3
6 files changed
+718
-156
No files matched your search
@@ -140,7 +140,7 @@ func TestInvoke_MissingRequired(t *testing.T) {
|
||||
if !errors.Is(err, ErrInvalidArgs) {
|
||||
t.Fatalf("Invoke error = %v, want ErrInvalidArgs", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "missing required argument") {
|
||||
if !strings.Contains(err.Error(), "missing properties") {
|
||||
t.Fatalf("error message = %q, want missing-required hint", err.Error())
|
||||
}
|
||||
}
|
||||
@@ -354,13 +354,20 @@ func TestValidateArgs_Direct(t *testing.T) {
|
||||
{name: "required missing", schema: map[string]any{"required": []any{"r"}}, args: map[string]any{}, wantErr: true},
|
||||
{name: "required present", schema: map[string]any{"required": []any{"r"}}, args: map[string]any{"r": 1}, wantErr: false},
|
||||
{name: "type string ok", schema: props("string"), args: map[string]any{"v": "x"}, wantErr: false},
|
||||
{name: "type string mismatch", schema: props("string"), args: map[string]any{"v": 1}, wantErr: true},
|
||||
{name: "type string mismatch", schema: props("string"), args: map[string]any{"v": []any{"x"}}, wantErr: true},
|
||||
{name: "type array ok", schema: props("array"), args: map[string]any{"v": []any{1}}, wantErr: false},
|
||||
{name: "type object ok", schema: props("object"), args: map[string]any{"v": map[string]any{}}, wantErr: false},
|
||||
{name: "type boolean ok", schema: props("boolean"), args: map[string]any{"v": true}, wantErr: false},
|
||||
{name: "type number ok", schema: props("number"), args: map[string]any{"v": 2.5}, wantErr: false},
|
||||
{name: "integer as number ok", schema: props("number"), args: map[string]any{"v": 2}, wantErr: false},
|
||||
{name: "unknown field ignored", schema: props("string"), args: map[string]any{"v": "x", "u": 9}, wantErr: false},
|
||||
{name: "coerce string to number", schema: props("number"), args: map[string]any{"v": "3"}, wantErr: false},
|
||||
{name: "coerce string to integer", schema: props("integer"), args: map[string]any{"v": "3"}, wantErr: false},
|
||||
{name: "coerce string to boolean", schema: props("boolean"), args: map[string]any{"v": "true"}, wantErr: false},
|
||||
{name: "coerce string to array", schema: props("array"), args: map[string]any{"v": "[1,2]"}, wantErr: false},
|
||||
{name: "coerce string to object", schema: props("object"), args: map[string]any{"v": `{"a":1}`}, wantErr: false},
|
||||
{name: "bad coercion still error", schema: props("number"), args: map[string]any{"v": "abc"}, wantErr: true},
|
||||
{name: "default fills missing", schema: map[string]any{"type": "object", "properties": map[string]any{"v": map[string]any{"type": "string", "default": "x"}}}, args: map[string]any{}, wantErr: false},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
@@ -373,3 +380,87 @@ func TestValidateArgs_Direct(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateArgs_CoercionValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
schema := func(typ string) map[string]any {
|
||||
return map[string]any{"type": "object", "properties": map[string]any{"v": map[string]any{"type": typ}}}
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
typ string
|
||||
input any
|
||||
want any
|
||||
}{
|
||||
{name: "string to number", typ: "number", input: "3", want: float64(3)},
|
||||
{name: "string to integer", typ: "integer", input: "4", want: float64(4)},
|
||||
{name: "string to boolean true", typ: "boolean", input: "true", want: true},
|
||||
{name: "number to string", typ: "string", input: 7, want: "7"},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
args := map[string]any{"v": tt.input}
|
||||
if err := validateArgs(schema(tt.typ), args); err != nil {
|
||||
t.Fatalf("validateArgs error = %v, want nil", err)
|
||||
}
|
||||
if !reflect.DeepEqual(args["v"], tt.want) {
|
||||
t.Fatalf("coerced v = %#v, want %#v", args["v"], tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateArgs_Defaults(t *testing.T) {
|
||||
t.Parallel()
|
||||
schema := map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"q": map[string]any{"type": "string", "default": "hello"},
|
||||
},
|
||||
}
|
||||
args := map[string]any{}
|
||||
if err := validateArgs(schema, args); err != nil {
|
||||
t.Fatalf("validateArgs error = %v, want nil", err)
|
||||
}
|
||||
if got := args["q"]; got != "hello" {
|
||||
t.Fatalf("args[q] = %#v, want default hello", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateArgs_ErrorPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
nested := map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"user": map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"age": map[string]any{"type": "integer"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": []any{"user"},
|
||||
}
|
||||
t.Run("nested property path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := validateArgs(nested, map[string]any{"user": map[string]any{"age": "x"}})
|
||||
if err == nil {
|
||||
t.Fatalf("validateArgs error = nil, want failure")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "/user/age") {
|
||||
t.Fatalf("error = %q, want instance path /user/age", err.Error())
|
||||
}
|
||||
})
|
||||
t.Run("required property path", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := validateArgs(nested, map[string]any{})
|
||||
if err == nil {
|
||||
t.Fatalf("validateArgs error = nil, want failure")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "/user") {
|
||||
t.Fatalf("error = %q, want instance path /user", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user