From 5d781ee6b7dcda2dd22d86108ae70f52253a889b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=85=B0=E9=91=AB?= <1735612127@qq.com> Date: Sat, 12 Sep 2026 23:25:57 +0800 Subject: [PATCH 1/3] fix(antigravity): handle prefixItems and ensure array schema has valid items fallback - Handle JSON Schema Draft 2020-12 prefixItems (tuple arrays) by converting to a best-match items schema - Ensure array schema always defines a valid items schema to prevent Gemini 400 'missing field: items.items' error - Fix recursive traversal when a schema defines both properties and items - Add comprehensive unit test cases for tuple and array schemas --- .../pkg/antigravity/schema_cleaner.go | 124 +++++++++++------- .../pkg/antigravity/schema_cleaner_test.go | 110 ++++++++++++++++ 2 files changed, 189 insertions(+), 45 deletions(-) create mode 100644 backend/internal/pkg/antigravity/schema_cleaner_test.go diff --git a/backend/internal/pkg/antigravity/schema_cleaner.go b/backend/internal/pkg/antigravity/schema_cleaner.go index 0ee746aa37..8988603ff1 100644 --- a/backend/internal/pkg/antigravity/schema_cleaner.go +++ b/backend/internal/pkg/antigravity/schema_cleaner.go @@ -119,14 +119,27 @@ func cleanJSONSchemaRecursive(value any) any { // 0. [NEW] 合并 allOf mergeAllOf(schemaMap) + // 0.5 [FIX] 处理 Draft 2020-12 的 prefixItems (元组定义) + if prefixItems, ok := schemaMap["prefixItems"].([]any); ok && len(prefixItems) > 0 { + itemsVal, hasItems := schemaMap["items"] + _, itemsIsBool := itemsVal.(bool) + if !hasItems || itemsVal == nil || itemsIsBool { + best := extractBestSchemaFromUnion(prefixItems) + if best == nil { + best = map[string]any{"type": "string"} + } + schemaMap["items"] = best + } + delete(schemaMap, "prefixItems") + } + // 1. [CRITICAL] 深度递归处理子项 if props, ok := schemaMap["properties"].(map[string]any); ok { for _, v := range props { cleanJSONSchemaRecursive(v) } - // Go 中不需要像 Rust 那样显式处理 nullable_keys remove required, - // 因为我们在子项处理中会正确设置 type 和 description - } else if items, ok := schemaMap["items"]; ok { + } + if items, ok := schemaMap["items"]; ok { // [FIX] Gemini 期望 "items" 是单个 Schema 对象(列表验证),而不是数组(元组验证)。 if itemsArr, ok := items.([]any); ok { // 策略:将元组 [A, B] 视为 A、B 中的最佳匹配项。 @@ -141,8 +154,9 @@ func cleanJSONSchemaRecursive(value any) any { } else { cleanJSONSchemaRecursive(items) } - } else { - // 遍历所有值递归 + } + // 如果既无 properties 也无 items,遍历其他 map/slice 子项递归 + if !hasKey(schemaMap, "properties") && !hasKey(schemaMap, "items") { for _, v := range schemaMap { if _, isMap := v.(map[string]any); isMap { cleanJSONSchemaRecursive(v) @@ -238,43 +252,7 @@ func cleanJSONSchemaRecursive(value any) any { } } - // 6. [SAFETY] 处理空 Object - if t, _ := schemaMap["type"].(string); t == "object" { - hasProps := false - if props, ok := schemaMap["properties"].(map[string]any); ok && len(props) > 0 { - hasProps = true - } - if !hasProps { - schemaMap["properties"] = map[string]any{ - "reason": map[string]any{ - "type": "string", - "description": "Reason for calling this tool", - }, - } - schemaMap["required"] = []any{"reason"} - } - } - - // 7. [SAFETY] Required 字段对齐 - if props, ok := schemaMap["properties"].(map[string]any); ok { - if req, ok := schemaMap["required"].([]any); ok { - var validReq []any - for _, r := range req { - if rStr, ok := r.(string); ok { - if _, exists := props[rStr]; exists { - validReq = append(validReq, r) - } - } - } - if len(validReq) > 0 { - schemaMap["required"] = validReq - } else { - delete(schemaMap, "required") - } - } - } - - // 8. 处理 type 字段 (Lowercase + Nullable 提取) + // 6. 处理 type 字段 (Lowercase + Nullable 提取) isEffectivelyNullable := false if typeVal, exists := schemaMap["type"]; exists { var selectedType string @@ -305,16 +283,72 @@ func cleanJSONSchemaRecursive(value any) any { } schemaMap["type"] = selectedType } else { - // 默认 object 如果有 properties (虽然上面白名单过滤可能删了 type 如果它不在... 但 type 必在 allowlist) - // 如果没有 type,但有 properties,补一个 + // 如果没有 type,但有 properties/items,推断并补全 if hasKey(schemaMap, "properties") { schemaMap["type"] = "object" + } else if hasKey(schemaMap, "items") { + schemaMap["type"] = "array" } else { - // 默认为 string ? or object? Gemini 通常需要明确 type schemaMap["type"] = "object" } } + // 7. [SAFETY] 处理空 Object + if t, _ := schemaMap["type"].(string); t == "object" { + hasProps := false + if props, ok := schemaMap["properties"].(map[string]any); ok && len(props) > 0 { + hasProps = true + } + if !hasProps { + schemaMap["properties"] = map[string]any{ + "reason": map[string]any{ + "type": "string", + "description": "Reason for calling this tool", + }, + } + schemaMap["required"] = []any{"reason"} + } + } + + // 8. [SAFETY] 处理缺失 items 的 Array (Gemini 严苛要求 array 必须声明 items Schema) + if t, _ := schemaMap["type"].(string); t == "array" { + itemsVal, hasItems := schemaMap["items"] + needDefaultItems := false + if !hasItems || itemsVal == nil { + needDefaultItems = true + } else if itemsMap, ok := itemsVal.(map[string]any); ok { + if len(itemsMap) == 0 { + needDefaultItems = true + } + } else if _, isBool := itemsVal.(bool); isBool { + needDefaultItems = true + } + if needDefaultItems { + schemaMap["items"] = map[string]any{ + "type": "string", + } + } + } + + // 9. [SAFETY] Required 字段对齐 + if props, ok := schemaMap["properties"].(map[string]any); ok { + if req, ok := schemaMap["required"].([]any); ok { + var validReq []any + for _, r := range req { + if rStr, ok := r.(string); ok { + if _, exists := props[rStr]; exists { + validReq = append(validReq, r) + } + } + } + if len(validReq) > 0 { + schemaMap["required"] = validReq + } else { + delete(schemaMap, "required") + } + } + } + if isEffectivelyNullable { desc, _ := schemaMap["description"].(string) if !strings.Contains(desc, "nullable") { diff --git a/backend/internal/pkg/antigravity/schema_cleaner_test.go b/backend/internal/pkg/antigravity/schema_cleaner_test.go new file mode 100644 index 0000000000..516d010c72 --- /dev/null +++ b/backend/internal/pkg/antigravity/schema_cleaner_test.go @@ -0,0 +1,110 @@ +package antigravity + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCleanJSONSchema_ArrayPrefixItems(t *testing.T) { + // 模拟 Claude Code 2.1 Artifact 工具的 query.where 参数 Schema (Draft 2020-12 prefixItems 元组) + input := map[string]any{ + "type": "object", + "properties": map[string]any{ + "query": map[string]any{ + "type": "object", + "properties": map[string]any{ + "where": map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "array", + "prefixItems": []any{ + map[string]any{"type": "string"}, + map[string]any{"type": "string", "enum": []any{"==", "!=", ">", "<"}}, + map[string]any{}, + }, + }, + "maxItems": float64(10), + }, + }, + }, + }, + } + + cleaned := CleanJSONSchema(input) + require.NotNil(t, cleaned) + + query, ok := cleaned["properties"].(map[string]any)["query"].(map[string]any) + require.True(t, ok) + where, ok := query["properties"].(map[string]any)["where"].(map[string]any) + require.True(t, ok) + + assert.Equal(t, "array", where["type"]) + // prefixItems 应被移除或替换 + assert.Nil(t, where["prefixItems"]) + + whereItems, ok := where["items"].(map[string]any) + require.True(t, ok, "where.items must be an object") + assert.Equal(t, "array", whereItems["type"]) + + // 关键验证:where.items.items 必须存在且有效,不能缺失导致 Gemini 400 + innerItems, ok := whereItems["items"].(map[string]any) + require.True(t, ok, "where.items.items must be an object") + assert.Equal(t, "string", innerItems["type"]) +} + +func TestCleanJSONSchema_ArrayMissingItemsFallback(t *testing.T) { + // 针对任何缺少 items 的 array,必须兜底注入 items: {type: string} + input := map[string]any{ + "type": "object", + "properties": map[string]any{ + "tags": map[string]any{ + "type": "array", + }, + "nested_empty_array": map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "array", + }, + }, + }, + } + + cleaned := CleanJSONSchema(input) + require.NotNil(t, cleaned) + + props := cleaned["properties"].(map[string]any) + tags := props["tags"].(map[string]any) + require.NotNil(t, tags["items"]) + assert.Equal(t, "string", tags["items"].(map[string]any)["type"]) + + nested := props["nested_empty_array"].(map[string]any) + nestedItems := nested["items"].(map[string]any) + assert.Equal(t, "array", nestedItems["type"]) + require.NotNil(t, nestedItems["items"]) + assert.Equal(t, "string", nestedItems["items"].(map[string]any)["type"]) +} + +func TestCleanJSONSchema_ArrayExistingItemsPreserved(t *testing.T) { + // 正常的 array items 不受影响 + input := map[string]any{ + "type": "object", + "properties": map[string]any{ + "numbers": map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "integer", + }, + }, + }, + } + + cleaned := CleanJSONSchema(input) + require.NotNil(t, cleaned) + + numbers := cleaned["properties"].(map[string]any)["numbers"].(map[string]any) + assert.Equal(t, "array", numbers["type"]) + items := numbers["items"].(map[string]any) + assert.Equal(t, "integer", items["type"]) +} From 69d49744cf644b1dc7c73bf57167047e4db1e089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=85=B0=E9=91=AB?= <1735612127@qq.com> Date: Sun, 13 Sep 2026 03:57:18 +0800 Subject: [PATCH 2/3] fix(antigravity): address code review feedback on schema cleaner - Fix test assertion target location for whereItems prefixItems - Support const keyword normalization to enum and infer scalar type - Infer scalar type for enum-only schema to prevent object reason injection - Recursively clean properties and items after anyOf/oneOf merge - Ensure empty prefixItems is safely deleted --- .../pkg/antigravity/schema_cleaner.go | 77 ++++++++++-- .../pkg/antigravity/schema_cleaner_test.go | 115 +++++++++++++++++- 2 files changed, 180 insertions(+), 12 deletions(-) diff --git a/backend/internal/pkg/antigravity/schema_cleaner.go b/backend/internal/pkg/antigravity/schema_cleaner.go index 8988603ff1..9ac4211e4a 100644 --- a/backend/internal/pkg/antigravity/schema_cleaner.go +++ b/backend/internal/pkg/antigravity/schema_cleaner.go @@ -120,16 +120,21 @@ func cleanJSONSchemaRecursive(value any) any { mergeAllOf(schemaMap) // 0.5 [FIX] 处理 Draft 2020-12 的 prefixItems (元组定义) - if prefixItems, ok := schemaMap["prefixItems"].([]any); ok && len(prefixItems) > 0 { - itemsVal, hasItems := schemaMap["items"] - _, itemsIsBool := itemsVal.(bool) - if !hasItems || itemsVal == nil || itemsIsBool { - best := extractBestSchemaFromUnion(prefixItems) - if best == nil { - best = map[string]any{"type": "string"} + if prefixItemsVal, hasPrefix := schemaMap["prefixItems"]; hasPrefix { + if prefixItems, ok := prefixItemsVal.([]any); ok && len(prefixItems) > 0 { + itemsVal, hasItems := schemaMap["items"] + _, itemsIsBool := itemsVal.(bool) + // 如果没有 items,或者 items 为 nil,或者 items 是布尔值(如 Draft 2020-12 中表示元组闭合的 items: false), + // 用选出的最佳元组项填充 items,以满足 Gemini 对 array.items 必须为合法 Schema 对象的规范。 + if !hasItems || itemsVal == nil || itemsIsBool { + best := extractBestSchemaFromUnion(prefixItems) + if best == nil { + best = map[string]any{"type": "string"} + } + schemaMap["items"] = best } - schemaMap["items"] = best } + // 无论 prefixItems 是否为空数组,都彻底删除 prefixItems 关键字以防 Gemini 400 delete(schemaMap, "prefixItems") } @@ -219,6 +224,18 @@ func cleanJSONSchemaRecursive(value any) any { schemaMap[k] = deepCopy(v) } } + // 清除已合并的 union 关键字 + delete(schemaMap, "anyOf") + delete(schemaMap, "oneOf") + // 对合并进来的新属性和 items 进行深度递归清洗,确保合并进来的子结构(如 prefixItems)也能被处理 + if props, ok := schemaMap["properties"].(map[string]any); ok { + for _, pv := range props { + cleanJSONSchemaRecursive(pv) + } + } + if items, ok := schemaMap["items"]; ok { + cleanJSONSchemaRecursive(items) + } } } } @@ -228,14 +245,37 @@ func cleanJSONSchemaRecursive(value any) any { hasKey(schemaMap, "properties") || hasKey(schemaMap, "items") || hasKey(schemaMap, "enum") || + hasKey(schemaMap, "const") || hasKey(schemaMap, "anyOf") || hasKey(schemaMap, "oneOf") || hasKey(schemaMap, "allOf") if looksLikeSchema { - // 4. [ROBUST] 约束迁移 + // 4. [ROBUST] 约束迁移与 const 规范化 migrateConstraints(schemaMap) + // 规范化 const 关键字 (Draft 6+ 转换为 Gemini 兼容的 enum: [const]) + if constVal, exists := schemaMap["const"]; exists { + if _, hasEnum := schemaMap["enum"]; !hasEnum { + schemaMap["enum"] = []any{constVal} + } + if _, hasType := schemaMap["type"]; !hasType { + switch constVal.(type) { + case string: + schemaMap["type"] = "string" + case int, int32, int64: + schemaMap["type"] = "integer" + case float32, float64: + schemaMap["type"] = "number" + case bool: + schemaMap["type"] = "boolean" + default: + schemaMap["type"] = "string" + } + } + delete(schemaMap, "const") + } + // 5. [CRITICAL] 白名单过滤 allowedFields := map[string]bool{ "type": true, @@ -283,11 +323,25 @@ func cleanJSONSchemaRecursive(value any) any { } schemaMap["type"] = selectedType } else { - // 如果没有 type,但有 properties/items,推断并补全 + // 如果没有 type,但有 properties/items/enum,推断并补全 if hasKey(schemaMap, "properties") { schemaMap["type"] = "object" } else if hasKey(schemaMap, "items") { schemaMap["type"] = "array" + } else if enumArr, ok := schemaMap["enum"].([]any); ok && len(enumArr) > 0 { + // [FIX] 针对仅含 enum 的 schema,按其元素类型推断标量类型,避免被误判为 object 并注入 reason 属性 + switch enumArr[0].(type) { + case int, int32, int64: + schemaMap["type"] = "integer" + case float32, float64: + schemaMap["type"] = "number" + case bool: + schemaMap["type"] = "boolean" + default: + schemaMap["type"] = "string" + } + } else if hasKey(schemaMap, "enum") { + schemaMap["type"] = "string" } else { schemaMap["type"] = "object" } @@ -525,6 +579,9 @@ func scoreSchemaOption(val any) int { if hasKey(m, "items") || typeStr == "array" { return 2 } + if hasKey(m, "enum") || hasKey(m, "const") { + return 1 + } if typeStr != "" && typeStr != "null" { return 1 } diff --git a/backend/internal/pkg/antigravity/schema_cleaner_test.go b/backend/internal/pkg/antigravity/schema_cleaner_test.go index 516d010c72..0401b670ad 100644 --- a/backend/internal/pkg/antigravity/schema_cleaner_test.go +++ b/backend/internal/pkg/antigravity/schema_cleaner_test.go @@ -41,12 +41,12 @@ func TestCleanJSONSchema_ArrayPrefixItems(t *testing.T) { require.True(t, ok) assert.Equal(t, "array", where["type"]) - // prefixItems 应被移除或替换 - assert.Nil(t, where["prefixItems"]) whereItems, ok := where["items"].(map[string]any) require.True(t, ok, "where.items must be an object") assert.Equal(t, "array", whereItems["type"]) + // prefixItems 应在 whereItems 中被彻底移除 + assert.Nil(t, whereItems["prefixItems"]) // 关键验证:where.items.items 必须存在且有效,不能缺失导致 Gemini 400 innerItems, ok := whereItems["items"].(map[string]any) @@ -108,3 +108,114 @@ func TestCleanJSONSchema_ArrayExistingItemsPreserved(t *testing.T) { items := numbers["items"].(map[string]any) assert.Equal(t, "integer", items["type"]) } + +func TestCleanJSONSchema_EnumOnlySchemaInTuple(t *testing.T) { + // 测试 enum-only schema 不会被误判为 object,也不应被注入 reason 属性 + input := map[string]any{ + "type": "object", + "properties": map[string]any{ + "choice": map[string]any{ + "enum": []any{"option_a", "option_b"}, + }, + "tuple_with_enum": map[string]any{ + "type": "array", + "prefixItems": []any{ + map[string]any{ + "enum": []any{"read", "write"}, + }, + }, + }, + }, + } + + cleaned := CleanJSONSchema(input) + require.NotNil(t, cleaned) + + props := cleaned["properties"].(map[string]any) + choice := props["choice"].(map[string]any) + assert.Equal(t, "string", choice["type"]) + assert.Nil(t, choice["properties"], "enum-only schema must NOT be treated as object with reason property") + assert.Equal(t, []any{"option_a", "option_b"}, choice["enum"]) + + tupleArray := props["tuple_with_enum"].(map[string]any) + tupleItems := tupleArray["items"].(map[string]any) + assert.Equal(t, "string", tupleItems["type"]) + assert.Nil(t, tupleItems["properties"]) + assert.Equal(t, []any{"read", "write"}, tupleItems["enum"]) +} + +func TestCleanJSONSchema_ConstKeywordConversion(t *testing.T) { + // 测试 const 关键字自动转换为 Gemini 兼容的 enum: [const] 且具有合法 type + input := map[string]any{ + "type": "object", + "properties": map[string]any{ + "action": map[string]any{ + "const": "ping", + }, + }, + } + + cleaned := CleanJSONSchema(input) + require.NotNil(t, cleaned) + + props := cleaned["properties"].(map[string]any) + action := props["action"].(map[string]any) + assert.Nil(t, action["const"], "const keyword must be removed") + assert.Equal(t, "string", action["type"]) + assert.Equal(t, []any{"ping"}, action["enum"]) +} + +func TestCleanJSONSchema_AnyOfNestedPrefixItems(t *testing.T) { + // 测试 anyOf 分支合并进来的嵌套 prefixItems 也被完整深度清洗 + input := map[string]any{ + "type": "object", + "properties": map[string]any{ + "status": map[string]any{"type": "string"}, + }, + "anyOf": []any{ + map[string]any{ + "properties": map[string]any{ + "extra_tuple": map[string]any{ + "type": "array", + "prefixItems": []any{ + map[string]any{"type": "integer"}, + }, + }, + }, + }, + }, + } + + cleaned := CleanJSONSchema(input) + require.NotNil(t, cleaned) + + props := cleaned["properties"].(map[string]any) + require.NotNil(t, props["extra_tuple"]) + extraTuple := props["extra_tuple"].(map[string]any) + assert.Nil(t, extraTuple["prefixItems"], "nested prefixItems from anyOf merge must be cleaned") + require.NotNil(t, extraTuple["items"]) + assert.Equal(t, "integer", extraTuple["items"].(map[string]any)["type"]) +} + +func TestCleanJSONSchema_EmptyPrefixItems(t *testing.T) { + // 验证空 prefixItems: [] 也被安全删除,不遗留非法关键字 + input := map[string]any{ + "type": "object", + "properties": map[string]any{ + "empty_tuple": map[string]any{ + "type": "array", + "prefixItems": []any{}, + }, + }, + } + + cleaned := CleanJSONSchema(input) + require.NotNil(t, cleaned) + + props := cleaned["properties"].(map[string]any) + emptyTuple := props["empty_tuple"].(map[string]any) + assert.Nil(t, emptyTuple["prefixItems"], "empty prefixItems array must be removed") + require.NotNil(t, emptyTuple["items"]) + assert.Equal(t, "string", emptyTuple["items"].(map[string]any)["type"]) +} + From 1e231ee2ae2da2b9d76267df48aa4eb3c1ed13c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E5=85=B0=E9=91=AB?= <1735612127@qq.com> Date: Tue, 15 Sep 2026 16:01:30 +0800 Subject: [PATCH 3/3] fix(antigravity): resolve golangci-lint errors in schema cleaner tests - Replace unchecked single-value type assertions with comma-ok form guarded by require.True (errcheck check-type-assertions) - Remove trailing blank line flagged by gofmt Co-Authored-By: Claude Code --- .../pkg/antigravity/schema_cleaner_test.go | 71 ++++++++++++------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/backend/internal/pkg/antigravity/schema_cleaner_test.go b/backend/internal/pkg/antigravity/schema_cleaner_test.go index 0401b670ad..d87a67a973 100644 --- a/backend/internal/pkg/antigravity/schema_cleaner_test.go +++ b/backend/internal/pkg/antigravity/schema_cleaner_test.go @@ -74,16 +74,22 @@ func TestCleanJSONSchema_ArrayMissingItemsFallback(t *testing.T) { cleaned := CleanJSONSchema(input) require.NotNil(t, cleaned) - props := cleaned["properties"].(map[string]any) - tags := props["tags"].(map[string]any) - require.NotNil(t, tags["items"]) - assert.Equal(t, "string", tags["items"].(map[string]any)["type"]) + props, ok := cleaned["properties"].(map[string]any) + require.True(t, ok) + tags, ok := props["tags"].(map[string]any) + require.True(t, ok) + tagsItems, ok := tags["items"].(map[string]any) + require.True(t, ok, "tags.items must be an object") + assert.Equal(t, "string", tagsItems["type"]) - nested := props["nested_empty_array"].(map[string]any) - nestedItems := nested["items"].(map[string]any) + nested, ok := props["nested_empty_array"].(map[string]any) + require.True(t, ok) + nestedItems, ok := nested["items"].(map[string]any) + require.True(t, ok) assert.Equal(t, "array", nestedItems["type"]) - require.NotNil(t, nestedItems["items"]) - assert.Equal(t, "string", nestedItems["items"].(map[string]any)["type"]) + nestedInnerItems, ok := nestedItems["items"].(map[string]any) + require.True(t, ok, "nested items.items must be an object") + assert.Equal(t, "string", nestedInnerItems["type"]) } func TestCleanJSONSchema_ArrayExistingItemsPreserved(t *testing.T) { @@ -103,9 +109,13 @@ func TestCleanJSONSchema_ArrayExistingItemsPreserved(t *testing.T) { cleaned := CleanJSONSchema(input) require.NotNil(t, cleaned) - numbers := cleaned["properties"].(map[string]any)["numbers"].(map[string]any) + props, ok := cleaned["properties"].(map[string]any) + require.True(t, ok) + numbers, ok := props["numbers"].(map[string]any) + require.True(t, ok) assert.Equal(t, "array", numbers["type"]) - items := numbers["items"].(map[string]any) + items, ok := numbers["items"].(map[string]any) + require.True(t, ok, "numbers.items must be an object") assert.Equal(t, "integer", items["type"]) } @@ -131,14 +141,18 @@ func TestCleanJSONSchema_EnumOnlySchemaInTuple(t *testing.T) { cleaned := CleanJSONSchema(input) require.NotNil(t, cleaned) - props := cleaned["properties"].(map[string]any) - choice := props["choice"].(map[string]any) + props, ok := cleaned["properties"].(map[string]any) + require.True(t, ok) + choice, ok := props["choice"].(map[string]any) + require.True(t, ok) assert.Equal(t, "string", choice["type"]) assert.Nil(t, choice["properties"], "enum-only schema must NOT be treated as object with reason property") assert.Equal(t, []any{"option_a", "option_b"}, choice["enum"]) - tupleArray := props["tuple_with_enum"].(map[string]any) - tupleItems := tupleArray["items"].(map[string]any) + tupleArray, ok := props["tuple_with_enum"].(map[string]any) + require.True(t, ok) + tupleItems, ok := tupleArray["items"].(map[string]any) + require.True(t, ok, "tuple_with_enum.items must be an object") assert.Equal(t, "string", tupleItems["type"]) assert.Nil(t, tupleItems["properties"]) assert.Equal(t, []any{"read", "write"}, tupleItems["enum"]) @@ -158,8 +172,10 @@ func TestCleanJSONSchema_ConstKeywordConversion(t *testing.T) { cleaned := CleanJSONSchema(input) require.NotNil(t, cleaned) - props := cleaned["properties"].(map[string]any) - action := props["action"].(map[string]any) + props, ok := cleaned["properties"].(map[string]any) + require.True(t, ok) + action, ok := props["action"].(map[string]any) + require.True(t, ok) assert.Nil(t, action["const"], "const keyword must be removed") assert.Equal(t, "string", action["type"]) assert.Equal(t, []any{"ping"}, action["enum"]) @@ -189,12 +205,15 @@ func TestCleanJSONSchema_AnyOfNestedPrefixItems(t *testing.T) { cleaned := CleanJSONSchema(input) require.NotNil(t, cleaned) - props := cleaned["properties"].(map[string]any) + props, ok := cleaned["properties"].(map[string]any) + require.True(t, ok) require.NotNil(t, props["extra_tuple"]) - extraTuple := props["extra_tuple"].(map[string]any) + extraTuple, ok := props["extra_tuple"].(map[string]any) + require.True(t, ok) assert.Nil(t, extraTuple["prefixItems"], "nested prefixItems from anyOf merge must be cleaned") - require.NotNil(t, extraTuple["items"]) - assert.Equal(t, "integer", extraTuple["items"].(map[string]any)["type"]) + extraItems, ok := extraTuple["items"].(map[string]any) + require.True(t, ok, "extra_tuple.items must be an object") + assert.Equal(t, "integer", extraItems["type"]) } func TestCleanJSONSchema_EmptyPrefixItems(t *testing.T) { @@ -212,10 +231,12 @@ func TestCleanJSONSchema_EmptyPrefixItems(t *testing.T) { cleaned := CleanJSONSchema(input) require.NotNil(t, cleaned) - props := cleaned["properties"].(map[string]any) - emptyTuple := props["empty_tuple"].(map[string]any) + props, ok := cleaned["properties"].(map[string]any) + require.True(t, ok) + emptyTuple, ok := props["empty_tuple"].(map[string]any) + require.True(t, ok) assert.Nil(t, emptyTuple["prefixItems"], "empty prefixItems array must be removed") - require.NotNil(t, emptyTuple["items"]) - assert.Equal(t, "string", emptyTuple["items"].(map[string]any)["type"]) + emptyTupleItems, ok := emptyTuple["items"].(map[string]any) + require.True(t, ok, "empty_tuple.items must be an object") + assert.Equal(t, "string", emptyTupleItems["type"]) } -